hd-garden 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.agents/skills/garden-post-review/SKILL.md +2 -0
  2. package/.agents/skills/garden-ui/SKILL.md +111 -0
  3. package/.env.example +8 -0
  4. package/app/admin/page.tsx +14 -3
  5. package/app/admin/preview/[...path]/page.tsx +88 -0
  6. package/app/admin/ui/blocks/page.tsx +16 -0
  7. package/app/admin/ui/catalog/page.tsx +16 -0
  8. package/app/admin/ui/evals/page.tsx +19 -0
  9. package/app/admin/ui/page.tsx +22 -0
  10. package/app/admin-ui.css +227 -0
  11. package/app/api/admin/distribution/route.ts +4 -2
  12. package/app/api/admin/posts/source/route.ts +21 -0
  13. package/app/api/admin/preview/asset/route.ts +32 -0
  14. package/app/api/admin/thoughts/route.ts +1 -1
  15. package/app/api/admin/ui/blocks/route.ts +19 -0
  16. package/app/api/admin/ui/draft/route.ts +30 -0
  17. package/app/api/auth/github/route.ts +40 -1
  18. package/app/blog/page.tsx +6 -5
  19. package/app/columns/page.tsx +1 -1
  20. package/app/components/AdminDashboard.tsx +53 -1
  21. package/app/components/AdminUiPanels.tsx +258 -0
  22. package/app/components/AdminUiShell.tsx +56 -0
  23. package/app/components/ContentEmptyState.tsx +6 -6
  24. package/app/components/MarkdownArticle.tsx +15 -5
  25. package/app/components/UiBlock.tsx +22 -0
  26. package/app/components/UiStudio.tsx +422 -0
  27. package/app/components/ui-studio-keys.ts +3 -0
  28. package/app/globals.css +2 -0
  29. package/app/lib/admin-auth.ts +13 -1
  30. package/app/lib/content.ts +2 -1
  31. package/app/lib/fallback-site-config.ts +18 -18
  32. package/app/lib/github-content.ts +47 -4
  33. package/app/lib/repository-path.mjs +55 -0
  34. package/app/lib/ui-content.ts +79 -0
  35. package/app/lib/ui-model.ts +145 -0
  36. package/app/raw.d.ts +4 -0
  37. package/app/rss.xml/route.ts +2 -1
  38. package/app/ui-parity/page.tsx +120 -0
  39. package/bin/garden.mjs +71 -0
  40. package/eslint.config.mjs +3 -0
  41. package/package.json +15 -1
  42. package/scripts/build-content.mjs +35 -4
  43. package/scripts/build-ui-preview.mjs +29 -0
  44. package/scripts/check-post.mjs +26 -4
  45. package/scripts/ui-cli.mjs +303 -0
  46. package/site.config.yaml +19 -19
  47. package/templates/site/site.config.yaml +19 -19
  48. package/tsconfig.json +1 -1
  49. package/ui/core/apply.mjs +81 -0
  50. package/ui/core/catalog.mjs +536 -0
  51. package/ui/core/compile.mjs +63 -0
  52. package/ui/core/demos.mjs +223 -0
  53. package/ui/core/expr.mjs +310 -0
  54. package/ui/core/fence.mjs +201 -0
  55. package/ui/core/fix-prompt.mjs +35 -0
  56. package/ui/core/format.mjs +96 -0
  57. package/ui/core/index.mjs +37 -0
  58. package/ui/core/parse.mjs +229 -0
  59. package/ui/core/scan.mjs +53 -0
  60. package/ui/core/sha256.mjs +66 -0
  61. package/ui/core/starters.mjs +18 -0
  62. package/ui/core/state.mjs +259 -0
  63. package/ui/core/static.mjs +524 -0
  64. package/ui/core/transform.mjs +146 -0
  65. package/ui/core/validate.mjs +665 -0
  66. package/ui/evals/cases.yaml +155 -0
  67. package/ui/preview/dist/garden-ui.css +1182 -0
  68. package/ui/preview/dist/garden-ui.esm.js +367 -0
  69. package/ui/preview/dist/garden-ui.iife.js +367 -0
  70. package/ui/preview/index.tsx +191 -0
  71. package/ui/preview/mermaid-shim.ts +19 -0
  72. package/ui/prompt/build.mjs +24 -0
  73. package/ui/prompt/recipes/code-walkthrough.md +68 -0
  74. package/ui/prompt/recipes/compare.md +48 -0
  75. package/ui/prompt/recipes/converter.md +55 -0
  76. package/ui/prompt/recipes/data-chart.md +57 -0
  77. package/ui/prompt/recipes/inspect-grid.md +45 -0
  78. package/ui/prompt/recipes/param-bench.md +55 -0
  79. package/ui/prompt/recipes/plan.md +45 -0
  80. package/ui/prompt/recipes/stepper.md +52 -0
  81. package/ui/prompt/recipes/trace.md +62 -0
  82. package/ui/prompt/rules.md +104 -0
  83. package/ui/prompt/studio.md +20 -0
  84. package/ui/react/CatalogPage.tsx +166 -0
  85. package/ui/react/components/chart.tsx +470 -0
  86. package/ui/react/components/display.tsx +296 -0
  87. package/ui/react/components/inputs.tsx +268 -0
  88. package/ui/react/components/layout.tsx +236 -0
  89. package/ui/react/components/media.tsx +184 -0
  90. package/ui/react/index.tsx +59 -0
  91. package/ui/react/recipes.ts +25 -0
  92. package/ui/react/runtime.tsx +239 -0
  93. package/ui/react/types.ts +29 -0
  94. package/ui/react/ui.css +1182 -0
  95. package/vite.config.ts +4 -3
