@ggui-ai/ui-gen 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.d.ts +24 -4
- package/dist/adapters/generation-dispatch.js +5188 -3920
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +33 -26
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +22 -13
- package/dist/advanced/index.js +4889 -3561
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +8 -12
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +19 -1
- package/dist/boilerplate.js +671 -254
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +17 -3
- package/dist/check/index.js +34 -25
- package/dist/check/index.js.map +1 -1
- package/dist/compose.js +1 -1
- package/dist/compose.js.map +1 -1
- package/dist/create-ui-generator-C8Ki9Wz1.d.ts +189 -0
- package/dist/design-mode-DLufhrNp.d.ts +61 -0
- package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-D2htzi9h.d.ts} +4 -1
- package/dist/evaluation/axis-checks/index.d.ts +4 -3
- package/dist/evaluation/axis-checks/index.js +300 -14
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +2 -1
- package/dist/evaluation/axis-checks/registry.js +253 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +297 -5
- package/dist/evaluation/index.js +4515 -120
- package/dist/evaluation/index.js.map +1 -1
- package/dist/fragments/index.js +1 -1
- package/dist/fragments/index.js.map +1 -1
- package/dist/{generate-BTfvELgl.d.ts → generate-9IeNT4ZY.d.ts} +10 -1
- package/dist/harness/check/runtime-render/index.d.ts +3 -1
- package/dist/harness/check/runtime-render/index.js +8 -12
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.d.ts +3 -1
- package/dist/harness/index.js +913 -441
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +3 -3
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -1
- package/dist/harness/prompts.js +6 -3
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +17 -5
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +28 -3
- package/dist/harness/runtime.js +702 -274
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +39 -1
- package/dist/harness/types-public.js +913 -441
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -1
- package/dist/index.d.ts +32 -6
- package/dist/index.js +4899 -3560
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +3 -1
- package/dist/{prompts-BVhtH3M1.d.ts → prompts-YU9piOgs.d.ts} +21 -45
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/styling-profile-CxfvV48g.d.ts +35 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-CkNWEov9.d.ts → types-public-DVSssdc-.d.ts} +85 -5
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +4 -4
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +3 -1
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +62 -4
- package/src/adapters/provider-router.ts +7 -6
- package/src/adapters/tools.ts +5 -5
- package/src/advanced/generator.ts +11 -10
- package/src/advanced/index.ts +5 -4
- package/src/boilerplate/free-design-prompt.ts +336 -0
- package/src/boilerplate/generate.ts +10 -1
- package/src/boilerplate/hard-sections.ts +257 -0
- package/src/boilerplate/render.ts +32 -8
- package/src/boilerplate/styling-profile.ts +166 -0
- package/src/boilerplate/system-prompt.ts +101 -237
- package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
- package/src/check/run-tier0.ts +82 -20
- package/src/coding-agent/tools.ts +8 -2
- package/src/contract-context.ts +43 -14
- package/src/create-harness.ts +15 -0
- package/src/create-ui-generator.ts +112 -27
- package/src/design-mode.ts +192 -0
- package/src/design-system-docs.ts +24 -17
- package/src/effort.ts +48 -0
- package/src/evaluation/axis-checks/checks/grid.ts +148 -0
- package/src/evaluation/axis-checks/checks/universal.ts +244 -0
- package/src/evaluation/axis-checks/dispatch.ts +82 -13
- package/src/evaluation/axis-checks/extras.ts +5 -0
- package/src/evaluation/axis-checks/helpers.ts +12 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/cost-tracker.ts +2 -3
- package/src/evaluation/design-identity.ts +96 -0
- package/src/evaluation/index.ts +63 -2
- package/src/evaluation/llm-evaluator.ts +89 -21
- package/src/evaluation/theme-css.ts +14 -0
- package/src/evaluation/types-public.ts +137 -8
- package/src/evaluation/visual-evaluator.ts +759 -54
- package/src/fragments/render.ts +1 -1
- package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
- package/src/harness/coding/dupe-break.ts +0 -0
- package/src/harness/coding/init-session.ts +2 -0
- package/src/harness/coding/run-coding-turn.ts +9 -14
- package/src/harness/coding/run-eval-round.ts +46 -5
- package/src/harness/llm-trace-sink.ts +3 -3
- package/src/harness/result-types.ts +15 -3
- package/src/harness/runtime.ts +28 -0
- package/src/harness/types-public.ts +38 -0
- package/src/index.ts +28 -0
- package/src/internal/extract-call-signatures.ts +2 -2
- package/src/pin-fixtures.ts +98 -0
- package/src/providers/bedrock.ts +4 -4
- package/src/run-check.ts +11 -10
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools.ts +2 -1
- package/src/validation/allowed-imports.ts +8 -2
- package/src/validation/primitives.ts +1 -1
- package/dist/create-ui-generator-D6TKuv9V.d.ts +0 -106
package/src/fragments/render.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const renderFragments: Record<string, HarnessFragment> = {
|
|
|
27
27
|
value: "grid",
|
|
28
28
|
cacheTier: "axisDelta",
|
|
29
29
|
promptText:
|
|
30
|
-
"## Render: grid\
|
|
30
|
+
"## Render: grid\nTwo shapes share this axis — read the contract:\n- A TILE grid (a gallery, a stat grid, items that may carry row/col fields): `<Grid columns={N} gap=\"md\">` (or `<CardGrid>`); if items carry row/col fields, position each tile at (row, col).\n- A BOARD (a columns / lanes / stages array whose items are cards — a kanban): the columns sit SIDE BY SIDE at md and wider — `<Grid columns={{ base: 1, md: columns.length }} gap=\"md\">` wrapping the columns map, each column its own `<Stack>` of `<Card>`s with its header and count; NEVER a `<Stack>` of column sections (that is a list — tier-0 fails it).\nNever hand-roll `display: grid`. Do not scroll horizontally.",
|
|
31
31
|
},
|
|
32
32
|
spatial: {
|
|
33
33
|
axis: "render",
|
|
@@ -129,6 +129,16 @@ function synthesizeFromSchema(
|
|
|
129
129
|
hint: string,
|
|
130
130
|
warnings: string[],
|
|
131
131
|
depth: number,
|
|
132
|
+
/**
|
|
133
|
+
* 1-based position when the value belongs to an array element (inherited
|
|
134
|
+
* by the element's own fields). Every synthesized string inside an element
|
|
135
|
+
* carries it, so two elements never share an id or a visible label — the
|
|
136
|
+
* runtime probe's selection-identity rule ("clicking each fired the action
|
|
137
|
+
* with the same id") is undecidable on identical items and read a correct
|
|
138
|
+
* component as broken (2026-09-10, the 1.6 hello: both quick replies were
|
|
139
|
+
* `Sample Id` / `Sample Label`).
|
|
140
|
+
*/
|
|
141
|
+
ordinal?: number,
|
|
132
142
|
): SynthOk | SynthFail {
|
|
133
143
|
if (!schema) return { kind: "fail", reason: "no schema" };
|
|
134
144
|
if (depth > MAX_DEPTH) return { kind: "fail", reason: "schema too deep" };
|
|
@@ -146,10 +156,10 @@ function synthesizeFromSchema(
|
|
|
146
156
|
|
|
147
157
|
// Union schemas — try first branch only
|
|
148
158
|
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
149
|
-
return synthesizeFromSchema(schema.oneOf[0], hint, warnings, depth + 1);
|
|
159
|
+
return synthesizeFromSchema(schema.oneOf[0], hint, warnings, depth + 1, ordinal);
|
|
150
160
|
}
|
|
151
161
|
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
152
|
-
return synthesizeFromSchema(schema.anyOf[0], hint, warnings, depth + 1);
|
|
162
|
+
return synthesizeFromSchema(schema.anyOf[0], hint, warnings, depth + 1, ordinal);
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
// Draft-07 type array (`['string','null']` — the canonical nullable
|
|
@@ -158,12 +168,7 @@ function synthesizeFromSchema(
|
|
|
158
168
|
// above bounds the recursion.
|
|
159
169
|
if (Array.isArray(schema.type)) {
|
|
160
170
|
const primary = schema.type.find((t) => t !== "null") ?? "null";
|
|
161
|
-
return synthesizeFromSchema(
|
|
162
|
-
{ ...schema, type: primary },
|
|
163
|
-
hint,
|
|
164
|
-
warnings,
|
|
165
|
-
depth + 1,
|
|
166
|
-
);
|
|
171
|
+
return synthesizeFromSchema({ ...schema, type: primary }, hint, warnings, depth + 1, ordinal);
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
switch (schema.type) {
|
|
@@ -184,7 +189,9 @@ function synthesizeFromSchema(
|
|
|
184
189
|
// Use field name as a hint so it appears in DOM and prop-coverage check sees it.
|
|
185
190
|
// Capitalize first letter for natural-looking placeholder.
|
|
186
191
|
const cap = hint.charAt(0).toUpperCase() + hint.slice(1);
|
|
187
|
-
|
|
192
|
+
// Inside an array element every string is distinct per element.
|
|
193
|
+
const value = ordinal === undefined ? `Sample ${cap}` : `Sample ${cap} ${ordinal}`;
|
|
194
|
+
return { kind: "ok", value, source: "schema-synth" };
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
}
|
|
@@ -211,7 +218,7 @@ function synthesizeFromSchema(
|
|
|
211
218
|
const items: JsonValue[] = [];
|
|
212
219
|
for (let i = 0; i < 2; i++) {
|
|
213
220
|
const itemHint = `${hint}Item${i + 1}`;
|
|
214
|
-
const itemSynth = synthesizeFromSchema(schema.items, itemHint, warnings, depth + 1);
|
|
221
|
+
const itemSynth = synthesizeFromSchema(schema.items, itemHint, warnings, depth + 1, i + 1);
|
|
215
222
|
if (itemSynth.kind === "ok") {
|
|
216
223
|
// Inject an `id` for list-key purposes if the item is an object lacking one.
|
|
217
224
|
if (
|
|
@@ -233,7 +240,7 @@ function synthesizeFromSchema(
|
|
|
233
240
|
const propsMap = schema.properties ?? {};
|
|
234
241
|
const required = new Set(schema.required ?? []);
|
|
235
242
|
for (const [k, sub] of Object.entries(propsMap)) {
|
|
236
|
-
const subSynth = synthesizeFromSchema(sub, k, warnings, depth + 1);
|
|
243
|
+
const subSynth = synthesizeFromSchema(sub, k, warnings, depth + 1, ordinal);
|
|
237
244
|
if (subSynth.kind === "ok") {
|
|
238
245
|
obj[k] = subSynth.value;
|
|
239
246
|
} else if (required.has(k)) {
|
|
Binary file
|
|
@@ -168,6 +168,8 @@ export async function initSession(input: {
|
|
|
168
168
|
originalPrompt: params.userPrompt,
|
|
169
169
|
contract: params.contract,
|
|
170
170
|
designContext: undefined,
|
|
171
|
+
designMode: harness.designMode,
|
|
172
|
+
profile: harness.profile,
|
|
171
173
|
},
|
|
172
174
|
{
|
|
173
175
|
provider: mapProviderForEvaluator(evalSpec.provider),
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
WRITE_PLAN_TOOL,
|
|
34
34
|
type LLMToolDef,
|
|
35
35
|
} from "../../tools.js";
|
|
36
|
+
import { LUCIDE_ICON_NAMES } from "@ggui-ai/design";
|
|
36
37
|
import { formatWithHashlines } from "../hashline.js";
|
|
37
38
|
import {
|
|
38
39
|
recordToolExchange,
|
|
@@ -592,20 +593,11 @@ ${closingInstruction}`;
|
|
|
592
593
|
// Handle get_available_icons — return icon list without consuming a turn
|
|
593
594
|
if (call.name === "get_available_icons") {
|
|
594
595
|
if (!iconNamesCache) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
).pathname;
|
|
601
|
-
const { LUCIDE_ICONS } = await import(iconDataPath);
|
|
602
|
-
iconNamesCache = Object.keys(LUCIDE_ICONS)
|
|
603
|
-
.map((n) => n.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase())
|
|
604
|
-
.join(", ");
|
|
605
|
-
} catch {
|
|
606
|
-
iconNamesCache =
|
|
607
|
-
"sun, moon, cloud, cloud-rain, heart, star, search, home, plus, minus, check, x, arrow-left, arrow-right, chevron-down, chevron-up, menu, user, settings, bell, mail, phone, shopping-cart, download, upload, edit, trash-2, save, eye, lock, globe, map-pin, calendar, clock, zap, alert-circle, info, help-circle";
|
|
608
|
-
}
|
|
596
|
+
// The one list the Icon primitive renders (ggui#1015) — the same
|
|
597
|
+
// export the check leg reads, so the tool never advertises a name
|
|
598
|
+
// the primitive does not know (the former dist-relative dynamic
|
|
599
|
+
// import fell back to a 40-name string whenever the path missed).
|
|
600
|
+
iconNamesCache = LUCIDE_ICON_NAMES.join(", ");
|
|
609
601
|
}
|
|
610
602
|
console.log(`[simple] turn ${turnsUsed}: get_available_icons`);
|
|
611
603
|
let iconResult = `Available Lucide icon names (use with <Icon name="...">):\n${iconNamesCache}`;
|
|
@@ -674,6 +666,9 @@ ${closingInstruction}`;
|
|
|
674
666
|
// typecheck overlays the real wrapper declarations and a generated
|
|
675
667
|
// direct gadget import gets strict option/return narrowing.
|
|
676
668
|
harness.what.gadgetTypes,
|
|
669
|
+
// Which tier-0 legs fire — the design-vocabulary legs stand down in
|
|
670
|
+
// `free` mode; every contract leg runs in both.
|
|
671
|
+
harness.designMode,
|
|
677
672
|
);
|
|
678
673
|
const toolMs = Date.now() - toolStart;
|
|
679
674
|
|
|
@@ -22,7 +22,7 @@ import { listContractGadgets } from "@ggui-ai/protocol";
|
|
|
22
22
|
import type { Classification } from "../../classifier/index.js";
|
|
23
23
|
import type { AgentWorkspace } from "../../coding-agent/workspace.js";
|
|
24
24
|
import type { CostTracker } from "../../evaluation/cost-tracker.js";
|
|
25
|
-
import type { EvalIssue, EvalResult, RuntimeProbeMeta } from "../../evaluation/types-public.js";
|
|
25
|
+
import type { EvalIssue, EvalResult, RuntimeProbeMeta, VisualEvalSummary } from "../../evaluation/types-public.js";
|
|
26
26
|
import { notApplicableCoverage } from "../../evaluation/types-public.js";
|
|
27
27
|
import { mapProviderForEvaluator } from "../enforced-coding.js";
|
|
28
28
|
import { runCheck } from "../index.js";
|
|
@@ -128,6 +128,9 @@ const MAX_FEEDBACK_ISSUES = 3;
|
|
|
128
128
|
* criterion is `not-applicable` (excluded from coverage denominators),
|
|
129
129
|
* not `skipped` (which would count as silent absence).
|
|
130
130
|
*/
|
|
131
|
+
/** Why an eval result carries no criteria: the round itself threw (ggui#1110). */
|
|
132
|
+
const EVAL_ROUND_THREW_REASON = "eval round threw";
|
|
133
|
+
|
|
131
134
|
const LOW_RISK_BYPASS_REASON =
|
|
132
135
|
"same-image low-risk bypass: axis checks clean, tier-1/2 evaluation not invoked";
|
|
133
136
|
|
|
@@ -453,6 +456,11 @@ export async function runEvalRound(
|
|
|
453
456
|
sourceCode: currentSource,
|
|
454
457
|
originalPrompt: userPrompt,
|
|
455
458
|
contract,
|
|
459
|
+
// Criteria + mother prompt follow the triad that produced
|
|
460
|
+
// the source (free mode: color-only tokens, arm-neutral
|
|
461
|
+
// visual judging).
|
|
462
|
+
designMode: harness.designMode,
|
|
463
|
+
profile: harness.profile,
|
|
456
464
|
},
|
|
457
465
|
{
|
|
458
466
|
provider: mapProviderForEvaluator(evaluationAgent.provider),
|
|
@@ -476,13 +484,25 @@ export async function runEvalRound(
|
|
|
476
484
|
: null,
|
|
477
485
|
visualMod
|
|
478
486
|
? visualMod.runVisualEval(
|
|
479
|
-
{
|
|
487
|
+
{
|
|
488
|
+
compiledCode,
|
|
489
|
+
originalPrompt: userPrompt,
|
|
490
|
+
profile: harness.profile,
|
|
491
|
+
// The caller's theme, when named — the round paints what the app paints.
|
|
492
|
+
...(visualEvaluation?.cssTokens !== undefined ? { cssTokens: visualEvaluation.cssTokens } : {}),
|
|
493
|
+
},
|
|
480
494
|
{
|
|
481
495
|
provider: mapProviderForEvaluator(visualEvalAgent.provider) as "claude" | "google",
|
|
482
496
|
model: visualEvalAgent.model,
|
|
483
497
|
passThreshold: visualThreshold,
|
|
484
|
-
|
|
498
|
+
// The in-loop visual round renders the same sample the runtime probe
|
|
499
|
+
// does (the caller's fixture) unless the config names its own.
|
|
500
|
+
sampleProps: visualEvaluation?.sampleProps ?? fixtureProps,
|
|
485
501
|
viewport: visualEvaluation?.viewport,
|
|
502
|
+
// Per-canvas judging (arm-neutral) — absent = single shot.
|
|
503
|
+
...(visualEvaluation?.canvases !== undefined
|
|
504
|
+
? { canvases: visualEvaluation.canvases }
|
|
505
|
+
: {}),
|
|
486
506
|
// #484/#489 — same threading as the LLM-eval leg above:
|
|
487
507
|
// the visual eval's multimodal call runs inside the same
|
|
488
508
|
// concurrent generation, so it needs the same routing
|
|
@@ -497,7 +517,8 @@ export async function runEvalRound(
|
|
|
497
517
|
]);
|
|
498
518
|
evalLlmMs = Date.now() - evalLlmStart;
|
|
499
519
|
llmResult = llm;
|
|
500
|
-
visualIssues = visual;
|
|
520
|
+
visualIssues = visual?.issues ?? null;
|
|
521
|
+
const visualSummary: VisualEvalSummary | undefined = visual?.summary;
|
|
501
522
|
|
|
502
523
|
// Track costs from LLM eval calls
|
|
503
524
|
if (llmResult) {
|
|
@@ -524,6 +545,10 @@ export async function runEvalRound(
|
|
|
524
545
|
...(llmResult?.criteriaCoverage !== undefined
|
|
525
546
|
? { criteriaCoverage: llmResult.criteriaCoverage }
|
|
526
547
|
: {}),
|
|
548
|
+
// Per-canvas visual summary — present only when canvases were
|
|
549
|
+
// requested and the leg ran; every later re-stamp spreads this
|
|
550
|
+
// base so it survives to the assembled result.
|
|
551
|
+
...(visualSummary !== undefined ? { visual: visualSummary } : {}),
|
|
527
552
|
};
|
|
528
553
|
|
|
529
554
|
// ── Log merged results, with mode-check subcategory breakdown ──
|
|
@@ -868,7 +893,23 @@ export async function runEvalRound(
|
|
|
868
893
|
lastDiffFailed: false,
|
|
869
894
|
};
|
|
870
895
|
} catch (e) {
|
|
871
|
-
|
|
896
|
+
// The round threw. Until ggui#1110 this returned `evalResult` UNDEFINED and
|
|
897
|
+
// said so only in a console line: downstream the generation carried no
|
|
898
|
+
// evaluation at all, an offline mint exported no `eval.json`, and the bar
|
|
899
|
+
// read the missing file as "probe skipped" — a thrown evaluation and an
|
|
900
|
+
// evaluation that never ran became the same fact, with the reason lost.
|
|
901
|
+
// Now the failure IS the result: the round reports what threw, on the
|
|
902
|
+
// channel a judge reads, and can never be mistaken for a pass (`pass` is
|
|
903
|
+
// empty, the probe status is `infra-skipped`, and a bar that requires the
|
|
904
|
+
// probe still refuses — with a cause instead of a silence).
|
|
905
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
906
|
+
console.error("[simple] eval failed:", message);
|
|
907
|
+
evalResult = evalResult ?? {
|
|
908
|
+
issues: [],
|
|
909
|
+
pass: [],
|
|
910
|
+
criteriaCoverage: notApplicableCoverage(`${EVAL_ROUND_THREW_REASON}: ${message}`),
|
|
911
|
+
runtimeProbe: { status: "infra-skipped", reason: `${EVAL_ROUND_THREW_REASON}: ${message}` },
|
|
912
|
+
};
|
|
872
913
|
return {
|
|
873
914
|
control: "break",
|
|
874
915
|
evalDone: false,
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* (createAgent → run-coding-turn → llm-evaluator → file-agent → …)
|
|
22
22
|
* touches ~5 packages for a devtools-only surface. The OSS ggui server
|
|
23
23
|
* is a single process per CLI invocation — global state has no
|
|
24
|
-
* confusion-cost there. A hosted
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* confusion-cost there. A hosted deployment isolates per request via a
|
|
25
|
+
* process pool, so a global per-pool is also safe. If we ever serve
|
|
26
|
+
* several apps inside one process we'll thread it then.
|
|
27
27
|
*
|
|
28
28
|
* **Default = no sink.** When unset, the router emits nothing and
|
|
29
29
|
* spends no CPU formatting events. Passing `null` removes a previously
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
import type { DataContract, JsonObject } from "@ggui-ai/protocol";
|
|
13
13
|
import type { AdapterResult } from "../adapters/types.js";
|
|
14
14
|
import type { RenderingContext } from "../contract-context.js";
|
|
15
|
-
import type { EvaluationResult } from "../evaluation/types.js";
|
|
16
15
|
import type { EvalResult } from "../evaluation/types-public.js";
|
|
16
|
+
import type { CanvasClass, DesignMode } from "../design-mode.js";
|
|
17
17
|
|
|
18
18
|
// =============================================================================
|
|
19
19
|
// Model Roles
|
|
@@ -52,8 +52,6 @@ export type ModelRoles = Partial<Record<ModelRole, string>>;
|
|
|
52
52
|
export interface GenerationResult extends AdapterResult {
|
|
53
53
|
/** Number of generation passes the harness performed */
|
|
54
54
|
passesUsed: number;
|
|
55
|
-
/** Evaluation results per round (legacy numeric scoring — backward compat) */
|
|
56
|
-
evaluations?: EvaluationResult[];
|
|
57
55
|
/** Three-tier evaluation result (tier 0 + LLM tier 1+2 + visual) */
|
|
58
56
|
evalResult?: EvalResult;
|
|
59
57
|
/** Whether background improvement should be spawned (auto-improve mode) */
|
|
@@ -62,6 +60,20 @@ export interface GenerationResult extends AdapterResult {
|
|
|
62
60
|
selfCheckPassed?: boolean;
|
|
63
61
|
/** Which model was used for each role (for benchmark reporting) */
|
|
64
62
|
modelRolesUsed?: Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* The triad this generation ran under — recorded ONLY when the caller
|
|
65
|
+
* set `designMode` on the dispatch (a bench arm switch, an operator
|
|
66
|
+
* option). Absent = the constrained default path, byte-identical to
|
|
67
|
+
* the pre-`designMode` result.
|
|
68
|
+
*/
|
|
69
|
+
designMode?: DesignMode;
|
|
70
|
+
/**
|
|
71
|
+
* The canvas class the `free` prompt stated — the explicit `canvas`
|
|
72
|
+
* when the caller passed one, else the shell × screen derivation the
|
|
73
|
+
* prompt itself used. Absent in constrained mode (that prompt states
|
|
74
|
+
* no canvas) unless the caller passed one explicitly.
|
|
75
|
+
*/
|
|
76
|
+
canvas?: CanvasClass;
|
|
65
77
|
/** Phase-level timing breakdown (ms) */
|
|
66
78
|
timing?: Record<string, number>;
|
|
67
79
|
/** Per-turn/per-outcome breakdown for benchmark analysis.
|
package/src/harness/runtime.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// directly to the coding agent in the system prompt — there is no
|
|
25
25
|
// separate planner stage.
|
|
26
26
|
|
|
27
|
+
import type { GenerationProfileInput } from "../boilerplate/styling-profile.js";
|
|
27
28
|
import type { QualityConfig } from "../evaluation/types-public.js";
|
|
28
29
|
import { DEFAULT_DESIGN_SYSTEM_DOCS } from "../design-system-docs.js";
|
|
29
30
|
import { PRIMITIVES_DOCUMENTATION } from "../validation/index.js";
|
|
@@ -32,6 +33,7 @@ import { renderPitfallsBlock } from "./pitfalls.js";
|
|
|
32
33
|
import type { GadgetDescriptor, DataContract, JsonObject } from "@ggui-ai/protocol";
|
|
33
34
|
import { buildSystemPrompt as buildSystemPromptSkeleton } from "../boilerplate.js";
|
|
34
35
|
import type { AgentConfig } from "./llm-router.js";
|
|
36
|
+
import type { CanvasClass, DesignMode } from "../design-mode.js";
|
|
35
37
|
|
|
36
38
|
// Re-export the boilerplate generator so existing internal importers
|
|
37
39
|
// (create-harness.ts, benchmarks/preview-boilerplate.ts) keep working.
|
|
@@ -82,6 +84,19 @@ export interface SingleComponentParams {
|
|
|
82
84
|
passThreshold?: number;
|
|
83
85
|
sampleProps?: JsonObject;
|
|
84
86
|
viewport?: { width: number; height: number };
|
|
87
|
+
/**
|
|
88
|
+
* Per-canvas judging — see `VisualEvalConfig.canvases`. When set, the
|
|
89
|
+
* eval round screenshots + judges once per class and the harness
|
|
90
|
+
* stamps `EvalResult.visual` with the PNG-free summary. Unset =
|
|
91
|
+
* today's single screenshot at `viewport`.
|
|
92
|
+
*/
|
|
93
|
+
canvases?: readonly CanvasClass[];
|
|
94
|
+
/**
|
|
95
|
+
* The stylesheet the in-loop visual round renders under (a caller's
|
|
96
|
+
* theme composed by `cssTokensForAppTheme`); absent = the design's
|
|
97
|
+
* default tokens. Never part of the prompt or the identity.
|
|
98
|
+
*/
|
|
99
|
+
cssTokens?: string;
|
|
85
100
|
};
|
|
86
101
|
onProgress?: (event: unknown) => void;
|
|
87
102
|
onInitialResult?: (result: {
|
|
@@ -133,6 +148,16 @@ export function buildSystemPrompt(
|
|
|
133
148
|
* stdlib gadgets do not need one.
|
|
134
149
|
*/
|
|
135
150
|
gadgetTypes?: Readonly<Record<string, string>>,
|
|
151
|
+
/**
|
|
152
|
+
* Which triad to teach — see `SystemPromptInputs.designMode`. Omitted
|
|
153
|
+
* (every pre-existing caller) means `constrained`, byte-identical to
|
|
154
|
+
* the pre-`designMode` prompt.
|
|
155
|
+
*/
|
|
156
|
+
designMode?: DesignMode,
|
|
157
|
+
/** Rendering canvas class (`free` mode) — see `SystemPromptInputs.canvas`. */
|
|
158
|
+
canvas?: CanvasClass,
|
|
159
|
+
/** The app's generation profile (#991). */
|
|
160
|
+
profile?: GenerationProfileInput,
|
|
136
161
|
): string {
|
|
137
162
|
return buildSystemPromptSkeleton({
|
|
138
163
|
userRequest,
|
|
@@ -145,6 +170,9 @@ export function buildSystemPrompt(
|
|
|
145
170
|
wireDoc: WIRE_DOCUMENTATION,
|
|
146
171
|
appGadgets,
|
|
147
172
|
gadgetTypes,
|
|
173
|
+
designMode,
|
|
174
|
+
canvas,
|
|
175
|
+
profile,
|
|
148
176
|
// criteriaBlock left undefined — ui-gen fills default from open CRITERIA.
|
|
149
177
|
});
|
|
150
178
|
}
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
// types hub for the harness module. `AxisCheck` + `AxisCheckInput` live
|
|
19
19
|
// in `@ggui-ai/ui-gen/evaluation`.
|
|
20
20
|
|
|
21
|
+
import type { GenerationProfileInput } from "../boilerplate/styling-profile.js";
|
|
21
22
|
import type { GadgetDescriptor, DataContract, JsonValue } from "@ggui-ai/protocol";
|
|
22
23
|
import type { Classification } from "../classifier/axes.js";
|
|
23
24
|
import type { EvalIssue, EvalTier, AxisCheck, RuntimeProbeStatus } from "../evaluation/types-public.js";
|
|
24
25
|
import type { LLMToolDef } from "../llm.js";
|
|
25
26
|
import type { CacheTier, HarnessFragment } from "../fragments/index.js";
|
|
26
27
|
import type { HarnessPolicy, ProcessMode } from "../policy.js";
|
|
28
|
+
import type { CanvasClass, DesignMode } from "../design-mode.js";
|
|
27
29
|
|
|
28
30
|
export type { ProcessMode };
|
|
29
31
|
|
|
@@ -293,6 +295,16 @@ export interface Harness {
|
|
|
293
295
|
readonly id: HarnessId;
|
|
294
296
|
readonly name: HarnessName;
|
|
295
297
|
readonly classification: Classification;
|
|
298
|
+
/**
|
|
299
|
+
* Which triad this harness materialised — threaded from
|
|
300
|
+
* `CreateHarnessInput.designMode` into the HOW leg (prompt), the WHAT
|
|
301
|
+
* leg (boilerplate) and every CHECK consumer (tier-0 gate, axis
|
|
302
|
+
* checks, evaluator criteria). `constrained` unless the caller opted
|
|
303
|
+
* into `free`.
|
|
304
|
+
*/
|
|
305
|
+
readonly designMode: DesignMode;
|
|
306
|
+
/** The app's generation profile in force for this harness (#991), if any. */
|
|
307
|
+
readonly profile?: GenerationProfileInput;
|
|
296
308
|
|
|
297
309
|
readonly how: HowLeg;
|
|
298
310
|
readonly what: WhatLeg;
|
|
@@ -383,6 +395,12 @@ export type SystemPromptBuilder = (input: {
|
|
|
383
395
|
readonly shellType?: string;
|
|
384
396
|
readonly screen?: string;
|
|
385
397
|
readonly axisDelta?: string;
|
|
398
|
+
/** Which triad to teach — see `CreateHarnessInput.designMode`. */
|
|
399
|
+
readonly designMode?: DesignMode;
|
|
400
|
+
/** Rendering canvas class (`free` mode) — see `CreateHarnessInput.canvas`. */
|
|
401
|
+
readonly canvas?: CanvasClass;
|
|
402
|
+
/** The app's generation profile (#991) — see `CreateHarnessInput.profile`. */
|
|
403
|
+
readonly profile?: GenerationProfileInput;
|
|
386
404
|
}) => string;
|
|
387
405
|
|
|
388
406
|
export interface CreateHarnessInput {
|
|
@@ -392,6 +410,26 @@ export interface CreateHarnessInput {
|
|
|
392
410
|
readonly shellType?: "chat" | "fullscreen" | "spatial";
|
|
393
411
|
readonly screen?: "mobile" | "tablet" | "desktop" | "universal";
|
|
394
412
|
readonly overrides?: HarnessOverrides;
|
|
413
|
+
/**
|
|
414
|
+
* Which triad to materialise. `constrained` (default) is today's
|
|
415
|
+
* design-system triad, byte-identical to the pre-`designMode`
|
|
416
|
+
* harness (same prompt, boilerplate, tier-0 legs, criteria and id).
|
|
417
|
+
* `free` relaxes only the design vocabulary; every hard contract
|
|
418
|
+
* stays. See `../design-mode.ts`.
|
|
419
|
+
*/
|
|
420
|
+
readonly designMode?: DesignMode;
|
|
421
|
+
/**
|
|
422
|
+
* Rendering canvas class the `free` prompt states explicitly. When
|
|
423
|
+
* omitted the prompt derives it from `shellType` × `screen`. Ignored
|
|
424
|
+
* by the `constrained` prompt.
|
|
425
|
+
*/
|
|
426
|
+
readonly canvas?: CanvasClass;
|
|
427
|
+
/**
|
|
428
|
+
* The app's generation profile (#991): operator-declared free text the
|
|
429
|
+
* HOW leg renders as one bounded section and every judge reads under its
|
|
430
|
+
* own frame. Absent = byte-identical prompts.
|
|
431
|
+
*/
|
|
432
|
+
readonly profile?: GenerationProfileInput;
|
|
395
433
|
/**
|
|
396
434
|
* Pre-resolved static harness policy. When omitted, `createHarness`
|
|
397
435
|
* falls back to `DEFAULT_HARNESS_POLICY` from `@ggui-ai/ui-gen/policy`.
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
} from './create-ui-generator.js';
|
|
22
22
|
export type { CreateUiGeneratorOptions } from './create-ui-generator.js';
|
|
23
23
|
export type { ProviderRetryInfo } from './harness/llm-router.js';
|
|
24
|
+
export type { GenerationResult } from './harness/result-types.js';
|
|
24
25
|
|
|
25
26
|
// Generator registry seam re-export. The interface + slug helpers live
|
|
26
27
|
// in `@ggui-ai/mcp-server-core`; the in-memory factory lives in
|
|
@@ -49,6 +50,31 @@ export {
|
|
|
49
50
|
injectVariance,
|
|
50
51
|
} from './contract-context.js';
|
|
51
52
|
export type { RenderingContext } from './contract-context.js';
|
|
53
|
+
export { canvasForRenderingContext } from './contract-context.js';
|
|
54
|
+
|
|
55
|
+
// ── Design mode ──────────────────────────────────────────────────────
|
|
56
|
+
//
|
|
57
|
+
// `createUiGenerator({ designMode })` — `constrained` (default) or
|
|
58
|
+
// `free`; the canvas vocabulary the `free` prompt states.
|
|
59
|
+
export {
|
|
60
|
+
DESIGN_MODES,
|
|
61
|
+
DEFAULT_DESIGN_MODE,
|
|
62
|
+
CANVAS_CLASSES,
|
|
63
|
+
CANVAS_VIEWPORTS,
|
|
64
|
+
CANVAS_BREAKPOINTS,
|
|
65
|
+
CANVAS_DESCRIPTORS,
|
|
66
|
+
canvasDisplayModeForShell,
|
|
67
|
+
canvasForRendering,
|
|
68
|
+
canvasForViewportWidth,
|
|
69
|
+
describeCanvas,
|
|
70
|
+
} from './design-mode.js';
|
|
71
|
+
export type {
|
|
72
|
+
DesignMode,
|
|
73
|
+
CanvasClass,
|
|
74
|
+
CanvasViewport,
|
|
75
|
+
CanvasDisplayMode,
|
|
76
|
+
CanvasDescriptor,
|
|
77
|
+
} from './design-mode.js';
|
|
52
78
|
|
|
53
79
|
// ── Anthropic SDK construction ──────────────────────────────────────
|
|
54
80
|
//
|
|
@@ -56,3 +82,5 @@ export type { RenderingContext } from './contract-context.js';
|
|
|
56
82
|
// go through this helper instead of `new Anthropic(...)` so future
|
|
57
83
|
// header / baseURL tweaks land in one place.
|
|
58
84
|
export { createAnthropicClient } from './adapters/claude/client.js';
|
|
85
|
+
export { AESTHETIC_BRIEF_MAX_CHARS, type GenerationProfileInput } from './boilerplate/styling-profile.js';
|
|
86
|
+
export { EFFORT_DIALS, EFFORT_TABLE_VERSION, effortDials, type EffortDials } from './effort.js';
|
|
@@ -419,7 +419,7 @@ export function extractCallSignaturesFromDts(
|
|
|
419
419
|
return {};
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
const cacheKey = `${dtsContent}
|
|
422
|
+
const cacheKey = `${dtsContent}\u0000${[...hookNames].sort().join(',')}`;
|
|
423
423
|
const cached = callSignatureCache.get(cacheKey);
|
|
424
424
|
if (cached !== undefined) return cached;
|
|
425
425
|
|
|
@@ -470,7 +470,7 @@ export function extractComponentPropsFromDts(
|
|
|
470
470
|
return {};
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
const cacheKey = `${dtsContent}
|
|
473
|
+
const cacheKey = `${dtsContent}\u0000${[...componentNames].sort().join(',')}`;
|
|
474
474
|
const cached = componentPropsCache.get(cacheKey);
|
|
475
475
|
if (cached !== undefined) return cached;
|
|
476
476
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two fixed fixtures every prompt pin renders — one action-bearing
|
|
3
|
+
* `chat`×`mobile` shell, one `fullscreen`×`desktop` board with a
|
|
4
|
+
* `contextSpec` — shared by `design-mode.pin.test.ts` (the stable prefix)
|
|
5
|
+
* and `shape-guidance.pin.test.ts` (the axis-conditioned Shape Guidance).
|
|
6
|
+
*/
|
|
7
|
+
import type { DataContract } from '@ggui-ai/protocol';
|
|
8
|
+
|
|
9
|
+
export interface PinFixture {
|
|
10
|
+
readonly userRequest: string;
|
|
11
|
+
readonly shellType: 'chat' | 'fullscreen' | 'spatial';
|
|
12
|
+
readonly screen: 'mobile' | 'tablet' | 'desktop' | 'universal';
|
|
13
|
+
readonly contract: DataContract;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PIN_FIXTURES: readonly PinFixture[] = [
|
|
17
|
+
{
|
|
18
|
+
userRequest: 'A todo list where each item can be toggled done',
|
|
19
|
+
shellType: 'chat',
|
|
20
|
+
screen: 'mobile',
|
|
21
|
+
contract: {
|
|
22
|
+
propsSpec: {
|
|
23
|
+
properties: {
|
|
24
|
+
todos: {
|
|
25
|
+
required: true,
|
|
26
|
+
description: 'The todos to show',
|
|
27
|
+
schema: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
id: { type: 'string' },
|
|
33
|
+
title: { type: 'string' },
|
|
34
|
+
done: { type: 'boolean' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
actionSpec: {
|
|
42
|
+
toggleTodo: {
|
|
43
|
+
label: 'Toggle',
|
|
44
|
+
description: 'Flip the done state of one todo',
|
|
45
|
+
schema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: { id: { type: 'string' } },
|
|
48
|
+
required: ['id'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
userRequest: 'A kanban board with live task updates and a selected card',
|
|
56
|
+
shellType: 'fullscreen',
|
|
57
|
+
screen: 'desktop',
|
|
58
|
+
contract: {
|
|
59
|
+
propsSpec: {
|
|
60
|
+
properties: {
|
|
61
|
+
columns: {
|
|
62
|
+
required: true,
|
|
63
|
+
schema: { type: 'array', items: { type: 'string' } },
|
|
64
|
+
},
|
|
65
|
+
tasks: {
|
|
66
|
+
required: false,
|
|
67
|
+
schema: {
|
|
68
|
+
type: 'array',
|
|
69
|
+
items: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
id: { type: 'string' },
|
|
73
|
+
column: { type: 'string' },
|
|
74
|
+
title: { type: 'string' },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
streamSpec: {
|
|
82
|
+
taskUpdates: {
|
|
83
|
+
description: 'Task CRUD feed',
|
|
84
|
+
schema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
action: { type: 'string', enum: ['create', 'move', 'edit', 'delete'] },
|
|
88
|
+
id: { type: 'string' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
contextSpec: {
|
|
94
|
+
selectedId: { schema: { type: 'string', nullable: true } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
package/src/providers/bedrock.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Concrete AWS Bedrock `ProviderAdapter` — invokes Anthropic Claude
|
|
3
3
|
* models on Bedrock via the official `@anthropic-ai/bedrock-sdk`
|
|
4
4
|
* package. IAM-based auth (no API key in flight); the AWS credential
|
|
5
|
-
* chain (
|
|
5
|
+
* chain (a workload-identity token / `~/.aws/credentials` / env vars) supplies
|
|
6
6
|
* SigV4 signatures automatically.
|
|
7
7
|
*
|
|
8
8
|
* ## Why this adapter exists
|
|
9
9
|
*
|
|
10
10
|
* Anyone running a one-shot LLM caller on an AWS-credentialed host
|
|
11
|
-
* (EC2, ECS, Lambda
|
|
11
|
+
* (EC2, ECS, Lambda and the like) can target Bedrock without managing API keys: IAM is
|
|
12
12
|
* the auth boundary, AWS rotates host credentials automatically, and
|
|
13
13
|
* a misconfigured IAM role surfaces as a clear `AccessDeniedException`
|
|
14
14
|
* the SDK funnels through `mapError`. The alternative — parking a
|
|
@@ -165,8 +165,8 @@ export function bedrockEndpointFor(model: string): BedrockEndpoint {
|
|
|
165
165
|
* Constructor options for the Bedrock adapter.
|
|
166
166
|
*
|
|
167
167
|
* `region` is the only required option in the common case — IAM
|
|
168
|
-
* credentials come from the standard AWS chain (
|
|
169
|
-
*
|
|
168
|
+
* credentials come from the standard AWS chain (a workload-identity token in
|
|
169
|
+
* a container cluster, instance role on EC2, env vars or shared credentials file
|
|
170
170
|
* locally). Tests pass `clientFactory` to inject a mock SDK client.
|
|
171
171
|
*/
|
|
172
172
|
export interface BedrockAdapterOptions {
|
package/src/run-check.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
newValidatorTraceId,
|
|
23
23
|
truncateSourceForTrace,
|
|
24
24
|
} from "./harness/validator-trace-sink.js";
|
|
25
|
+
import { runGatedAxisChecks, traceAxisChecksSkipped } from "./evaluation/axis-checks/dispatch.js";
|
|
25
26
|
|
|
26
27
|
export interface RunCheckInput {
|
|
27
28
|
readonly sourceCode: string;
|
|
@@ -66,6 +67,10 @@ export async function runCheck(input: RunCheckInput): Promise<CheckResult> {
|
|
|
66
67
|
const startedAt = Date.now();
|
|
67
68
|
|
|
68
69
|
if (compiledCode === null) {
|
|
70
|
+
traceAxisChecksSkipped(
|
|
71
|
+
{ sourceCode, contract, originalPrompt: prompt, classification: harness.classification, designMode: harness.designMode },
|
|
72
|
+
"compiledCode null — no check ran",
|
|
73
|
+
);
|
|
69
74
|
const result: CheckResult = {
|
|
70
75
|
issues: [],
|
|
71
76
|
axisIssueCount: 0,
|
|
@@ -113,17 +118,13 @@ export async function runCheck(input: RunCheckInput): Promise<CheckResult> {
|
|
|
113
118
|
contract: contract,
|
|
114
119
|
originalPrompt: prompt,
|
|
115
120
|
classification: harness.classification,
|
|
121
|
+
designMode: harness.designMode,
|
|
116
122
|
};
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
firedIds.push(axisCheck.id);
|
|
123
|
-
const axisIssues = axisCheck.run(axisInput);
|
|
124
|
-
issues.push(...axisIssues);
|
|
125
|
-
axisIssueCount += axisIssues.length;
|
|
126
|
-
}
|
|
123
|
+
// The one runner `runAxisChecks` shares (ggui#1046 trace prints here, on the served path).
|
|
124
|
+
const axisRun = runGatedAxisChecks(check.axisChecks, axisInput);
|
|
125
|
+
issues.push(...axisRun.issues);
|
|
126
|
+
firedIds.push(...axisRun.firedIds);
|
|
127
|
+
const axisIssueCount = axisRun.issues.length;
|
|
127
128
|
|
|
128
129
|
// ── Tier checks (deterministic, harness-owned) ────────────────────────
|
|
129
130
|
let tierIssueCount = 0;
|