@compilr-dev/sdk 0.18.10 → 0.19.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/canvas/assets.d.ts +99 -0
- package/dist/canvas/assets.js +111 -0
- package/dist/canvas/color.d.ts +85 -0
- package/dist/canvas/color.js +216 -0
- package/dist/canvas/index.d.ts +8 -0
- package/dist/canvas/index.js +16 -0
- package/dist/canvas/quality-checks.d.ts +75 -0
- package/dist/canvas/quality-checks.js +217 -0
- package/dist/canvas/ramp.d.ts +45 -0
- package/dist/canvas/ramp.js +98 -0
- package/dist/capabilities/packs.js +16 -2
- package/dist/platform/context.d.ts +16 -2
- package/dist/platform/context.js +13 -1
- package/dist/platform/repositories.d.ts +4 -0
- package/dist/platform/sqlite/canvas-repository.d.ts +9 -0
- package/dist/platform/sqlite/canvas-repository.js +52 -0
- package/dist/platform/sqlite/db.js +29 -0
- package/dist/platform/sqlite/schema.d.ts +1 -1
- package/dist/platform/sqlite/schema.js +1 -1
- package/dist/platform/tools/canvas-tools.d.ts +12 -2
- package/dist/platform/tools/canvas-tools.js +316 -6
- package/dist/platform/tools/image-tools.d.ts +9 -2
- package/dist/platform/tools/image-tools.js +4 -2
- package/dist/platform/tools/index.js +10 -2
- package/dist/platform/tools/project-tools.js +3 -2
- package/dist/skills/canvas-exemplars.d.ts +8 -0
- package/dist/skills/canvas-exemplars.js +36 -0
- package/dist/skills/canvas-icons.d.ts +6 -0
- package/dist/skills/canvas-icons.js +84 -0
- package/dist/skills/canvas-skills.js +207 -20
- package/dist/skills/platform-skills.d.ts +2 -0
- package/dist/skills/platform-skills.js +8 -0
- package/dist/team/tool-config.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { parseColor, contrastRatio, isNeutral, hueAngle, AA_TEXT } from './color.js';
|
|
2
|
+
/** Every `style="…"` attribute body and every `{…}` CSS rule body, as separate scopes. */
|
|
3
|
+
function declarationScopes(html) {
|
|
4
|
+
const scopes = [];
|
|
5
|
+
for (const m of html.matchAll(/style\s*=\s*"([^"]*)"/gi))
|
|
6
|
+
scopes.push(m[1]);
|
|
7
|
+
for (const m of html.matchAll(/style\s*=\s*'([^']*)'/gi))
|
|
8
|
+
scopes.push(m[1]);
|
|
9
|
+
for (const m of html.matchAll(/\{([^{}]*)\}/g))
|
|
10
|
+
scopes.push(m[1]);
|
|
11
|
+
return scopes;
|
|
12
|
+
}
|
|
13
|
+
const declaration = (scope, property) => {
|
|
14
|
+
const m = property.exec(scope);
|
|
15
|
+
return m ? m[1].trim() : null;
|
|
16
|
+
};
|
|
17
|
+
const TEXT_COLOR = /(?:^|[;{\s])color\s*:\s*([^;}]+)/i;
|
|
18
|
+
const BACKGROUND = /(?:^|[;{\s])background(?:-color)?\s*:\s*([^;}]+)/i;
|
|
19
|
+
/**
|
|
20
|
+
* The custom properties a value actually USES — primaries only, never fallbacks.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ THIS EXISTS BECAUSE A SUBSTRING TEST GETS IT BACKWARDS. The handoff tells authors to
|
|
23
|
+
* write `var(--canvas-surface-1, var(--canvas-card))` so a canvas degrades on a host
|
|
24
|
+
* without the ramp, and to KEEP that fallback afterwards. A plain
|
|
25
|
+
* `/var\(--canvas-card/` test matches the fallback and fires the surface error on the
|
|
26
|
+
* exact corrected form the error message asks for — a validator that punishes compliance
|
|
27
|
+
* teaches an agent to ignore it. Caught by the negative test, not by reading the regex.
|
|
28
|
+
*
|
|
29
|
+
* So: walk to each `var(`, take the name up to the first comma, then skip the whole
|
|
30
|
+
* fallback by tracking paren depth.
|
|
31
|
+
*/
|
|
32
|
+
export function primaryVarNames(value) {
|
|
33
|
+
const names = [];
|
|
34
|
+
for (let i = value.indexOf('var('); i !== -1; i = value.indexOf('var(', i)) {
|
|
35
|
+
let j = i + 4;
|
|
36
|
+
while (j < value.length && /\s/.test(value[j]))
|
|
37
|
+
j++;
|
|
38
|
+
let name = '';
|
|
39
|
+
while (j < value.length && /[-\w]/.test(value[j]))
|
|
40
|
+
name += value[j++];
|
|
41
|
+
if (name.startsWith('--'))
|
|
42
|
+
names.push(name);
|
|
43
|
+
// Skip to the matching close paren — the fallback is deliberately not scanned.
|
|
44
|
+
let depth = 1;
|
|
45
|
+
while (j < value.length && depth > 0) {
|
|
46
|
+
if (value[j] === '(')
|
|
47
|
+
depth++;
|
|
48
|
+
else if (value[j] === ')')
|
|
49
|
+
depth--;
|
|
50
|
+
j++;
|
|
51
|
+
}
|
|
52
|
+
i = j;
|
|
53
|
+
}
|
|
54
|
+
return names;
|
|
55
|
+
}
|
|
56
|
+
const usesVar = (value, name) => primaryVarNames(value).some((n) => n.toLowerCase() === name);
|
|
57
|
+
/**
|
|
58
|
+
* A card that shares its ground's value is a rectangle of text.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ This flags the TOKEN PAIRING, not a measured delta, and that is the whole point:
|
|
61
|
+
* whether `--canvas-card` is 0.0% or 2.1% from `--canvas-bg` depends on the user's
|
|
62
|
+
* theme, and it is under 4% on every palette anyone measured. The fix is the same in
|
|
63
|
+
* all of them — use the ramp — so the check needs no palette to be right.
|
|
64
|
+
*/
|
|
65
|
+
export function checkSurfaceTokens(html) {
|
|
66
|
+
const usesCardAsSurface = declarationScopes(html).some((scope) => {
|
|
67
|
+
const bg = declaration(scope, BACKGROUND);
|
|
68
|
+
return bg !== null && usesVar(bg, '--canvas-card');
|
|
69
|
+
});
|
|
70
|
+
if (!usesCardAsSurface)
|
|
71
|
+
return [];
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
level: 'error',
|
|
75
|
+
message: 'A card is filled with var(--canvas-card) over a var(--canvas-bg) ground. On a ' +
|
|
76
|
+
'theme-derived palette those two sit within a few percent of each other, so the ' +
|
|
77
|
+
'card disappears and only its border holds the layout together. Use the elevation ' +
|
|
78
|
+
'ramp instead: background: var(--canvas-surface-1, var(--canvas-card)) for a raised ' +
|
|
79
|
+
'card, var(--canvas-surface-2, var(--canvas-card)) for an inset area (tables, code), ' +
|
|
80
|
+
'and border-color: var(--canvas-hairline, var(--canvas-border)). Keep the fallbacks ' +
|
|
81
|
+
'so the canvas still renders on a host that has not shipped the ramp.',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
/** Near-white, for the "white on the accent" trap. */
|
|
86
|
+
function isNearWhite(value) {
|
|
87
|
+
if (/^\s*white\s*$/i.test(value))
|
|
88
|
+
return true;
|
|
89
|
+
const c = parseColor(value);
|
|
90
|
+
return c !== null && c.r > 0.85 && c.g > 0.85 && c.b > 0.85;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Text that cannot be read on the surface it actually sits on.
|
|
94
|
+
*
|
|
95
|
+
* Two halves, both palette-free:
|
|
96
|
+
* - a literal colour on a literal background, measured exactly;
|
|
97
|
+
* - white on `var(--canvas-accent)`, which is a pairing rather than a measurement —
|
|
98
|
+
* the accent is a mid-tone in every theme we ship (#FF5722 gives 3.2:1), so the pair
|
|
99
|
+
* is wrong whatever it resolves to.
|
|
100
|
+
*/
|
|
101
|
+
export function checkTextContrast(html) {
|
|
102
|
+
const issues = [];
|
|
103
|
+
let whiteOnAccent = false;
|
|
104
|
+
const failures = [];
|
|
105
|
+
for (const scope of declarationScopes(html)) {
|
|
106
|
+
const fg = declaration(scope, TEXT_COLOR);
|
|
107
|
+
const bg = declaration(scope, BACKGROUND);
|
|
108
|
+
if (fg === null || bg === null)
|
|
109
|
+
continue;
|
|
110
|
+
if (usesVar(bg, '--canvas-accent') && isNearWhite(fg)) {
|
|
111
|
+
whiteOnAccent = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const fgc = parseColor(fg);
|
|
115
|
+
const bgc = parseColor(bg);
|
|
116
|
+
// ⚠️ A null here means a token or a keyword, NOT a failure — skip rather than guess.
|
|
117
|
+
if (!fgc || !bgc)
|
|
118
|
+
continue;
|
|
119
|
+
const ratio = contrastRatio(fgc, bgc);
|
|
120
|
+
if (ratio < AA_TEXT) {
|
|
121
|
+
failures.push(`${fg.trim()} on ${bg.trim()} — ${ratio.toFixed(1)}:1`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (whiteOnAccent) {
|
|
125
|
+
issues.push({
|
|
126
|
+
level: 'error',
|
|
127
|
+
message: 'White text on var(--canvas-accent). The accent is a mid-tone in every theme — ' +
|
|
128
|
+
'on the default it is 3.2:1, below the 4.5:1 floor. Either put a dark ink label on ' +
|
|
129
|
+
'the accent fill, or drop the fill and use the accent as the text colour on the ' +
|
|
130
|
+
'page ground.',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (failures.length) {
|
|
134
|
+
issues.push({
|
|
135
|
+
level: 'error',
|
|
136
|
+
message: `Text below the 4.5:1 floor against the surface it sits on: ${failures.slice(0, 4).join('; ')}` +
|
|
137
|
+
(failures.length > 4 ? ` (+${String(failures.length - 4)} more)` : '') +
|
|
138
|
+
'. Check a muted grey against the INSET surface, not the page — one that passes on ' +
|
|
139
|
+
'white will fail on surface-2.',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return issues;
|
|
143
|
+
}
|
|
144
|
+
/** Hues within this many degrees are the same hue wearing two tints. */
|
|
145
|
+
const HUE_BUCKET = 30;
|
|
146
|
+
/**
|
|
147
|
+
* More hues than reasons.
|
|
148
|
+
*
|
|
149
|
+
* ⚠️ Greys never count. They are the ground a composition sits on, and a palette of six
|
|
150
|
+
* greys is a perfectly good canvas — what makes one look machine-made is four competing
|
|
151
|
+
* *hues* with nothing to distinguish. Warn, never error: multiple hues are legitimate
|
|
152
|
+
* when they encode categories a reader must tell apart.
|
|
153
|
+
*/
|
|
154
|
+
export function checkHueCount(html) {
|
|
155
|
+
const buckets = new Set();
|
|
156
|
+
for (const m of html.matchAll(/#[0-9a-f]{3,8}\b|\b(?:rgb|hsl)a?\s*\([^)]*\)/gi)) {
|
|
157
|
+
const c = parseColor(m[0]);
|
|
158
|
+
if (!c || isNeutral(c))
|
|
159
|
+
continue;
|
|
160
|
+
buckets.add(Math.round(hueAngle(c) / HUE_BUCKET));
|
|
161
|
+
}
|
|
162
|
+
if (buckets.size <= 2)
|
|
163
|
+
return [];
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
level: 'warn',
|
|
167
|
+
message: `${String(buckets.size)} distinct hues are hardcoded in this canvas. Count the hues, then ` +
|
|
168
|
+
'count the reasons — if there are more hues than things a reader must tell apart, ' +
|
|
169
|
+
'colour is decoration. One accent marking the finding, and greys for everything else.',
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A Tweak the user can drag that changes nothing.
|
|
175
|
+
*
|
|
176
|
+
* ⚠️ SKIPPED ENTIRELY WHEN THE CANVAS DEFINES `applyParams`. That is the host bridge's
|
|
177
|
+
* fourth binding path — a canvas can take the whole params object in JS and do what it
|
|
178
|
+
* likes with it, in which case no static reference to the param name need exist. Without
|
|
179
|
+
* this guard the check would report confident errors about a legitimate pattern, which is
|
|
180
|
+
* worse than not checking at all.
|
|
181
|
+
*/
|
|
182
|
+
export function checkDeadTweaks(html, manifest) {
|
|
183
|
+
const controls = manifest?.controls ?? [];
|
|
184
|
+
if (controls.length === 0)
|
|
185
|
+
return [];
|
|
186
|
+
if (/\bapplyParams\s*[=:(]/.test(html))
|
|
187
|
+
return [];
|
|
188
|
+
const dead = controls
|
|
189
|
+
.map((c) => c.param)
|
|
190
|
+
.filter((param) => {
|
|
191
|
+
if (!param)
|
|
192
|
+
return false;
|
|
193
|
+
const safe = param.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
194
|
+
const bound = new RegExp(`var\\(\\s*--${safe}\\b|data-bind\\s*=\\s*["']${safe}["']|data-show\\s*=\\s*["']${safe}["']`, 'i');
|
|
195
|
+
return !bound.test(html);
|
|
196
|
+
});
|
|
197
|
+
if (dead.length === 0)
|
|
198
|
+
return [];
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
level: 'error',
|
|
202
|
+
message: `Tweak${dead.length > 1 ? 's' : ''} declared but bound to nothing: ${dead.join(', ')}. ` +
|
|
203
|
+
'A control the user can drag that changes nothing reads as a broken canvas. Either ' +
|
|
204
|
+
'reference it — var(--param) in CSS, data-bind="param" for text, data-show="param" ' +
|
|
205
|
+
'to toggle — or remove it from the manifest.',
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
/** Every quality check, in the order their messages should be read. */
|
|
210
|
+
export function runQualityChecks(html, manifest) {
|
|
211
|
+
return [
|
|
212
|
+
...checkSurfaceTokens(html),
|
|
213
|
+
...checkTextContrast(html),
|
|
214
|
+
...checkHueCount(html),
|
|
215
|
+
...checkDeadTweaks(html, manifest),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surface ramp the host injects.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ WHY THE HOST AND NOT CSS. A single `color-mix` cannot serve both polarities: a light
|
|
5
|
+
* theme's ground has no headroom above it (Catppuccin Latte sits at ~95% lightness, so a
|
|
6
|
+
* card 6% "above" it would be past white), while a dark theme's has plenty. CSS cannot
|
|
7
|
+
* branch on a palette's polarity. Whatever computes this has to know the luminance, so it
|
|
8
|
+
* lives here — pure, testable, and shared with the validator so the two cannot disagree
|
|
9
|
+
* about what "5–7% apart" means.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ THE GROUND MOVES ONLY WHEN IT MUST. The handoff's worked examples darken the ground
|
|
12
|
+
* in every case; this does it only when there is no room for the raised surface, so a
|
|
13
|
+
* dark theme's canvas keeps exactly the background its theme specifies and nothing about
|
|
14
|
+
* an existing canvas shifts. Latte has no room, so it darkens — there is no alternative
|
|
15
|
+
* that also keeps the card raised, which is the point of the ramp.
|
|
16
|
+
*/
|
|
17
|
+
import { type Rgb } from './color.js';
|
|
18
|
+
export interface SurfaceRamp {
|
|
19
|
+
/** The page ground. Equal to the input unless there was no room for the ramp. */
|
|
20
|
+
bg: string;
|
|
21
|
+
/** Raised — cards. */
|
|
22
|
+
surface1: string;
|
|
23
|
+
/** Inset — tables, code, an app screen's work area. */
|
|
24
|
+
surface2: string;
|
|
25
|
+
/** 1px borders. */
|
|
26
|
+
hairline: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The same colour at a different perceived lightness.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ Binary search over a mix toward white or black rather than an OKLab inverse. It
|
|
32
|
+
* keeps the hue of the ground (a tinted terminal ground stays tinted, which is most of
|
|
33
|
+
* why these palettes look like themselves) and cannot produce an out-of-gamut value,
|
|
34
|
+
* which an inverse transform can.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withLightness(color: Rgb, targetL: number): Rgb;
|
|
37
|
+
/**
|
|
38
|
+
* Derive the three ramp tokens from a theme's ground and foreground.
|
|
39
|
+
*
|
|
40
|
+
* Raised is always lighter and inset always darker, in both polarities — a card that
|
|
41
|
+
* recedes from its page does not read as a card. When the ground cannot support both
|
|
42
|
+
* steps, the ground itself is moved just far enough to make room, and the returned `bg`
|
|
43
|
+
* says so.
|
|
44
|
+
*/
|
|
45
|
+
export declare function deriveSurfaceRamp(bgInput: string, fgInput: string): SurfaceRamp | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surface ramp the host injects.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ WHY THE HOST AND NOT CSS. A single `color-mix` cannot serve both polarities: a light
|
|
5
|
+
* theme's ground has no headroom above it (Catppuccin Latte sits at ~95% lightness, so a
|
|
6
|
+
* card 6% "above" it would be past white), while a dark theme's has plenty. CSS cannot
|
|
7
|
+
* branch on a palette's polarity. Whatever computes this has to know the luminance, so it
|
|
8
|
+
* lives here — pure, testable, and shared with the validator so the two cannot disagree
|
|
9
|
+
* about what "5–7% apart" means.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ THE GROUND MOVES ONLY WHEN IT MUST. The handoff's worked examples darken the ground
|
|
12
|
+
* in every case; this does it only when there is no room for the raised surface, so a
|
|
13
|
+
* dark theme's canvas keeps exactly the background its theme specifies and nothing about
|
|
14
|
+
* an existing canvas shifts. Latte has no room, so it darkens — there is no alternative
|
|
15
|
+
* that also keeps the card raised, which is the point of the ramp.
|
|
16
|
+
*/
|
|
17
|
+
import { parseColor, oklabLightness, SURFACE_DELTA_TARGET, HAIRLINE_MIN_PCT, HAIRLINE_MAX_PCT, } from './color.js';
|
|
18
|
+
const hex = (n) => Math.round(Math.min(1, Math.max(0, n)) * 255)
|
|
19
|
+
.toString(16)
|
|
20
|
+
.padStart(2, '0');
|
|
21
|
+
const toHex = ({ r, g, b }) => `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
22
|
+
const mix = (a, b, t) => ({
|
|
23
|
+
r: a.r + (b.r - a.r) * t,
|
|
24
|
+
g: a.g + (b.g - a.g) * t,
|
|
25
|
+
b: a.b + (b.b - a.b) * t,
|
|
26
|
+
});
|
|
27
|
+
const WHITE = { r: 1, g: 1, b: 1 };
|
|
28
|
+
const BLACK = { r: 0, g: 0, b: 0 };
|
|
29
|
+
/**
|
|
30
|
+
* The same colour at a different perceived lightness.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ Binary search over a mix toward white or black rather than an OKLab inverse. It
|
|
33
|
+
* keeps the hue of the ground (a tinted terminal ground stays tinted, which is most of
|
|
34
|
+
* why these palettes look like themselves) and cannot produce an out-of-gamut value,
|
|
35
|
+
* which an inverse transform can.
|
|
36
|
+
*/
|
|
37
|
+
export function withLightness(color, targetL) {
|
|
38
|
+
const current = oklabLightness(color);
|
|
39
|
+
if (Math.abs(current - targetL) < 0.001)
|
|
40
|
+
return color;
|
|
41
|
+
const toward = targetL > current ? WHITE : BLACK;
|
|
42
|
+
let lo = 0;
|
|
43
|
+
let hi = 1;
|
|
44
|
+
let out = color;
|
|
45
|
+
for (let i = 0; i < 24; i++) {
|
|
46
|
+
const mid = (lo + hi) / 2;
|
|
47
|
+
out = mix(color, toward, mid);
|
|
48
|
+
const l = oklabLightness(out);
|
|
49
|
+
if (Math.abs(l - targetL) < 0.0005)
|
|
50
|
+
break;
|
|
51
|
+
if (targetL > current ? l < targetL : l > targetL)
|
|
52
|
+
lo = mid;
|
|
53
|
+
else
|
|
54
|
+
hi = mid;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Derive the three ramp tokens from a theme's ground and foreground.
|
|
60
|
+
*
|
|
61
|
+
* Raised is always lighter and inset always darker, in both polarities — a card that
|
|
62
|
+
* recedes from its page does not read as a card. When the ground cannot support both
|
|
63
|
+
* steps, the ground itself is moved just far enough to make room, and the returned `bg`
|
|
64
|
+
* says so.
|
|
65
|
+
*/
|
|
66
|
+
export function deriveSurfaceRamp(bgInput, fgInput) {
|
|
67
|
+
const bg = parseColor(bgInput);
|
|
68
|
+
const fg = parseColor(fgInput);
|
|
69
|
+
if (!bg || !fg)
|
|
70
|
+
return null;
|
|
71
|
+
const step = SURFACE_DELTA_TARGET / 100;
|
|
72
|
+
let ground = oklabLightness(bg);
|
|
73
|
+
/*
|
|
74
|
+
Keep both steps inside the gamut by sliding the ground, never by shrinking the step —
|
|
75
|
+
a 3% ramp is the defect this exists to fix, so the separation is the invariant and the
|
|
76
|
+
ground's exact value is not.
|
|
77
|
+
*/
|
|
78
|
+
const headroom = 1 - (ground + step);
|
|
79
|
+
const footroom = ground - step;
|
|
80
|
+
if (headroom < 0)
|
|
81
|
+
ground += headroom;
|
|
82
|
+
else if (footroom < 0)
|
|
83
|
+
ground -= footroom;
|
|
84
|
+
const groundRgb = withLightness(bg, ground);
|
|
85
|
+
/*
|
|
86
|
+
The hairline sits a fixed fraction of the way toward the foreground. Terminal palettes
|
|
87
|
+
derive their border from a comment or selection colour, which lands 35–100% of the way
|
|
88
|
+
there — that is the "hard borders" complaint, and it is why this ignores
|
|
89
|
+
--canvas-border entirely rather than trying to soften it.
|
|
90
|
+
*/
|
|
91
|
+
const hairlineMix = (HAIRLINE_MIN_PCT + HAIRLINE_MAX_PCT) / 2 / 100;
|
|
92
|
+
return {
|
|
93
|
+
bg: toHex(groundRgb),
|
|
94
|
+
surface1: toHex(withLightness(groundRgb, ground + step)),
|
|
95
|
+
surface2: toHex(withLightness(groundRgb, ground - step)),
|
|
96
|
+
hairline: toHex(mix(groundRgb, fg, hairlineMix)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -244,11 +244,25 @@ export const CAPABILITY_PACKS = {
|
|
|
244
244
|
'canvas_delete',
|
|
245
245
|
'canvas_validate',
|
|
246
246
|
'canvas_screenshot',
|
|
247
|
+
'canvas_guide',
|
|
248
|
+
'canvas_asset_add',
|
|
249
|
+
'canvas_asset_list',
|
|
250
|
+
'canvas_asset_delete',
|
|
247
251
|
],
|
|
248
252
|
readOnly: false,
|
|
249
253
|
promptModules: ['platform-tool-hints'],
|
|
250
|
-
promptSnippet: 'Visual canvases (infographic/carousel/board) rendered from sandboxed HTML/SVG.
|
|
251
|
-
|
|
254
|
+
promptSnippet: 'Visual canvases (infographic/carousel/board) rendered from sandboxed HTML/SVG. ' +
|
|
255
|
+
// ⚠️ FIRST, and stated as a requirement rather than a suggestion. The craft guides
|
|
256
|
+
// used to be reachable only when a user typed /canvas, so most canvases were
|
|
257
|
+
// authored with no design guidance at all — see canvas_guide.
|
|
258
|
+
'START by calling canvas_guide("canvas") — the design rules live there, not here, and a ' +
|
|
259
|
+
'canvas authored without them renders correctly and still reads as generic. Then ' +
|
|
260
|
+
'canvas_guide on the type you pick. ' +
|
|
261
|
+
// ⚠️ Real images go in as ASSETS, never as base64 in the markup.
|
|
262
|
+
'When the user has supplied real images, put them in with canvas_asset_add and reference ' +
|
|
263
|
+
'them as <img src="asset:REF"> — never paste base64, never draw a grey box instead. ' +
|
|
264
|
+
'Create/replace with canvas_write (raw HTML only — host injects the CSP; inline <style>/<script> only, no network/remote assets; optional Tweaks controls manifest). EDIT via canvas_get (outline=true or startLine/maxLines) then canvas_edit (str_replace/append/prepend) — never re-send the whole document. After authoring, canvas_validate (static) then canvas_screenshot to SEE the render (vision models) — fix overflow/readability with canvas_edit.',
|
|
265
|
+
estimatedPromptTokens: 190,
|
|
252
266
|
estimatedToolTokens: 1500,
|
|
253
267
|
},
|
|
254
268
|
plans: {
|
|
@@ -45,8 +45,22 @@ export interface PlatformHooks {
|
|
|
45
45
|
export interface PlatformToolsConfig {
|
|
46
46
|
/** Data access layer */
|
|
47
47
|
context: PlatformContext;
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Working directory override (defaults to process.cwd()).
|
|
50
|
+
* Accepts a getter, because platform tools are memoised and a host's active
|
|
51
|
+
* project can change after they are built.
|
|
52
|
+
*/
|
|
53
|
+
cwd?: string | (() => string | undefined | null);
|
|
50
54
|
/** Optional hooks for CLI-specific side effects */
|
|
51
55
|
hooks?: PlatformHooks;
|
|
52
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a `cwd` that may be a value or a getter.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ Platform tools are built once and memoised by every host, so a plain string is
|
|
61
|
+
* captured at construction. In Desktop that meant relative paths resolved against
|
|
62
|
+
* whichever project was open when the tools were first requested — for the rest of the
|
|
63
|
+
* session, across every project switch. Hosts whose working directory moves pass a
|
|
64
|
+
* getter; this is the one place that unwraps it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveCwd(cwd?: string | (() => string | undefined | null)): string;
|
package/dist/platform/context.js
CHANGED
|
@@ -4,4 +4,16 @@
|
|
|
4
4
|
* Provides a single entry point for data access, abstracting over the
|
|
5
5
|
* underlying storage backend (SQLite in CLI, PostgreSQL in web/API).
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a `cwd` that may be a value or a getter.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ Platform tools are built once and memoised by every host, so a plain string is
|
|
11
|
+
* captured at construction. In Desktop that meant relative paths resolved against
|
|
12
|
+
* whichever project was open when the tools were first requested — for the rest of the
|
|
13
|
+
* session, across every project switch. Hosts whose working directory moves pass a
|
|
14
|
+
* getter; this is the one place that unwraps it.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveCwd(cwd) {
|
|
17
|
+
const value = typeof cwd === 'function' ? cwd() : cwd;
|
|
18
|
+
return value ?? process.cwd();
|
|
19
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Project, WorkItem, WorkItemComment, ProjectDocument, Plan, PlanSummary, PlanWithWorkItem, HistoryEntry, CreateProjectInput, UpdateProjectInput, ProjectListOptions, CreateWorkItemInput, UpdateWorkItemInput, QueryWorkItemsInput, CreateCommentInput, UpdateCommentInput, CreateDocumentInput, UpdateDocumentInput, CreatePlanInput, UpdatePlanInput, ListPlansOptions, WorkItemQueryResult, ProjectListResult, BulkCreateItem, ProjectStatus, WorkItemType, WorkItemStatus, DocumentType, PlanStatus } from './types.js';
|
|
9
9
|
import type { CanvasRecord, CanvasSummary, CreateCanvasInput, UpdateCanvasInput } from '../canvas/types.js';
|
|
10
|
+
import type { CanvasAsset, CreateCanvasAssetInput } from '../canvas/assets.js';
|
|
10
11
|
export interface IProjectRepository {
|
|
11
12
|
create(input: CreateProjectInput): Promise<Project>;
|
|
12
13
|
getById(id: number): Promise<Project | null>;
|
|
@@ -53,6 +54,9 @@ export interface ICanvasRepository {
|
|
|
53
54
|
listByProject(projectId: number): Promise<CanvasSummary[]>;
|
|
54
55
|
update(id: number, input: UpdateCanvasInput): Promise<CanvasRecord | null>;
|
|
55
56
|
delete(id: number): Promise<boolean>;
|
|
57
|
+
addAsset?(input: CreateCanvasAssetInput): Promise<CanvasAsset>;
|
|
58
|
+
listAssets?(canvasId: number): Promise<CanvasAsset[]>;
|
|
59
|
+
deleteAsset?(canvasId: number, ref: string): Promise<boolean>;
|
|
56
60
|
}
|
|
57
61
|
export interface IPlanRepository {
|
|
58
62
|
create(input: CreatePlanInput): Promise<Plan>;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* SQL reserved word, so it is always quoted as "values" in statements.
|
|
6
6
|
*/
|
|
7
7
|
import type Database from 'better-sqlite3';
|
|
8
|
+
import type { CanvasAsset, CreateCanvasAssetInput } from '../../canvas/assets.js';
|
|
8
9
|
import type { ICanvasRepository } from '../repositories.js';
|
|
9
10
|
import type { CanvasRecord, CanvasSummary, CreateCanvasInput, UpdateCanvasInput } from '../../canvas/types.js';
|
|
10
11
|
export declare class SQLiteCanvasRepository implements ICanvasRepository {
|
|
@@ -15,4 +16,12 @@ export declare class SQLiteCanvasRepository implements ICanvasRepository {
|
|
|
15
16
|
listByProject(projectId: number): Promise<CanvasSummary[]>;
|
|
16
17
|
update(id: number, input: UpdateCanvasInput): Promise<CanvasRecord | null>;
|
|
17
18
|
delete(id: number): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* ⚠️ UPSERT on (canvas_id, ref). Re-adding the same handle REPLACES the image rather
|
|
21
|
+
* than failing on the unique index — an agent correcting a photo should not have to
|
|
22
|
+
* delete first, and a half-applied fix is worse than an overwrite.
|
|
23
|
+
*/
|
|
24
|
+
addAsset(input: CreateCanvasAssetInput): Promise<CanvasAsset>;
|
|
25
|
+
listAssets(canvasId: number): Promise<CanvasAsset[]>;
|
|
26
|
+
deleteAsset(canvasId: number, ref: string): Promise<boolean>;
|
|
18
27
|
}
|
|
@@ -92,4 +92,56 @@ export class SQLiteCanvasRepository {
|
|
|
92
92
|
const result = this.db.prepare('DELETE FROM canvases WHERE id = ?').run(id);
|
|
93
93
|
return Promise.resolve(result.changes > 0);
|
|
94
94
|
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Assets — images stored beside the canvas, referenced from its markup as
|
|
97
|
+
// `asset:<ref>` and substituted by the host at render and export.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
/**
|
|
100
|
+
* ⚠️ UPSERT on (canvas_id, ref). Re-adding the same handle REPLACES the image rather
|
|
101
|
+
* than failing on the unique index — an agent correcting a photo should not have to
|
|
102
|
+
* delete first, and a half-applied fix is worse than an overwrite.
|
|
103
|
+
*/
|
|
104
|
+
addAsset(input) {
|
|
105
|
+
this.db
|
|
106
|
+
.prepare(`INSERT INTO canvas_assets (canvas_id, ref, media_type, data, width, height, bytes, source_path)
|
|
107
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
108
|
+
ON CONFLICT(canvas_id, ref) DO UPDATE SET
|
|
109
|
+
media_type = excluded.media_type,
|
|
110
|
+
data = excluded.data,
|
|
111
|
+
width = excluded.width,
|
|
112
|
+
height = excluded.height,
|
|
113
|
+
bytes = excluded.bytes,
|
|
114
|
+
source_path = excluded.source_path`)
|
|
115
|
+
.run(input.canvasId, input.ref, input.mediaType, input.data, input.width ?? null, input.height ?? null, input.bytes, input.sourcePath ?? null);
|
|
116
|
+
const row = this.db
|
|
117
|
+
.prepare('SELECT * FROM canvas_assets WHERE canvas_id = ? AND ref = ?')
|
|
118
|
+
.get(input.canvasId, input.ref);
|
|
119
|
+
return Promise.resolve(toAsset(row));
|
|
120
|
+
}
|
|
121
|
+
listAssets(canvasId) {
|
|
122
|
+
const rows = this.db
|
|
123
|
+
.prepare('SELECT * FROM canvas_assets WHERE canvas_id = ? ORDER BY id')
|
|
124
|
+
.all(canvasId);
|
|
125
|
+
return Promise.resolve(rows.map(toAsset));
|
|
126
|
+
}
|
|
127
|
+
deleteAsset(canvasId, ref) {
|
|
128
|
+
const result = this.db
|
|
129
|
+
.prepare('DELETE FROM canvas_assets WHERE canvas_id = ? AND ref = ?')
|
|
130
|
+
.run(canvasId, ref);
|
|
131
|
+
return Promise.resolve(result.changes > 0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function toAsset(row) {
|
|
135
|
+
return {
|
|
136
|
+
id: row.id,
|
|
137
|
+
canvasId: row.canvas_id,
|
|
138
|
+
ref: row.ref,
|
|
139
|
+
mediaType: row.media_type,
|
|
140
|
+
data: row.data,
|
|
141
|
+
width: row.width ?? undefined,
|
|
142
|
+
height: row.height ?? undefined,
|
|
143
|
+
bytes: row.bytes,
|
|
144
|
+
sourcePath: row.source_path ?? undefined,
|
|
145
|
+
createdAt: new Date(row.created_at),
|
|
146
|
+
};
|
|
95
147
|
}
|
|
@@ -180,4 +180,33 @@ function runMigrations(db, fromVersion, toVersion) {
|
|
|
180
180
|
db.exec(`ALTER TABLE canvases ADD COLUMN page_size TEXT;`);
|
|
181
181
|
db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(9);
|
|
182
182
|
}
|
|
183
|
+
if (fromVersion < 10 && toVersion >= 10) {
|
|
184
|
+
/*
|
|
185
|
+
Canvas assets — images stored BESIDE a canvas rather than inside it.
|
|
186
|
+
|
|
187
|
+
⚠️ The markup references `asset:<ref>` and the host substitutes a data URI at render
|
|
188
|
+
and at export. Inlining base64 into `content` would also render (the CSP allows
|
|
189
|
+
data:) but would put hundreds of KB in the middle of a document agents read as an
|
|
190
|
+
outline and patch with str_replace — the editing discipline that keeps canvas
|
|
191
|
+
authoring reliable stops working at that size.
|
|
192
|
+
*/
|
|
193
|
+
db.exec(`
|
|
194
|
+
CREATE TABLE IF NOT EXISTS canvas_assets (
|
|
195
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
196
|
+
canvas_id INTEGER NOT NULL,
|
|
197
|
+
ref TEXT NOT NULL,
|
|
198
|
+
media_type TEXT NOT NULL,
|
|
199
|
+
data TEXT NOT NULL,
|
|
200
|
+
width INTEGER,
|
|
201
|
+
height INTEGER,
|
|
202
|
+
bytes INTEGER NOT NULL,
|
|
203
|
+
source_path TEXT,
|
|
204
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
205
|
+
FOREIGN KEY (canvas_id) REFERENCES canvases(id) ON DELETE CASCADE
|
|
206
|
+
);
|
|
207
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_canvas_assets_ref
|
|
208
|
+
ON canvas_assets(canvas_id, ref);
|
|
209
|
+
`);
|
|
210
|
+
db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(10);
|
|
211
|
+
}
|
|
183
212
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Shared between CLI and Desktop — both access ~/.compilr-dev/projects.db
|
|
5
5
|
* Schema version must be kept in sync across all consumers.
|
|
6
6
|
*/
|
|
7
|
-
export declare const SCHEMA_VERSION =
|
|
7
|
+
export declare const SCHEMA_VERSION = 10;
|
|
8
8
|
export declare const SCHEMA_SQL = "\n-- Schema version tracking\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Projects table\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n display_name TEXT NOT NULL,\n description TEXT,\n type TEXT DEFAULT 'general',\n status TEXT DEFAULT 'active',\n path TEXT NOT NULL,\n docs_path TEXT,\n repo_pattern TEXT DEFAULT 'single',\n language TEXT,\n framework TEXT,\n package_manager TEXT,\n runtime_version TEXT,\n commands TEXT,\n git_remote TEXT,\n git_branch TEXT DEFAULT 'main',\n workflow_mode TEXT DEFAULT 'flexible',\n lifecycle_state TEXT DEFAULT 'setup',\n current_item_id TEXT,\n last_context TEXT,\n metadata TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n last_activity_at DATETIME\n);\n\n-- Work items (backlog items, tasks, bugs)\nCREATE TABLE IF NOT EXISTS work_items (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n item_number INTEGER NOT NULL,\n item_id TEXT NOT NULL,\n type TEXT NOT NULL,\n status TEXT DEFAULT 'backlog',\n priority TEXT DEFAULT 'medium',\n guided_step TEXT,\n owner TEXT,\n title TEXT NOT NULL,\n description TEXT,\n estimated_effort TEXT,\n actual_minutes INTEGER,\n completed_at DATETIME,\n completed_by TEXT,\n commit_hash TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n UNIQUE (project_id, item_id)\n);\n\n-- Project documents (PRD, architecture, plans, etc.)\nCREATE TABLE IF NOT EXISTS project_documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n doc_type TEXT NOT NULL,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n status TEXT,\n work_item_id INTEGER,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE SET NULL\n);\n\n-- Work item history (audit trail)\nCREATE TABLE IF NOT EXISTS work_item_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n work_item_id INTEGER NOT NULL,\n project_id INTEGER NOT NULL,\n action TEXT NOT NULL,\n old_value TEXT,\n new_value TEXT,\n notes TEXT,\n changed_by TEXT,\n changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_path ON projects(path);\nCREATE INDEX IF NOT EXISTS idx_projects_docs_path ON projects(docs_path);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_work_items_project ON work_items(project_id);\nCREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status);\nCREATE INDEX IF NOT EXISTS idx_work_items_priority ON work_items(priority);\nCREATE INDEX IF NOT EXISTS idx_work_items_owner ON work_items(owner);\nCREATE INDEX IF NOT EXISTS idx_project_documents_project ON project_documents(project_id);\nCREATE INDEX IF NOT EXISTS idx_project_documents_type ON project_documents(doc_type);\nCREATE INDEX IF NOT EXISTS idx_project_documents_status ON project_documents(status);\nCREATE INDEX IF NOT EXISTS idx_project_documents_work_item ON project_documents(work_item_id);\nCREATE INDEX IF NOT EXISTS idx_work_item_history_item ON work_item_history(work_item_id);\n\n-- Terminal sessions (multi-terminal awareness)\nCREATE TABLE IF NOT EXISTS terminal_sessions (\n id TEXT PRIMARY KEY,\n project_id INTEGER,\n pid INTEGER NOT NULL,\n tty_path TEXT,\n label TEXT,\n started_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,\n active_agent TEXT DEFAULT 'default',\n agents_json TEXT DEFAULT '[]',\n status TEXT DEFAULT 'active',\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL\n);\nCREATE INDEX IF NOT EXISTS idx_terminal_sessions_project ON terminal_sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_terminal_sessions_status ON terminal_sessions(status);\n\n-- File locks (multi-terminal file lock awareness)\nCREATE TABLE IF NOT EXISTS file_locks (\n path TEXT NOT NULL,\n project_id INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n locked_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (path, project_id),\n FOREIGN KEY (session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_file_locks_session ON file_locks(session_id);\n\n-- Session notifications (cross-session notifications)\nCREATE TABLE IF NOT EXISTS session_notifications (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n from_session_id TEXT NOT NULL,\n to_session_id TEXT,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n message TEXT,\n payload_json TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n read_at DATETIME,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,\n FOREIGN KEY (from_session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_project ON session_notifications(project_id);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_to_session ON session_notifications(to_session_id);\nCREATE INDEX IF NOT EXISTS idx_session_notifications_unread ON session_notifications(read_at);\n\n-- Work item comments\nCREATE TABLE IF NOT EXISTS work_item_comments (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n work_item_id INTEGER NOT NULL,\n project_id INTEGER NOT NULL,\n author TEXT NOT NULL,\n content TEXT NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE CASCADE,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_work_item_comments_work_item ON work_item_comments(work_item_id);\nCREATE INDEX IF NOT EXISTS idx_work_item_comments_project ON work_item_comments(project_id);\n\n-- Canvases (visual reasoning surfaces: infographic / carousel / board)\nCREATE TABLE IF NOT EXISTS canvases (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n controls TEXT NOT NULL DEFAULT '{\"controls\":[]}',\n \"values\" TEXT NOT NULL DEFAULT '{}',\n page_size TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE\n);\nCREATE INDEX IF NOT EXISTS idx_canvases_project ON canvases(project_id);\n";
|
|
9
9
|
export interface ProjectRecord {
|
|
10
10
|
id: number;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Shared between CLI and Desktop — both access ~/.compilr-dev/projects.db
|
|
5
5
|
* Schema version must be kept in sync across all consumers.
|
|
6
6
|
*/
|
|
7
|
-
export const SCHEMA_VERSION =
|
|
7
|
+
export const SCHEMA_VERSION = 10;
|
|
8
8
|
export const SCHEMA_SQL = `
|
|
9
9
|
-- Schema version tracking
|
|
10
10
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* manifest (validated here). Persistence is delegated to ctx.canvases
|
|
11
11
|
* (ICanvasRepository); the current project is resolved like the document tools.
|
|
12
12
|
*/
|
|
13
|
+
import type { ImageResizer } from './image-tools.js';
|
|
13
14
|
import type { PlatformToolsConfig } from '../context.js';
|
|
14
15
|
import type { CanvasType, ControlManifest } from '../../canvas/types.js';
|
|
15
16
|
/**
|
|
@@ -29,8 +30,10 @@ export interface CanvasIssue {
|
|
|
29
30
|
message: string;
|
|
30
31
|
}
|
|
31
32
|
/** Run deterministic static checks on canvas content. No rendering. */
|
|
32
|
-
export declare function runCanvasChecks(html: string, type: CanvasType): CanvasIssue[];
|
|
33
|
-
export declare function createCanvasTools(config: PlatformToolsConfig
|
|
33
|
+
export declare function runCanvasChecks(html: string, type: CanvasType, manifest?: ControlManifest): CanvasIssue[];
|
|
34
|
+
export declare function createCanvasTools(config: PlatformToolsConfig, imageConfig?: {
|
|
35
|
+
resizer?: ImageResizer;
|
|
36
|
+
}): (import("@compilr-dev/agents").Tool<{
|
|
34
37
|
type: string;
|
|
35
38
|
title: string;
|
|
36
39
|
html: string;
|
|
@@ -50,4 +53,11 @@ export declare function createCanvasTools(config: PlatformToolsConfig): (import(
|
|
|
50
53
|
new_str?: string;
|
|
51
54
|
content?: string;
|
|
52
55
|
replace_all?: boolean;
|
|
56
|
+
}> | import("@compilr-dev/agents").Tool<{
|
|
57
|
+
topic: string;
|
|
58
|
+
}> | import("@compilr-dev/agents").Tool<{
|
|
59
|
+
canvas_id: number;
|
|
60
|
+
path: string;
|
|
61
|
+
ref: string;
|
|
62
|
+
max_width?: number;
|
|
53
63
|
}>)[];
|