@@ -0,0 +1,66 @@
1
+ const K = new Uint32Array([
2
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
3
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
4
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
5
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
6
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
7
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
8
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
9
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
10
+ ]);
11
+
12
+ const rotr = (value, bits) => (value >>> bits) | (value << (32 - bits));
13
+
14
+ /** Synchronous SHA-256 so the browser, Node and editor hosts compute identical hashes. */
15
+ export function sha256Hex(text) {
16
+ const bytes = new TextEncoder().encode(text);
17
+ const bitLength = bytes.length * 8;
18
+ const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
19
+ const data = new Uint8Array(paddedLength);
20
+ data.set(bytes);
21
+ data[bytes.length] = 0x80;
22
+ const view = new DataView(data.buffer);
23
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 2 ** 32));
24
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
25
+
26
+ const hash = new Uint32Array([
27
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
28
+ ]);
29
+ const words = new Uint32Array(64);
30
+
31
+ for (let offset = 0; offset < paddedLength; offset += 64) {
32
+ for (let i = 0; i < 16; i += 1) words[i] = view.getUint32(offset + i * 4);
33
+ for (let i = 16; i < 64; i += 1) {
34
+ const s0 = rotr(words[i - 15], 7) ^ rotr(words[i - 15], 18) ^ (words[i - 15] >>> 3);
35
+ const s1 = rotr(words[i - 2], 17) ^ rotr(words[i - 2], 19) ^ (words[i - 2] >>> 10);
36
+ words[i] = (words[i - 16] + s0 + words[i - 7] + s1) >>> 0;
37
+ }
38
+ let [a, b, c, d, e, f, g, h] = hash;
39
+ for (let i = 0; i < 64; i += 1) {
40
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
41
+ const choice = (e & f) ^ (~e & g);
42
+ const t1 = (h + s1 + choice + K[i] + words[i]) >>> 0;
43
+ const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
44
+ const majority = (a & b) ^ (a & c) ^ (b & c);
45
+ const t2 = (s0 + majority) >>> 0;
46
+ h = g;
47
+ g = f;
48
+ f = e;
49
+ e = (d + t1) >>> 0;
50
+ d = c;
51
+ c = b;
52
+ b = a;
53
+ a = (t1 + t2) >>> 0;
54
+ }
55
+ hash[0] = (hash[0] + a) >>> 0;
56
+ hash[1] = (hash[1] + b) >>> 0;
57
+ hash[2] = (hash[2] + c) >>> 0;
58
+ hash[3] = (hash[3] + d) >>> 0;
59
+ hash[4] = (hash[4] + e) >>> 0;
60
+ hash[5] = (hash[5] + f) >>> 0;
61
+ hash[6] = (hash[6] + g) >>> 0;
62
+ hash[7] = (hash[7] + h) >>> 0;
63
+ }
64
+
65
+ return Array.from(hash, (word) => word.toString(16).padStart(8, "0")).join("");
66
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Spoken starters for "insert an intent block". Editors show `label`, insert
3
+ * `text`; component names never appear, authors only describe what readers do.
4
+ */
5
+ export const STARTERS = [
6
+ { id: "knob", label: "让读者拧一个数", text: "让读者拖动「」,看「」怎么跟着变。" },
7
+ { id: "steps", label: "让读者一步步走完", text: "分几步演示「」,每一步高亮「」,读者可以前后翻。" },
8
+ { id: "compare", label: "并排对比两种做法", text: "把「」和「」并排对比,读者切换时能看出「」的差别。" },
9
+ { id: "grid", label: "让读者点格子看解释", text: "画一张「」的格子图,读者点每一格,显示「」。" },
10
+ { id: "flow", label: "沿着流程走", text: "沿着「」的流程走一遍,每一步说明「」。" },
11
+ { id: "checklist", label: "给一张可勾选的清单", text: "列一张「」的清单,读者可以勾选,显示完成了几项。" },
12
+ { id: "data", label: "用这份数据画图", text: "用同名目录里的「.csv」画图,横轴是「」,纵轴是「」。" },
13
+ ];
14
+
15
+ /** The fence an editor inserts; `text` may be empty for a blank block. */
16
+ export function intentFence(text = "") {
17
+ return `\`\`\`intent\n${text}\n\`\`\`\n`;
18
+ }
@@ -0,0 +1,259 @@
1
+ import { analyzeExpr, createBudget, evaluate, ExprError, RESERVED_NAMES } from "./expr.mjs";
2
+
3
+ export const NAME_PATTERN = /^[a-z][A-Za-z0-9_]*$/u;
4
+
5
+ export const isValidName = (name) => NAME_PATTERN.test(name) && !RESERVED_NAMES.has(name);
6
+
7
+ /** Walks the whole tree for State / Let / Data; they are block-wide wherever written. */
8
+ export function collectDecls(root) {
9
+ const states = [];
10
+ const lets = [];
11
+ const data = [];
12
+ const visit = (node) => {
13
+ if (!node || typeof node !== "object") return;
14
+ if (node.t === "el") {
15
+ if (node.n === "State") {
16
+ for (const [name, prop] of Object.entries(node.p)) states.push({ name, prop, line: node.l });
17
+ } else if (node.n === "Let") {
18
+ for (const [name, prop] of Object.entries(node.p)) lets.push({ name, prop, line: node.l });
19
+ } else if (node.n === "Data") {
20
+ const name = node.p.name?.k === "lit" ? node.p.name.v : null;
21
+ const src = node.p.src?.k === "lit" ? node.p.src.v : null;
22
+ if (typeof name === "string" && typeof src === "string") data.push({ name, src, line: node.l });
23
+ }
24
+ }
25
+ if (Array.isArray(node.c)) node.c.forEach(visit);
26
+ };
27
+ visit(root);
28
+ return { states, lets, data };
29
+ }
30
+
31
+ export function defaultStateValues(decls) {
32
+ const values = {};
33
+ for (const state of decls.states) {
34
+ if (state.prop.k === "lit") values[state.name] = state.prop.v;
35
+ }
36
+ return values;
37
+ }
38
+
39
+ /** Topological order of Let; `cycle` lists the names involved when there is one. */
40
+ export function orderLets(lets) {
41
+ const byName = new Map(lets.map((item) => [item.name, item]));
42
+ const dependencies = new Map(
43
+ lets.map((item) => [
44
+ item.name,
45
+ item.prop.k === "expr" ? [...analyzeExpr(item.prop.e).names].filter((name) => byName.has(name)) : [],
46
+ ]),
47
+ );
48
+ const order = [];
49
+ const state = new Map();
50
+ let cycle = null;
51
+ const visit = (name, path) => {
52
+ if (cycle) return;
53
+ const mark = state.get(name);
54
+ if (mark === "done") return;
55
+ if (mark === "active") {
56
+ cycle = [...path.slice(path.indexOf(name)), name];
57
+ return;
58
+ }
59
+ state.set(name, "active");
60
+ for (const dependency of dependencies.get(name) ?? []) visit(dependency, [...path, name]);
61
+ state.set(name, "done");
62
+ order.push(byName.get(name));
63
+ };
64
+ for (const item of lets) visit(item.name, []);
65
+ return { order: cycle ? [] : order, cycle };
66
+ }
67
+
68
+ export function lookupFrom(...layers) {
69
+ return (name) => {
70
+ for (const layer of layers) {
71
+ if (layer && Object.hasOwn(layer, name)) return { found: true, value: layer[name] };
72
+ }
73
+ return { found: false, value: undefined };
74
+ };
75
+ }
76
+
77
+ /**
78
+ * State values plus every Let, evaluated in dependency order. A Let that
79
+ * throws becomes `undefined`; `onError(item, error)` lets callers report it.
80
+ * @param {{ states: any[], lets: any[], data: any[] }} decls
81
+ * @param {{ stateValues?: Record<string, unknown>, dataValues?: Record<string, unknown>, budget?: { steps: number, max: number }, onError?: (item: any, error: any) => void }} [options]
82
+ * @returns {Record<string, unknown>}
83
+ */
84
+ export function computeScope(decls, { stateValues, dataValues = {}, budget = createBudget(), onError } = {}) {
85
+ const scope = { ...dataValues, ...stateValues };
86
+ const { order, cycle } = orderLets(decls.lets);
87
+ if (cycle) {
88
+ for (const item of decls.lets) scope[item.name] = undefined;
89
+ return scope;
90
+ }
91
+ for (const item of order) {
92
+ if (item.prop.k === "lit") {
93
+ scope[item.name] = item.prop.v;
94
+ continue;
95
+ }
96
+ try {
97
+ scope[item.name] = evaluate(item.prop.e, lookupFrom(scope), budget);
98
+ } catch (error) {
99
+ scope[item.name] = undefined;
100
+ if (onError) onError(item, error);
101
+ else if (!(error instanceof ExprError)) throw error;
102
+ }
103
+ }
104
+ return scope;
105
+ }
106
+
107
+ const isPowerOfTwo = (value) => Number.isInteger(value) && value > 0 && (value & (value - 1)) === 0;
108
+ export { isPowerOfTwo };
109
+
110
+ export function sampleDomain({ from, to, scale = "linear", samples = 64, pow2 = false }) {
111
+ if (pow2) {
112
+ const values = [];
113
+ for (let value = from; value <= to && values.length < 200; value *= 2) values.push(value);
114
+ return values;
115
+ }
116
+ const count = Math.max(2, Math.min(200, Math.trunc(samples)));
117
+ if (scale === "log") {
118
+ const low = Math.log(from);
119
+ const high = Math.log(to);
120
+ return Array.from({ length: count }, (_, index) => Math.exp(low + ((high - low) * index) / (count - 1)));
121
+ }
122
+ return Array.from({ length: count }, (_, index) => from + ((to - from) * index) / (count - 1));
123
+ }
124
+
125
+ /**
126
+ * Evaluates each series at every sample of the swept state.
127
+ * @param {{ states: any[], lets: any[], data: any[] }} decls
128
+ * @param {{ stateValues: Record<string, unknown>, dataValues?: Record<string, unknown>, sweep: string, domain: any, series: any[], budget?: { steps: number, max: number } }} options
129
+ */
130
+ export function sweepSeries(decls, { stateValues, dataValues, sweep, domain, series, budget = createBudget(400000) }) {
131
+ return sampleDomain(domain).map((x) => {
132
+ const scope = computeScope(decls, { stateValues: { ...stateValues, [sweep]: x }, dataValues, budget, onError: () => {} });
133
+ const ys = series.map((expression) => {
134
+ try {
135
+ const value = evaluate(expression, lookupFrom(scope), budget);
136
+ return typeof value === "number" ? value : Number.NaN;
137
+ } catch {
138
+ return Number.NaN;
139
+ }
140
+ });
141
+ return { x, ys };
142
+ });
143
+ }
144
+
145
+ const closeEnough = (actual, expected, tolerance) => {
146
+ if (typeof actual !== "number" || !Number.isFinite(actual)) return false;
147
+ if (expected === 0) return Math.abs(actual) <= tolerance;
148
+ return Math.abs(actual - expected) <= tolerance * Math.abs(expected);
149
+ };
150
+
151
+ /** Runs block-header `expect` items; returns failures with stable reasons. */
152
+ export function runExpect(decls, expectList, { dataValues = {} } = {}) {
153
+ const failures = [];
154
+ if (!Array.isArray(expectList)) return failures;
155
+ const defaults = defaultStateValues(decls);
156
+ const stateNames = new Set(decls.states.map((state) => state.name));
157
+ expectList.forEach((item, index) => {
158
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
159
+ failures.push({ index, reason: "shape", name: null });
160
+ return;
161
+ }
162
+ const given = item.given ?? {};
163
+ if (typeof given !== "object" || Array.isArray(given)) {
164
+ failures.push({ index, reason: "shape", name: "given" });
165
+ return;
166
+ }
167
+ const unknownGiven = Object.keys(given).find((name) => !stateNames.has(name));
168
+ if (unknownGiven) {
169
+ failures.push({ index, reason: "unknown-state", name: unknownGiven });
170
+ return;
171
+ }
172
+ const scope = computeScope(decls, { stateValues: { ...defaults, ...given }, dataValues, onError: () => {} });
173
+ for (const [name, expectation] of Object.entries(item)) {
174
+ if (name === "given") continue;
175
+ if (!Object.hasOwn(scope, name)) {
176
+ failures.push({ index, reason: "unknown-name", name });
177
+ continue;
178
+ }
179
+ const actual = scope[name];
180
+ if (expectation && typeof expectation === "object" && !Array.isArray(expectation)) {
181
+ const tolerance = typeof expectation.tol === "number" ? expectation.tol : 1e-9;
182
+ if (!closeEnough(actual, expectation.value, tolerance)) failures.push({ index, reason: "mismatch", name, actual, expected: expectation.value });
183
+ } else if (typeof expectation === "number") {
184
+ if (!closeEnough(actual, expectation, 1e-9)) failures.push({ index, reason: "mismatch", name, actual, expected: expectation });
185
+ } else if (actual !== expectation) {
186
+ failures.push({ index, reason: "mismatch", name, actual, expected: expectation });
187
+ }
188
+ }
189
+ });
190
+ return failures;
191
+ }
192
+
193
+ const NUMBER_CELL = /^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/iu;
194
+
195
+ function parseCsv(text) {
196
+ const rows = [];
197
+ let row = [];
198
+ let field = "";
199
+ let quoted = false;
200
+ for (let index = 0; index < text.length; index += 1) {
201
+ const char = text[index];
202
+ if (quoted) {
203
+ if (char === '"' && text[index + 1] === '"') {
204
+ field += '"';
205
+ index += 1;
206
+ } else if (char === '"') {
207
+ quoted = false;
208
+ } else {
209
+ field += char;
210
+ }
211
+ } else if (char === '"') {
212
+ quoted = true;
213
+ } else if (char === ",") {
214
+ row.push(field);
215
+ field = "";
216
+ } else if (char === "\n" || char === "\r") {
217
+ if (char === "\r" && text[index + 1] === "\n") index += 1;
218
+ row.push(field);
219
+ rows.push(row);
220
+ row = [];
221
+ field = "";
222
+ } else {
223
+ field += char;
224
+ }
225
+ }
226
+ if (field || row.length) {
227
+ row.push(field);
228
+ rows.push(row);
229
+ }
230
+ const [header = [], ...body] = rows.filter((current) => current.some((cell) => cell.trim()));
231
+ const keys = header.map((cell) => cell.trim());
232
+ return body.map((cells) =>
233
+ Object.fromEntries(
234
+ keys.map((key, index) => {
235
+ const raw = (cells[index] ?? "").trim();
236
+ return [key, NUMBER_CELL.test(raw) ? Number(raw) : raw];
237
+ }),
238
+ ),
239
+ );
240
+ }
241
+
242
+ export const MAX_DATA_BYTES = 200 * 1024;
243
+
244
+ /** Parses a Data file; returns `{ value }` or `{ error }`. */
245
+ export function parseDataFile(src, text) {
246
+ if (new TextEncoder().encode(text).length > MAX_DATA_BYTES) return { error: "数据文件超过 200 KB" };
247
+ const lower = src.toLowerCase();
248
+ if (lower.endsWith(".json")) {
249
+ try {
250
+ const value = JSON.parse(text);
251
+ if (!value || typeof value !== "object") return { error: "JSON 顶层必须是数组或对象" };
252
+ return { value };
253
+ } catch {
254
+ return { error: "不是有效的 JSON" };
255
+ }
256
+ }
257
+ if (lower.endsWith(".csv")) return { value: parseCsv(text) };
258
+ return { error: "只支持 .csv 或 .json" };
259
+ }