@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The design tree a judge paints with is an INPUT to the judgement and a
|
|
2
|
+
// part of its identity (ggui#1042, D3): a judge-only re-judge renders every
|
|
3
|
+
// arm with the tree on disk, so the row must say WHICH tree — `srcSha256`
|
|
4
|
+
// over every RENDERING INPUT under the design package's `src`, sorted by
|
|
5
|
+
// relative path — and the local judge path must accept the tree explicitly.
|
|
6
|
+
// A served hello is compared to a judged one only when this hash and the
|
|
7
|
+
// runtime's design agree (`design@judge == design@runtime`, generation-triad.md).
|
|
8
|
+
//
|
|
9
|
+
// Rendering inputs only: tests (`*.test.*`, `__tests__/`), stories
|
|
10
|
+
// (`*.stories.*`) and docs (`*.md`) paint nothing, and a built image ships
|
|
11
|
+
// the design package without them — the first rows on candidate 34 stamped
|
|
12
|
+
// `e2dc89e4…` (123 files) while a checkout of the same git tree computed
|
|
13
|
+
// `2cd62cb7…` (199 files). One identity must read the same on every host
|
|
14
|
+
// that carries the same rendering inputs.
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
|
|
19
|
+
export interface JudgeDesignIdentity {
|
|
20
|
+
/** Absolute path of the design `src` tree the judge bundled against. */
|
|
21
|
+
readonly src: string;
|
|
22
|
+
/** sha256 over every rendering input under `src` (see {@link isDesignRenderingInput}) — sorted relative path, then contents. */
|
|
23
|
+
readonly srcSha256: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a file under the design `src` tree is something the judge PAINTS
|
|
28
|
+
* with. Tests, stories and docs are not — and a built image carries the
|
|
29
|
+
* package without them, so they must not move the identity.
|
|
30
|
+
*/
|
|
31
|
+
export function isDesignRenderingInput(relativePath: string): boolean {
|
|
32
|
+
const rel = relativePath.split('\\').join('/');
|
|
33
|
+
if (rel.startsWith('__tests__/') || rel.includes('/__tests__/')) return false;
|
|
34
|
+
if (/\.test\.[cm]?[jt]sx?$/.test(rel)) return false;
|
|
35
|
+
if (/\.stories\.[cm]?[jt]sx?$/.test(rel)) return false;
|
|
36
|
+
if (rel.endsWith('.md')) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** sha256 over every rendering input under `root` (sorted relative path + contents) — the identity of a design tree as it paints. */
|
|
41
|
+
export function designTreeSha256(root: string): string {
|
|
42
|
+
const files: string[] = [];
|
|
43
|
+
const walk = (dir: string): void => {
|
|
44
|
+
for (const name of readdirSync(dir).sort()) {
|
|
45
|
+
const full = resolve(dir, name);
|
|
46
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
47
|
+
else if (isDesignRenderingInput(full.slice(root.length + 1))) files.push(full);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(root);
|
|
51
|
+
const h = createHash('sha256');
|
|
52
|
+
for (const f of files) {
|
|
53
|
+
h.update(f.slice(root.length + 1) + '\n');
|
|
54
|
+
h.update(readFileSync(f));
|
|
55
|
+
h.update('\n');
|
|
56
|
+
}
|
|
57
|
+
return h.digest('hex');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const identityCache = new Map<string, JudgeDesignIdentity | null>();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The identity of the tree at `src`, computed once per process per path (a
|
|
64
|
+
* judge run paints hundreds of frames from one tree).
|
|
65
|
+
*
|
|
66
|
+
* `null` when the tree cannot be READ (ggui#1110): a deployed image ships the
|
|
67
|
+
* design package as built output with no `src/`, and an identity is a receipt
|
|
68
|
+
* about a judgement — never a precondition for making one. Before this,
|
|
69
|
+
* `scandir` threw ENOENT inside the evaluation round wherever the package ships
|
|
70
|
+
* built output only, the round returned no evaluation at all, and a bar refused
|
|
71
|
+
* a card that had cleared it. A row without the receipt says so; a round that cannot
|
|
72
|
+
* stamp one still runs.
|
|
73
|
+
*/
|
|
74
|
+
export function judgeDesignIdentity(src: string): JudgeDesignIdentity | null {
|
|
75
|
+
const key = resolve(src);
|
|
76
|
+
const cached = identityCache.get(key);
|
|
77
|
+
if (cached !== undefined) return cached;
|
|
78
|
+
let identity: JudgeDesignIdentity | null;
|
|
79
|
+
try {
|
|
80
|
+
identity = { src: key, srcSha256: designTreeSha256(key) };
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Unreadable tree: no source directory (a built package), no permission,
|
|
83
|
+
// a path that is not a directory. The reason is logged ONCE per path —
|
|
84
|
+
// the judgement proceeds unstamped rather than not at all.
|
|
85
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
86
|
+
console.warn(`[visual-eval] design tree unreadable at ${key} — the judgement carries no design identity (${reason})`);
|
|
87
|
+
identity = null;
|
|
88
|
+
}
|
|
89
|
+
identityCache.set(key, identity);
|
|
90
|
+
return identity;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test seam: forget cached identities (a test that rewrites a tree between runs). */
|
|
94
|
+
export function resetJudgeDesignIdentityCache(): void {
|
|
95
|
+
identityCache.clear();
|
|
96
|
+
}
|
package/src/evaluation/index.ts
CHANGED
|
@@ -22,6 +22,9 @@ export type {
|
|
|
22
22
|
EvalOutcome,
|
|
23
23
|
EvalCategory,
|
|
24
24
|
EvalResult,
|
|
25
|
+
VisualEvalSummary,
|
|
26
|
+
CanvasJudgeRecord,
|
|
27
|
+
CanvasVisualSummary,
|
|
25
28
|
EvalCriterion,
|
|
26
29
|
CriterionCoverage,
|
|
27
30
|
CriterionRunStatus,
|
|
@@ -39,6 +42,7 @@ export {
|
|
|
39
42
|
priorityForIssue,
|
|
40
43
|
isBlocked,
|
|
41
44
|
getActionableIssues,
|
|
45
|
+
criteriaFor,
|
|
42
46
|
getCriteriaByPriority,
|
|
43
47
|
getCriterionById,
|
|
44
48
|
getLLMCriteria,
|
|
@@ -84,5 +88,62 @@ export type { RunAxisChecksInput } from './axis-checks/index.js';
|
|
|
84
88
|
// rubric — scoring generation on criteria the generator was never taught
|
|
85
89
|
// is a constraint-alignment violation, so out-of-loop judges get this
|
|
86
90
|
// seam instead of a rubric extension.
|
|
87
|
-
export {
|
|
88
|
-
|
|
91
|
+
export {
|
|
92
|
+
callMultimodalLLM,
|
|
93
|
+
captureScreenshot,
|
|
94
|
+
resolveLaunchOptions,
|
|
95
|
+
runVisualEvaluation,
|
|
96
|
+
resolveDesignPackageDir,
|
|
97
|
+
runVisualEvaluationDetailed,
|
|
98
|
+
VISUAL_JUDGE_MAX_OUTPUT_TOKENS,
|
|
99
|
+
VISUAL_JUDGE_PROMPT_VERSION,
|
|
100
|
+
VISUAL_JUDGE_PROMPT_DIGEST,
|
|
101
|
+
VISUAL_EVAL_PROMPT,
|
|
102
|
+
captureScreenshotDetailed,
|
|
103
|
+
canvasFitPolicy,
|
|
104
|
+
canvasOverflowIssue,
|
|
105
|
+
CONTENT_HEIGHT_EXPRESSION,
|
|
106
|
+
runVisualEval,
|
|
107
|
+
summarizeVisualResult,
|
|
108
|
+
} from './visual-evaluator.js';
|
|
109
|
+
export type {
|
|
110
|
+
VisualEvalConfig,
|
|
111
|
+
VisualEvalContext,
|
|
112
|
+
VisualEvalDeps,
|
|
113
|
+
VisualEvalOutcome,
|
|
114
|
+
VisualEvaluationResult,
|
|
115
|
+
VisualEvalDetailed,
|
|
116
|
+
ScreenshotAttempt,
|
|
117
|
+
CanvasVisualResult,
|
|
118
|
+
CanvasFitPolicy,
|
|
119
|
+
CaptureMode,
|
|
120
|
+
ScreenshotDeps,
|
|
121
|
+
ScreenshotLauncher,
|
|
122
|
+
ScreenshotBrowser,
|
|
123
|
+
ScreenshotPage,
|
|
124
|
+
ChromiumProvider,
|
|
125
|
+
} from './visual-evaluator.js';
|
|
126
|
+
|
|
127
|
+
// Canvas vocabulary — the per-canvas visual evaluation speaks it
|
|
128
|
+
// (`VisualEvalConfig.canvases`, `CanvasVisualResult.canvas`); re-exported
|
|
129
|
+
// here so a caller driving `runVisualEvaluation` from this subpath never
|
|
130
|
+
// has to reach the package root for the class names, viewports or
|
|
131
|
+
// descriptors.
|
|
132
|
+
export {
|
|
133
|
+
CANVAS_CLASSES,
|
|
134
|
+
CANVAS_VIEWPORTS,
|
|
135
|
+
CANVAS_BREAKPOINTS,
|
|
136
|
+
CANVAS_DESCRIPTORS,
|
|
137
|
+
canvasForViewportWidth,
|
|
138
|
+
canvasForRendering,
|
|
139
|
+
canvasDisplayModeForShell,
|
|
140
|
+
describeCanvas,
|
|
141
|
+
} from '../design-mode.js';
|
|
142
|
+
export type {
|
|
143
|
+
CanvasClass,
|
|
144
|
+
CanvasViewport,
|
|
145
|
+
CanvasDisplayMode,
|
|
146
|
+
CanvasDescriptor,
|
|
147
|
+
} from '../design-mode.js';
|
|
148
|
+
export { cssTokensForAppTheme } from './theme-css.js';
|
|
149
|
+
export { designTreeSha256, judgeDesignIdentity, resetJudgeDesignIdentityCache, type JudgeDesignIdentity } from './design-identity.js';
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// Caching: Anthropic ephemeral cache, OpenAI auto-cache, Google explicit cache.
|
|
15
15
|
// Same system prompt across all 7 calls → first call caches, 6 remaining hit cache.
|
|
16
16
|
|
|
17
|
+
import { buildStylingProfileJudgeBlock } from '../boilerplate/styling-profile.js';
|
|
18
|
+
import type { GenerationProfileInput } from '../boilerplate/styling-profile.js';
|
|
17
19
|
import type {
|
|
18
20
|
EvalIssue,
|
|
19
21
|
EvalResult,
|
|
@@ -26,6 +28,7 @@ import { createAgent } from '../harness/llm-router';
|
|
|
26
28
|
import type { AgentConfig, LLMToolDef, LLMAgent } from '../harness/llm-router';
|
|
27
29
|
import type { JsonObject } from '@ggui-ai/protocol';
|
|
28
30
|
import { getCriterionById } from './types-public.js';
|
|
31
|
+
import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
|
|
29
32
|
|
|
30
33
|
// =============================================================================
|
|
31
34
|
// Helpers
|
|
@@ -66,6 +69,16 @@ export interface LLMEvalContext {
|
|
|
66
69
|
originalPrompt: string;
|
|
67
70
|
designContext?: string;
|
|
68
71
|
contract?: unknown;
|
|
72
|
+
/**
|
|
73
|
+
* Which triad produced the source. Selects the criteria text
|
|
74
|
+
* (`criteriaFor`) and the mother prompt's styling rules: in `free`
|
|
75
|
+
* mode the `tokens` criterion narrows to color and the `visual`
|
|
76
|
+
* criterion + "Design System Rules" block become arm-neutral. Absent
|
|
77
|
+
* means `constrained` (byte-identical to the pre-`designMode` prompt).
|
|
78
|
+
*/
|
|
79
|
+
designMode?: DesignMode;
|
|
80
|
+
/** The app's generation profile (#991) — the judge reads the same text the coder was given. */
|
|
81
|
+
profile?: GenerationProfileInput;
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
export interface LLMEvalConfig {
|
|
@@ -108,14 +121,66 @@ export interface EvalContext {
|
|
|
108
121
|
originalPrompt: string;
|
|
109
122
|
contract?: unknown;
|
|
110
123
|
designSystemSummary?: string;
|
|
124
|
+
/** See `LLMEvalContext.designMode`. */
|
|
125
|
+
designMode?: DesignMode;
|
|
126
|
+
/** See `LLMEvalContext.profile`. */
|
|
127
|
+
profile?: GenerationProfileInput;
|
|
111
128
|
}
|
|
112
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Arm-neutral styling rules for `designMode: 'free'` — replaces the
|
|
132
|
+
* constrained "Design System Rules" block. Mirrors the free prompt: color
|
|
133
|
+
* on the closed token manifest, everything else may be literal, raw HTML
|
|
134
|
+
* + CSS is a first-class choice.
|
|
135
|
+
*/
|
|
136
|
+
const FREE_STYLING_RULES = `## Styling Rules (important for evaluation)
|
|
137
|
+
- Color: every color must be a bare \`var(--ggui-color-*)\` token — a hardcoded #hex, rgb()/hsl() or CSS named color is a fail, and so is a literal fallback inside \`var(--ggui-*, …)\`.
|
|
138
|
+
- Accent text (eyebrow, tagline, link, label) painted with a bare \`var(--ggui-color-primary-300…600)\` is a readability fail — the accent ink is \`var(--ggui-color-link)\`; a tint as text on a primary fill and a deep stop (700–900) on a light tint are fine
|
|
139
|
+
- Spacing, typography, radius, shadow geometry and layout values MAY be literals (\`padding: '20px'\`, \`fontSize: 18\`, \`borderRadius: 12\`) — NEVER flag them.
|
|
140
|
+
- Raw HTML elements with inline styles or \`<style>\` blocks are a first-class choice — never flag "not using the design system" or reward using it.
|
|
141
|
+
- When \`@ggui-ai/design\` primitives ARE used, their enum/scale props (\`variant="primary"\`, \`size="lg"\`, \`gap="md"\`, \`tone="muted"\`, \`surface="accent"\`) are valid.
|
|
142
|
+
- One \`export default function Component\` — helper components as named functions above it are fine`;
|
|
143
|
+
|
|
144
|
+
const CONSTRAINED_STYLING_RULES = `## Design System Rules (important for evaluation)
|
|
145
|
+
- Spacing props (\`gap\`, \`padding\`, \`margin\`) take a t-shirt-scale name: \`gap="md"\`, \`padding="lg"\` (\`none|xs|sm|md|lg|xl|2xl\`). **These ARE design tokens** — each resolves to a \`--ggui-spacing-*\` variable. A raw \`var(--ggui-spacing-*)\` string is an accepted escape hatch. NEVER flag a scale name as "hardcoded". Only a numeric prop (\`padding={24}\`) or a raw CSS length (\`gap="13px"\`) bypasses the scale and should be warned.
|
|
146
|
+
- All colors must use CSS variables: \`color="var(--ggui-color-onContainer)"\` — hardcoded #hex or rgb() is a fail
|
|
147
|
+
- Accent text (eyebrow, tagline, link, label) painted with a bare \`primary-300…600\` stop is a readability fail — the accent ink is \`var(--ggui-color-link)\` (or \`tone="emphasized"\`); tints as text on a primary fill and deep stops on a light tint are fine
|
|
148
|
+
- Component props that take enum/scale values — \`variant="primary"\`, \`size="lg"\`, \`gap="md"\`, \`padding="lg"\`, \`shadow="md"\`, \`radius="lg"\`, \`tone="muted"\`, \`surface="accent"\` — are all design system tokens and are always valid
|
|
149
|
+
- One \`export default function Component\` — helper components as named functions above it are fine`;
|
|
150
|
+
|
|
151
|
+
const CONSTRAINED_VISUAL_REFERENCE = `**visual**: Design system consistency.
|
|
152
|
+
- Uses CSS variables (var(--ggui-*)) for colors, not hardcoded values
|
|
153
|
+
- Consistent use of spacing tokens
|
|
154
|
+
- Proper use of component variants (primary, outline, ghost)
|
|
155
|
+
- Shadow, border-radius from design system tokens`;
|
|
156
|
+
|
|
157
|
+
const FREE_VISUAL_REFERENCE = `**visual**: Visual composition.
|
|
158
|
+
- Clear hierarchy (one focal point, headings distinct from body), consistent spacing rhythm
|
|
159
|
+
- Coherent palette drawn from the theme color tokens
|
|
160
|
+
- Fit to the canvas: fluid width, nothing cramped or stretched
|
|
161
|
+
- No reward or penalty for using (or not using) the design-system primitives`;
|
|
162
|
+
|
|
163
|
+
const CONSTRAINED_RAW_CLICK_TARGET_GAP = `- A raw \`<div>\` / \`<span>\` used as a click target without \`as={Clickable}\`
|
|
164
|
+
(no role, no keyboard) — or any hand-rolled interactive element.`;
|
|
165
|
+
|
|
166
|
+
const FREE_RAW_CLICK_TARGET_GAP = `- A raw \`<div>\` / \`<span>\` used as a click target with no \`role="button"\`,
|
|
167
|
+
\`tabIndex\` and keyboard handler — a real \`<button>\` / \`<a href>\` is the
|
|
168
|
+
correct raw element and is never a gap.`;
|
|
169
|
+
|
|
113
170
|
export function buildMotherPrompt(ctx: EvalContext): string {
|
|
114
171
|
const parts: string[] = [];
|
|
115
|
-
|
|
116
|
-
|
|
172
|
+
const designMode = ctx.designMode ?? DEFAULT_DESIGN_MODE;
|
|
173
|
+
const free = designMode === 'free';
|
|
174
|
+
|
|
175
|
+
parts.push(
|
|
176
|
+
free
|
|
177
|
+
? `You are evaluating a generated React component. It was composed freely — raw HTML elements, inline styles and <style> blocks are allowed and the ggui design package is optional — against a data contract and a theme-token color rule.
|
|
178
|
+
The code has already passed automated checks (compilation, security, imports, color tokens).
|
|
179
|
+
Your job is to evaluate ONE specific quality criterion per call.`
|
|
180
|
+
: `You are evaluating a generated React component built with the ggui design system.
|
|
117
181
|
The code has already passed automated checks (compilation, security, imports, design tokens).
|
|
118
|
-
Your job is to evaluate ONE specific quality criterion per call
|
|
182
|
+
Your job is to evaluate ONE specific quality criterion per call.`,
|
|
183
|
+
);
|
|
119
184
|
|
|
120
185
|
parts.push(`## About This Prompt
|
|
121
186
|
This system prompt has 2 sections:
|
|
@@ -173,11 +238,7 @@ ${JSON.stringify(ctx.contract).slice(0, 6000)}
|
|
|
173
238
|
${ctx.designSystemSummary}`);
|
|
174
239
|
}
|
|
175
240
|
|
|
176
|
-
parts.push(
|
|
177
|
-
- Spacing props (\`gap\`, \`padding\`, \`margin\`) take a t-shirt-scale name: \`gap="md"\`, \`padding="lg"\` (\`none|xs|sm|md|lg|xl|2xl\`). **These ARE design tokens** — each resolves to a \`--ggui-spacing-*\` variable. A raw \`var(--ggui-spacing-*)\` string is an accepted escape hatch. NEVER flag a scale name as "hardcoded". Only a numeric prop (\`padding={24}\`) or a raw CSS length (\`gap="13px"\`) bypasses the scale and should be warned.
|
|
178
|
-
- All colors must use CSS variables: \`color="var(--ggui-color-primary-600)"\` — hardcoded #hex or rgb() is a fail
|
|
179
|
-
- Component props that take enum/scale values — \`variant="primary"\`, \`size="lg"\`, \`gap="md"\`, \`padding="lg"\`, \`shadow="md"\`, \`radius="lg"\`, \`tone="muted"\`, \`surface="accent"\` — are all design system tokens and are always valid
|
|
180
|
-
- One \`export default function Component\` — helper components as named functions above it are fine
|
|
241
|
+
parts.push(`${free ? FREE_STYLING_RULES : CONSTRAINED_STYLING_RULES}
|
|
181
242
|
|
|
182
243
|
## Evaluation Criteria Reference
|
|
183
244
|
|
|
@@ -221,11 +282,7 @@ ${ctx.designSystemSummary}`);
|
|
|
221
282
|
- Error boundaries or error messages for failure cases
|
|
222
283
|
- Empty states ("No items found") when data arrays could be empty
|
|
223
284
|
|
|
224
|
-
|
|
225
|
-
- Uses CSS variables (var(--ggui-*)) for colors, not hardcoded values
|
|
226
|
-
- Consistent use of spacing tokens
|
|
227
|
-
- Proper use of component variants (primary, outline, ghost)
|
|
228
|
-
- Shadow, border-radius from design system tokens
|
|
285
|
+
${free ? FREE_VISUAL_REFERENCE : CONSTRAINED_VISUAL_REFERENCE}
|
|
229
286
|
|
|
230
287
|
## Primitive Accessibility (built-in — do NOT flag as missing)
|
|
231
288
|
|
|
@@ -259,8 +316,7 @@ role, aria-*, label, or keyboard support — it is already there:
|
|
|
259
316
|
to text is CORRECTLY silent — never ask for \`aria-hidden\`, it is there.
|
|
260
317
|
|
|
261
318
|
REAL accessibility gaps worth flagging:
|
|
262
|
-
|
|
263
|
-
(no role, no keyboard) — or any hand-rolled interactive element.
|
|
319
|
+
${free ? FREE_RAW_CLICK_TARGET_GAP : CONSTRAINED_RAW_CLICK_TARGET_GAP}
|
|
264
320
|
- A hand-rolled control whose state (checked / selected / expanded /
|
|
265
321
|
pressed) is conveyed ONLY by styling — e.g. a styled-div "checkbox"
|
|
266
322
|
with a check icon + strikethrough label but no \`role="checkbox"\` /
|
|
@@ -275,6 +331,8 @@ REAL accessibility gaps worth flagging:
|
|
|
275
331
|
- Inverted or skipped heading levels (an \`<Heading level={3}>\` rendered
|
|
276
332
|
before the page's \`<Heading level={1}>\`).`);
|
|
277
333
|
|
|
334
|
+
const profileBlock = buildStylingProfileJudgeBlock(ctx.profile);
|
|
335
|
+
if (profileBlock.length > 0) parts.push(profileBlock);
|
|
278
336
|
return parts.join('\n\n');
|
|
279
337
|
}
|
|
280
338
|
|
|
@@ -329,11 +387,15 @@ function makeTier2Tool(criterion: string): LLMToolDef {
|
|
|
329
387
|
// Per-criterion user prompts
|
|
330
388
|
// =============================================================================
|
|
331
389
|
|
|
332
|
-
function getCriterionPrompt(
|
|
390
|
+
function getCriterionPrompt(
|
|
391
|
+
criterion: string,
|
|
392
|
+
numberedSource: string,
|
|
393
|
+
designMode: DesignMode,
|
|
394
|
+
): string {
|
|
333
395
|
const sourceBlock = `\nSource code (with line numbers):\n\`\`\`tsx\n${numberedSource}\n\`\`\``;
|
|
334
396
|
|
|
335
397
|
// Use criteria.ts as single source of truth when available
|
|
336
|
-
const criterionDef = getCriterionById(criterion);
|
|
398
|
+
const criterionDef = getCriterionById(criterion, designMode);
|
|
337
399
|
if (criterionDef && criterionDef.tier > 0) {
|
|
338
400
|
return `${criterionDef.evalInstruction}
|
|
339
401
|
${sourceBlock}
|
|
@@ -489,7 +551,7 @@ Return your evaluation using the evaluate_visual tool.
|
|
|
489
551
|
|
|
490
552
|
Example outputs:
|
|
491
553
|
- Clean: {"result": "pass"}
|
|
492
|
-
- Minor issues: {"result": "warn", "issues": ["Line 45: hardcoded '#f3f4f6' should use var(--ggui-color-
|
|
554
|
+
- Minor issues: {"result": "warn", "issues": ["Line 45: hardcoded '#f3f4f6' should use var(--ggui-color-sunken)", "Line 80: numeric padding={16} should use a scale name like padding='md'"]}
|
|
493
555
|
- Severe: {"result": "fail", "issues": ["Component uses entirely custom styling with no design system tokens"]}
|
|
494
556
|
|
|
495
557
|
Use 'fail' only for pervasive design system violations. Use 'warn' for isolated cases.`;
|
|
@@ -553,10 +615,11 @@ async function evalCriterion(
|
|
|
553
615
|
numberedSource: string,
|
|
554
616
|
agent: LLMAgent,
|
|
555
617
|
model: string,
|
|
618
|
+
designMode: DesignMode,
|
|
556
619
|
): Promise<CriterionResult> {
|
|
557
620
|
const start = Date.now();
|
|
558
621
|
const tool = tier === 1 ? makeTier1Tool(criterion) : makeTier2Tool(criterion);
|
|
559
|
-
const userPrompt = getCriterionPrompt(criterion, numberedSource);
|
|
622
|
+
const userPrompt = getCriterionPrompt(criterion, numberedSource, designMode);
|
|
560
623
|
|
|
561
624
|
try {
|
|
562
625
|
const response = await agent.callTools(
|
|
@@ -679,6 +742,8 @@ export async function preWarmEval(
|
|
|
679
742
|
originalPrompt: context.originalPrompt,
|
|
680
743
|
contract: context.contract,
|
|
681
744
|
designSystemSummary: context.designContext,
|
|
745
|
+
designMode: context.designMode,
|
|
746
|
+
profile: context.profile,
|
|
682
747
|
});
|
|
683
748
|
|
|
684
749
|
return { motherPrompt, dynamicCriteria: [] };
|
|
@@ -777,10 +842,13 @@ export async function runLLMEvaluation(
|
|
|
777
842
|
: (providerName as 'openai' | 'google');
|
|
778
843
|
|
|
779
844
|
// Use pre-warmed context if available, otherwise build fresh
|
|
845
|
+
const designMode = context.designMode ?? DEFAULT_DESIGN_MODE;
|
|
780
846
|
const motherPrompt = preWarmedContext?.motherPrompt ?? buildMotherPrompt({
|
|
781
847
|
originalPrompt: context.originalPrompt,
|
|
782
848
|
contract: context.contract,
|
|
783
849
|
designSystemSummary: context.designContext,
|
|
850
|
+
designMode,
|
|
851
|
+
profile: context.profile,
|
|
784
852
|
});
|
|
785
853
|
|
|
786
854
|
// Add line numbers to source
|
|
@@ -810,8 +878,8 @@ export async function runLLMEvaluation(
|
|
|
810
878
|
|
|
811
879
|
// Run ALL criteria in parallel: 7 static (2 tier 1 + 5 tier 2)
|
|
812
880
|
const results = await Promise.all([
|
|
813
|
-
...TIER_1_CRITERIA.map(c => evalCriterion(c, 1, motherPrompt, numberedSource, agent, model)),
|
|
814
|
-
...TIER_2_CRITERIA.map(c => evalCriterion(c, 2, motherPrompt, numberedSource, agent, model)),
|
|
881
|
+
...TIER_1_CRITERIA.map(c => evalCriterion(c, 1, motherPrompt, numberedSource, agent, model, designMode)),
|
|
882
|
+
...TIER_2_CRITERIA.map(c => evalCriterion(c, 2, motherPrompt, numberedSource, agent, model, designMode)),
|
|
815
883
|
]);
|
|
816
884
|
|
|
817
885
|
// Merge results
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AppTheme } from '@ggui-ai/protocol';
|
|
2
|
+
import { composeThemeCss, getCssTokens } from '@ggui-ai/design/rendering';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The stylesheet a visual judge renders under for an app: the app's theme
|
|
6
|
+
* composed the way the runtime composes it (`composeThemeCss`, layer `page`
|
|
7
|
+
* — one composition, never a second), so a judged canvas paints the tokens a
|
|
8
|
+
* visitor's page paints. Absent theme ⇒ the design's default tokens,
|
|
9
|
+
* byte-for-byte what the judge used before.
|
|
10
|
+
*/
|
|
11
|
+
export function cssTokensForAppTheme(theme: AppTheme | undefined, mode: 'light' | 'dark' = 'light'): string {
|
|
12
|
+
if (theme === undefined) return getCssTokens(mode);
|
|
13
|
+
return composeThemeCss({ layer: 'page', mode: theme.mode ?? mode, appTheme: theme });
|
|
14
|
+
}
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
export type Priority = "P0" | "P1" | "P2";
|
|
22
22
|
|
|
23
|
+
import type { CanvasClass, DesignMode } from "../design-mode.js";
|
|
24
|
+
|
|
23
25
|
// ─── Issue shape ───────────────────────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
export type EvalTier = 0 | 1 | 2;
|
|
@@ -174,6 +176,68 @@ export interface EvalResult {
|
|
|
174
176
|
* probe runner; absent on eval paths that never invoke the probe.
|
|
175
177
|
*/
|
|
176
178
|
runtimeProbe?: RuntimeProbeMeta;
|
|
179
|
+
/**
|
|
180
|
+
* Per-canvas visual verdicts. Stamped by the harness ONLY when the
|
|
181
|
+
* visual leg ran with `visualEvaluation.canvases` set (arm-neutral —
|
|
182
|
+
* the same judge in every design mode); absent on the single-shot
|
|
183
|
+
* visual path and whenever the visual leg did not run. PNGs stay on
|
|
184
|
+
* `runVisualEvaluation`'s own result (`VisualEvaluationResult.canvases[*].screenshotPng`),
|
|
185
|
+
* never here — this object is serialized into reports.
|
|
186
|
+
*/
|
|
187
|
+
visual?: VisualEvalSummary;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** One canvas's visual verdict, PNG-free — see `EvalResult.visual`. */
|
|
191
|
+
/**
|
|
192
|
+
* How one canvas's `score` was reached (ggui#1072): `k` vision calls on the
|
|
193
|
+
* SAME captured frame, aggregated by `rule`. Always present — `k: 1` ⇒
|
|
194
|
+
* `samples: [score]`, `sigma: 0`, `notes: [critique]` — so one shape serves
|
|
195
|
+
* a single judgement and a median of many. `samples`/`notes` carry only the
|
|
196
|
+
* calls the judge could parse (in call order); `k` stays the requested count,
|
|
197
|
+
* so a short `samples` is visible on the row rather than filled in.
|
|
198
|
+
*/
|
|
199
|
+
export interface CanvasJudgeRecord {
|
|
200
|
+
readonly k: number;
|
|
201
|
+
readonly rule: 'median';
|
|
202
|
+
readonly samples: number[];
|
|
203
|
+
/** Population σ of `samples` (0 when one sample). */
|
|
204
|
+
readonly sigma: number;
|
|
205
|
+
/** The judge's per-canvas critique per sample — the words beside a swing. */
|
|
206
|
+
readonly notes: string[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface CanvasVisualSummary {
|
|
210
|
+
canvas: CanvasClass;
|
|
211
|
+
viewport: { width: number; height: number };
|
|
212
|
+
/** The judge's weighted score at this canvas (0-100). */
|
|
213
|
+
score: number;
|
|
214
|
+
/** `score >= passThreshold`, and on the inline card: no overflow (ggui#1027). */
|
|
215
|
+
passed: boolean;
|
|
216
|
+
/** The document's scroll height at this canvas, CSS px; `null` when unmeasurable (ggui#1027). */
|
|
217
|
+
contentHeight: number | null;
|
|
218
|
+
/** `contentHeight > viewport.height` — measured on every canvas, judged per `canvasFitPolicy`. */
|
|
219
|
+
overflow: boolean;
|
|
220
|
+
/** How `score` was reached — always present (ggui#1072). */
|
|
221
|
+
judge: CanvasJudgeRecord;
|
|
222
|
+
/**
|
|
223
|
+
* How the judge composed the mount (ggui#1100): `'fill'` on every
|
|
224
|
+
* fullscreen canvas — the served runtime's fit, the root stretched to
|
|
225
|
+
* the frame; absent on the inline card, which keeps its natural height.
|
|
226
|
+
*/
|
|
227
|
+
fit?: 'fill';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The visual leg's per-canvas summary — see `EvalResult.visual`. */
|
|
231
|
+
export interface VisualEvalSummary {
|
|
232
|
+
/** Mean of the canvas scores (rounded). */
|
|
233
|
+
score: number;
|
|
234
|
+
/** Every canvas passed. */
|
|
235
|
+
passed: boolean;
|
|
236
|
+
canvases: CanvasVisualSummary[];
|
|
237
|
+
/** The design tree the judge painted with (ggui#1042): `src` + `srcSha256` — `design@judge`. */
|
|
238
|
+
design?: { readonly src: string; readonly srcSha256: string };
|
|
239
|
+
/** The mode the judge's tokens were composed in, when the caller said (ggui#1076). */
|
|
240
|
+
themeMode?: 'light' | 'dark';
|
|
177
241
|
}
|
|
178
242
|
|
|
179
243
|
// ─── Quality mode ──────────────────────────────────────────────────────────
|
|
@@ -209,6 +273,13 @@ export interface AxisCheckInput {
|
|
|
209
273
|
originalPrompt: string;
|
|
210
274
|
/** Full classification — checks may read sibling axes. */
|
|
211
275
|
classification: Classification;
|
|
276
|
+
/**
|
|
277
|
+
* Which triad produced the source. Checks that assert a design-package
|
|
278
|
+
* component (e.g. `layout.multi_step.stepper_adopted`) read it and
|
|
279
|
+
* stand down in `free` mode, where the primitive is optional. Absent
|
|
280
|
+
* means `constrained`.
|
|
281
|
+
*/
|
|
282
|
+
designMode?: DesignMode;
|
|
212
283
|
}
|
|
213
284
|
|
|
214
285
|
/**
|
|
@@ -506,26 +577,84 @@ Only list ACTUAL violations. Use 'fail' only for pervasive violations.`,
|
|
|
506
577
|
},
|
|
507
578
|
] as const;
|
|
508
579
|
|
|
580
|
+
/**
|
|
581
|
+
* `designMode: 'free'` overrides — ONLY the two criteria whose text
|
|
582
|
+
* encodes the design vocabulary. `tokens` narrows to COLOR (the free arm
|
|
583
|
+
* relaxes spacing / typography / radius / shadow to literals but keeps
|
|
584
|
+
* brand-bearing color on the closed `--ggui-color-*` manifest, no
|
|
585
|
+
* fallbacks); `visual` becomes arm-neutral (composition, hierarchy,
|
|
586
|
+
* consistency, fit to canvas — no reward or penalty for design-system
|
|
587
|
+
* usage). Every other criterion is shared verbatim between the modes.
|
|
588
|
+
* Constraint alignment: what the free prompt says, the free evaluator
|
|
589
|
+
* checks — and nothing more.
|
|
590
|
+
*/
|
|
591
|
+
const FREE_DESIGN_CRITERIA_OVERRIDES: readonly EvalCriterion[] = [
|
|
592
|
+
{
|
|
593
|
+
id: "tokens",
|
|
594
|
+
name: "Color tokens",
|
|
595
|
+
priority: "P1",
|
|
596
|
+
tier: 0,
|
|
597
|
+
failOutcome: "warn",
|
|
598
|
+
codingGuidance:
|
|
599
|
+
"Every color is a bare var(--ggui-color-*) from the token manifest — no hex, rgb()/hsl(), CSS named colors or literal fallbacks, in style props and <style> blocks alike. Spacing, typography, radius and shadow geometry may be literals.",
|
|
600
|
+
evalInstruction:
|
|
601
|
+
"Flag hardcoded hex colors, rgba/hsl functions, CSS named colors, and literal fallbacks inside var(--ggui-*) references. Do NOT flag literal spacing, font sizes, radii or pixel values — they are allowed in this mode.",
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
id: "visual",
|
|
605
|
+
name: "Visual composition",
|
|
606
|
+
priority: "P2",
|
|
607
|
+
tier: 2,
|
|
608
|
+
failOutcome: "warn",
|
|
609
|
+
codingGuidance:
|
|
610
|
+
"Compose deliberately: one clear focal point, consistent spacing rhythm, a coherent theme-token palette, a fit to the rendering canvas.",
|
|
611
|
+
evalInstruction: `Evaluate VISUAL COMPOSITION: Is the component composed deliberately for its canvas?
|
|
612
|
+
|
|
613
|
+
Judge: visual hierarchy (one clear focal point, headings distinct from body text), consistent spacing rhythm and alignment, a coherent palette drawn from the theme tokens, fit to the canvas (fluid width, no fixed widths, nothing cramped or stretched).
|
|
614
|
+
Do NOT reward or penalise the use of design-system primitives — raw HTML with inline CSS is a first-class choice here. Literal spacing / typography / radius values are fine.
|
|
615
|
+
|
|
616
|
+
Only list ACTUAL composition problems. Use 'fail' only for a fundamentally incoherent composition.`,
|
|
617
|
+
},
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* The criteria registry for a design mode. `constrained` IS `CRITERIA`
|
|
622
|
+
* (same array, byte-identical summary); `free` swaps in the two
|
|
623
|
+
* overrides above by id, preserving order.
|
|
624
|
+
*/
|
|
625
|
+
export function criteriaFor(designMode: DesignMode = "constrained"): readonly EvalCriterion[] {
|
|
626
|
+
if (designMode === "constrained") return CRITERIA;
|
|
627
|
+
return CRITERIA.map(
|
|
628
|
+
(c) => FREE_DESIGN_CRITERIA_OVERRIDES.find((o) => o.id === c.id) ?? c,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
509
632
|
/** Get all criteria for a specific priority level */
|
|
510
|
-
export function getCriteriaByPriority(
|
|
511
|
-
|
|
633
|
+
export function getCriteriaByPriority(
|
|
634
|
+
priority: Priority,
|
|
635
|
+
designMode: DesignMode = "constrained",
|
|
636
|
+
): EvalCriterion[] {
|
|
637
|
+
return criteriaFor(designMode).filter((c) => c.priority === priority);
|
|
512
638
|
}
|
|
513
639
|
|
|
514
640
|
/** Get a specific criterion by ID */
|
|
515
|
-
export function getCriterionById(
|
|
516
|
-
|
|
641
|
+
export function getCriterionById(
|
|
642
|
+
id: string,
|
|
643
|
+
designMode: DesignMode = "constrained",
|
|
644
|
+
): EvalCriterion | undefined {
|
|
645
|
+
return criteriaFor(designMode).find((c) => c.id === id);
|
|
517
646
|
}
|
|
518
647
|
|
|
519
648
|
/** Get all LLM-evaluated criteria (tier 1 + 2) */
|
|
520
|
-
export function getLLMCriteria(): EvalCriterion[] {
|
|
521
|
-
return
|
|
649
|
+
export function getLLMCriteria(designMode: DesignMode = "constrained"): EvalCriterion[] {
|
|
650
|
+
return criteriaFor(designMode).filter((c) => c.tier > 0);
|
|
522
651
|
}
|
|
523
652
|
|
|
524
653
|
/**
|
|
525
654
|
* Build the coding agent's criteria summary from the single source of truth.
|
|
526
655
|
* Grouped by priority for the P0→P1→P2 hierarchy.
|
|
527
656
|
*/
|
|
528
|
-
export function buildCodingCriteriaSummary(): string {
|
|
657
|
+
export function buildCodingCriteriaSummary(designMode: DesignMode = "constrained"): string {
|
|
529
658
|
const lines: string[] = ["## Priority (P0 first, then P1, then P2)", ""];
|
|
530
659
|
|
|
531
660
|
for (const priority of ["P0", "P1", "P2"] as Priority[]) {
|
|
@@ -535,7 +664,7 @@ export function buildCodingCriteriaSummary(): string {
|
|
|
535
664
|
: priority === "P1"
|
|
536
665
|
? "Should (safety)"
|
|
537
666
|
: "Nice (quality)";
|
|
538
|
-
const criteria = getCriteriaByPriority(priority);
|
|
667
|
+
const criteria = getCriteriaByPriority(priority, designMode);
|
|
539
668
|
lines.push(`**${priority} — ${label}:**`);
|
|
540
669
|
for (const c of criteria) {
|
|
541
670
|
lines.push(`- ${c.codingGuidance}`);
|