@lark-apaas/coding-miaoda-sandbox-skills 0.1.0-dev.7b2c2ad → 0.1.0-dev.7cbbb20

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 (72) hide show
  1. package/miaoda/charts-skill/SKILL.md +1 -1
  2. package/miaoda/creative-to-fullstack/SKILL.md +38 -226
  3. package/miaoda/debug-investigation/SKILL.md +17 -8
  4. package/miaoda/feishu/SKILL.md +4 -4
  5. package/miaoda/forms-skill/SKILL.md +46 -4
  6. package/miaoda/lark-apps-db/SKILL.md +12 -4
  7. package/miaoda/lark-apps-db/references/full-reference.md +13 -4
  8. package/miaoda/lark-apps-ops/SKILL.md +2 -1
  9. package/miaoda/lark-apps-ops/references/lark-apps-export.md +46 -0
  10. package/miaoda/lark-design-prototype/DESIGN.md +603 -0
  11. package/miaoda/lark-design-prototype/SKILL.md +85 -0
  12. package/miaoda/lark-design-prototype/references/assets/card-illustration-library.md +113 -0
  13. package/miaoda/lark-design-prototype/references/case-matching.md +53 -0
  14. package/miaoda/lark-design-prototype/references/cases/conversational-ai-home.md +27 -0
  15. package/miaoda/lark-design-prototype/references/cases/data-table.md +30 -0
  16. package/miaoda/lark-design-prototype/references/cases/official-home.md +26 -0
  17. package/miaoda/lark-design-prototype/references/cases/workspace-home.md +34 -0
  18. package/miaoda/lark-design-prototype/references/color-roles.md +163 -0
  19. package/miaoda/lark-design-prototype/references/component-selection.md +134 -0
  20. package/miaoda/lark-design-prototype/references/design-quality-checklist.md +156 -0
  21. package/miaoda/lark-design-prototype/references/form-shell-patterns.md +77 -0
  22. package/miaoda/lark-design-prototype/references/icon-semantics.md +237 -0
  23. package/miaoda/lark-design-prototype/references/layout-interaction.md +164 -0
  24. package/miaoda/lark-design-prototype/references/page-contract.md +281 -0
  25. package/miaoda/lark-design-prototype/references/product-patterns.md +93 -0
  26. package/miaoda/lark-design-prototype/references/prompt-expansion.md +107 -0
  27. package/miaoda/lark-design-prototype/references/restoration-traps.md +113 -0
  28. package/miaoda/lark-design-prototype/references/token-semantics.md +112 -0
  29. package/miaoda/lark-design-prototype/references/visual-brief.md +125 -0
  30. package/miaoda/lark-design-prototype/references/visual-style-prompts.md +61 -0
  31. package/miaoda/lark-design-prototype/scripts/icon-query.mjs +272 -0
  32. package/miaoda/lark-design-prototype/scripts/token-query.mjs +76 -0
  33. package/miaoda/lark-design-prototype/scripts/verify-static-html.mjs +117 -0
  34. package/miaoda/miaoda-sql/SKILL.md +6 -0
  35. package/miaoda/performance-review/SKILL.md +3 -3
  36. package/miaoda/testing-guide/SKILL.md +114 -13
  37. package/miaoda-modern/charts-skill/SKILL.md +1 -1
  38. package/miaoda-modern/debug-investigation/SKILL.md +1 -1
  39. package/miaoda-modern/forms-skill/SKILL.md +70 -7
  40. package/miaoda-modern/lark-apps-ops/SKILL.md +2 -1
  41. package/miaoda-modern/lark-apps-ops/references/lark-apps-export.md +46 -0
  42. package/miaoda-modern/lark-design-prototype/DESIGN.md +603 -0
  43. package/miaoda-modern/lark-design-prototype/SKILL.md +85 -0
  44. package/miaoda-modern/lark-design-prototype/references/assets/card-illustration-library.md +113 -0
  45. package/miaoda-modern/lark-design-prototype/references/case-matching.md +53 -0
  46. package/miaoda-modern/lark-design-prototype/references/cases/conversational-ai-home.md +27 -0
  47. package/miaoda-modern/lark-design-prototype/references/cases/data-table.md +30 -0
  48. package/miaoda-modern/lark-design-prototype/references/cases/official-home.md +26 -0
  49. package/miaoda-modern/lark-design-prototype/references/cases/workspace-home.md +34 -0
  50. package/miaoda-modern/lark-design-prototype/references/color-roles.md +163 -0
  51. package/miaoda-modern/lark-design-prototype/references/component-selection.md +134 -0
  52. package/miaoda-modern/lark-design-prototype/references/design-quality-checklist.md +156 -0
  53. package/miaoda-modern/lark-design-prototype/references/form-shell-patterns.md +77 -0
  54. package/miaoda-modern/lark-design-prototype/references/icon-semantics.md +237 -0
  55. package/miaoda-modern/lark-design-prototype/references/layout-interaction.md +164 -0
  56. package/miaoda-modern/lark-design-prototype/references/page-contract.md +281 -0
  57. package/miaoda-modern/lark-design-prototype/references/product-patterns.md +93 -0
  58. package/miaoda-modern/lark-design-prototype/references/prompt-expansion.md +107 -0
  59. package/miaoda-modern/lark-design-prototype/references/restoration-traps.md +113 -0
  60. package/miaoda-modern/lark-design-prototype/references/token-semantics.md +112 -0
  61. package/miaoda-modern/lark-design-prototype/references/visual-brief.md +125 -0
  62. package/miaoda-modern/lark-design-prototype/references/visual-style-prompts.md +61 -0
  63. package/miaoda-modern/lark-design-prototype/scripts/icon-query.mjs +272 -0
  64. package/miaoda-modern/lark-design-prototype/scripts/token-query.mjs +76 -0
  65. package/miaoda-modern/lark-design-prototype/scripts/verify-static-html.mjs +117 -0
  66. package/miaoda-modern/performance-review/SKILL.md +3 -3
  67. package/miaoda-modern/reviewer-usage/SKILL.md +2 -0
  68. package/package.json +1 -1
  69. package/shared/attachment/SKILL.md +5 -1
  70. package/miaoda/creative-to-fullstack/references/artifact-signals.md +0 -46
  71. package/miaoda/creative-to-fullstack/references/ui-to-function.md +0 -134
  72. package/miaoda-modern/testing-guide/SKILL.md +0 -221
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import { createHash } from 'node:crypto';
4
+ import path from 'node:path';
5
+
6
+ const SVG_BASE = 'https://cdn-tos-cn.bytedance.net/obj/archi/ee/es-design-base/svgs';
7
+ const REMOTE_CATALOG =
8
+ 'https://lf3-static.bytednsdoc.com/obj/eden-cn/pshi/lark-design-prototype/1.0.4/db71a6cd-33f97519-0d446cd5/icons.catalog.json';
9
+ const REMOTE_CATALOG_SHA256 = 'db71a6cd6ac90c77629e8ce0fc0905ca1488a6601800ff7533385490b10eac9d';
10
+
11
+ function usage() {
12
+ return `Usage:
13
+ node icon-query.mjs [--catalog <remote|catalog.json|url>] --query "<keywords>" [--family outlined] [--style v2-outlined|source-outlined|source-filled|source-colorful] [--shape normal|round] [--prefer-v2] [--require-v2] [--limit 5]
14
+
15
+ Returns a compact JSON array with name, hash, family, version, shape, score, description, and final svgUrl.
16
+ Never print the raw icon catalog during page generation.`;
17
+ }
18
+
19
+ function argValue(name, fallback = '') {
20
+ const index = process.argv.indexOf(name);
21
+ return index === -1 ? fallback : process.argv[index + 1] || fallback;
22
+ }
23
+
24
+ function argFlag(name) {
25
+ return process.argv.includes(name);
26
+ }
27
+
28
+ function tokenize(text) {
29
+ const rawTokens = String(text || '')
30
+ .toLowerCase()
31
+ .split(/[\s,,、::;;/|()()\[\]{}'"`<>.!??。_-]+/)
32
+ .map((item) => item.trim())
33
+ .filter(Boolean);
34
+ const aliases = {
35
+ 搜索: ['search'],
36
+ 查找: ['search'],
37
+ 通知: ['notification', 'bell'],
38
+ 提醒: ['reminder', 'bell'],
39
+ 主页: ['home'],
40
+ 首页: ['home'],
41
+ 模板: ['template'],
42
+ 文档: ['doc', 'document', 'file'],
43
+ document: ['doc'],
44
+ 文件: ['file'],
45
+ 文件夹: ['folder'],
46
+ 表格: ['sheet', 'table', 'bitable'],
47
+ spreadsheet: ['sheet'],
48
+ table: ['sheet'],
49
+ 图表: ['chart'],
50
+ 看板: ['dashboard', 'chart'],
51
+ 帮助: ['help', 'faq'],
52
+ 指南: ['guide', 'info'],
53
+ 飞书: ['feishu', 'lark', 'logo', 'logotype'],
54
+ 品牌: ['brand', 'logo', 'logotype'],
55
+ 标识: ['logo', 'logotype'],
56
+ logo: ['brand', 'logotype'],
57
+ };
58
+ const expanded = new Set(rawTokens);
59
+ for (const token of rawTokens) {
60
+ for (const alias of aliases[token] || []) {
61
+ expanded.add(alias);
62
+ }
63
+ }
64
+ return [...expanded];
65
+ }
66
+
67
+ async function readJsonFile(filePath) {
68
+ const text = await fs.readFile(filePath, 'utf8');
69
+ return JSON.parse(text);
70
+ }
71
+
72
+ async function readCatalog(source) {
73
+ const resolved = !source || source === 'remote' ? REMOTE_CATALOG : source;
74
+ if (/^https?:\/\//.test(resolved)) {
75
+ const response = await fetch(resolved);
76
+ if (!response.ok) {
77
+ throw new Error(`Catalog request failed with HTTP ${response.status}`);
78
+ }
79
+ const text = await response.text();
80
+ if (resolved === REMOTE_CATALOG) {
81
+ const actualHash = createHash('sha256').update(text).digest('hex');
82
+ if (actualHash !== REMOTE_CATALOG_SHA256) {
83
+ throw new Error(
84
+ `Catalog integrity check failed: expected ${REMOTE_CATALOG_SHA256}, got ${actualHash}`,
85
+ );
86
+ }
87
+ }
88
+ return { catalog: JSON.parse(text), catalogSource: resolved };
89
+ }
90
+ const filePath = path.resolve(resolved);
91
+ return { catalog: await readJsonFile(filePath), catalogSource: filePath };
92
+ }
93
+
94
+ function detectVersion(name) {
95
+ return /(?:^|[-_])v2(?:[-_]|$)/i.test(name) ? 'v2' : 'non-v2';
96
+ }
97
+
98
+ function detectShape(icon) {
99
+ const name = icon.name.toLowerCase();
100
+ if (name.includes('file-round-') || name.includes('wiki-round-')) return 'round';
101
+ if (icon.family === 'colorful' && /(?:icon_|wiki-)?file[-_]/.test(name)) return 'normal';
102
+ return 'none';
103
+ }
104
+
105
+ function detectStyleType(icon) {
106
+ const version = detectVersion(icon.name);
107
+ const shape = detectShape(icon);
108
+ if (icon.family === 'outlined') {
109
+ return version === 'v2' ? 'v2-outlined' : 'non-v2-outlined';
110
+ }
111
+ if (icon.family === 'filled') {
112
+ return 'source-filled';
113
+ }
114
+ if (icon.family === 'colorful' && /(?:feishu|lark).*logo|logotype|logo/i.test(icon.name)) {
115
+ return 'brand-colorful';
116
+ }
117
+ if (icon.family === 'colorful' && version === 'v2' && shape === 'round') {
118
+ return 'file-v2-colorful-round';
119
+ }
120
+ if (icon.family === 'colorful' && version === 'v2' && shape === 'normal') {
121
+ return 'file-v2-colorful-normal';
122
+ }
123
+ return icon.family;
124
+ }
125
+
126
+ function normalizeOptions() {
127
+ const style = argValue('--style');
128
+ const options = {
129
+ family: argValue('--family', 'outlined'),
130
+ style,
131
+ shape: argValue('--shape'),
132
+ preferV2: argFlag('--prefer-v2'),
133
+ requireV2: argFlag('--require-v2'),
134
+ requireNonV2: argFlag('--require-non-v2'),
135
+ include: argValue('--name-include'),
136
+ exclude: argValue('--name-exclude'),
137
+ };
138
+
139
+ if (style === 'v2-outlined') {
140
+ options.family = 'outlined';
141
+ options.requireV2 = true;
142
+ } else if (style === 'non-v2-outlined') {
143
+ options.family = 'outlined';
144
+ options.requireNonV2 = true;
145
+ } else if (style === 'file-v2-colorful-normal') {
146
+ options.family = 'colorful';
147
+ options.requireV2 = true;
148
+ options.shape = 'normal';
149
+ options.include ||= 'file';
150
+ } else if (style === 'file-v2-colorful-round') {
151
+ options.family = 'colorful';
152
+ options.requireV2 = true;
153
+ options.shape = 'round';
154
+ options.include ||= 'file-round';
155
+ } else if (style === 'source-outlined') {
156
+ options.family = 'outlined';
157
+ options.preferV2 = true;
158
+ } else if (style === 'source-filled') {
159
+ options.family = 'filled';
160
+ } else if (style === 'brand-colorful' || style === 'source-colorful') {
161
+ options.family = 'colorful';
162
+ if (style === 'source-colorful') {
163
+ options.preferV2 = true;
164
+ }
165
+ }
166
+ return options;
167
+ }
168
+
169
+ function flattenCatalog(catalog, options) {
170
+ const rows = [];
171
+ for (const category of catalog) {
172
+ for (const [family, icons] of Object.entries(category.icons || {})) {
173
+ if (options.family && family !== options.family) {
174
+ continue;
175
+ }
176
+ for (const icon of icons || []) {
177
+ const row = {
178
+ category: category.id || '',
179
+ family,
180
+ name: icon.name,
181
+ hash: icon.hash,
182
+ darkHash: icon.darkHash,
183
+ description: icon.description || '',
184
+ };
185
+ row.version = detectVersion(row.name);
186
+ row.shape = detectShape(row);
187
+ row.styleType = detectStyleType(row);
188
+ rows.push(row);
189
+ }
190
+ }
191
+ }
192
+ return rows;
193
+ }
194
+
195
+ function passesFilters(icon, options) {
196
+ const name = icon.name.toLowerCase();
197
+ if (options.requireV2 && icon.version !== 'v2') return false;
198
+ if (options.requireNonV2 && icon.version !== 'non-v2') return false;
199
+ if (options.shape && icon.shape !== options.shape) return false;
200
+ if (options.include && !name.includes(options.include.toLowerCase())) return false;
201
+ if (options.exclude && name.includes(options.exclude.toLowerCase())) return false;
202
+ const sourceStyles = new Set(['source-outlined', 'source-filled', 'source-colorful']);
203
+ if (options.style && !sourceStyles.has(options.style) && icon.styleType !== options.style)
204
+ return false;
205
+ return true;
206
+ }
207
+
208
+ function scoreIcon(icon, queryTokens, options) {
209
+ const name = icon.name.toLowerCase();
210
+ const description = icon.description.toLowerCase();
211
+ let score = 0;
212
+ for (const token of queryTokens) {
213
+ if (name.includes(token)) score += 5;
214
+ if (description.includes(token)) score += 3;
215
+ if (name === `icon_${token}_outlined`) score += 8;
216
+ if (name === `icon_${token}-v2_outlined`) score += 8;
217
+ if (name === `icon_${token}_filled`) score += 8;
218
+ if (name === `icon_file-${token}-v2_colorful`) score += 8;
219
+ }
220
+ if (score === 0) return 0;
221
+ if (icon.family === 'outlined') score += 1;
222
+ if ((options.preferV2 || options.style?.includes('v2')) && icon.version === 'v2') score += 4;
223
+ if (
224
+ options.style === 'brand-colorful' &&
225
+ /feishu|lark|logotype|logo/i.test(`${name} ${description}`)
226
+ )
227
+ score += 10;
228
+
229
+ const objectTokens = ['doc', 'document', 'file', 'card', 'sheet', 'bitable', 'ai'];
230
+ const queryHasObject = objectTokens.some((token) => queryTokens.includes(token));
231
+ if (
232
+ !queryHasObject &&
233
+ objectTokens.some((token) => name.includes(`_${token}`) || name.includes(`-${token}`))
234
+ ) {
235
+ score -= 3;
236
+ }
237
+ return score;
238
+ }
239
+
240
+ const catalogSource = argValue('--catalog', 'remote');
241
+ const query = argValue('--query');
242
+ const limit = Math.max(1, Number.parseInt(argValue('--limit', '5'), 10) || 5);
243
+
244
+ if (!query) {
245
+ console.error(usage());
246
+ process.exit(2);
247
+ }
248
+
249
+ const options = normalizeOptions();
250
+ const queryTokens = tokenize(query);
251
+ const { catalog, catalogSource: usedCatalogSource } = await readCatalog(catalogSource);
252
+ const matches = flattenCatalog(catalog, options)
253
+ .filter((icon) => passesFilters(icon, options))
254
+ .map((icon) => ({ ...icon, score: scoreIcon(icon, queryTokens, options) }))
255
+ .filter((icon) => icon.score > 0)
256
+ .sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
257
+ .slice(0, limit)
258
+ .map((icon) => ({
259
+ name: icon.name,
260
+ hash: icon.hash,
261
+ darkHash: icon.darkHash || undefined,
262
+ family: icon.family,
263
+ version: icon.version,
264
+ shape: icon.shape,
265
+ styleType: icon.styleType,
266
+ score: icon.score,
267
+ description: icon.description.slice(0, 180),
268
+ svgUrl: `${SVG_BASE}/${icon.name}.${icon.hash}.svg`,
269
+ catalogSource: usedCatalogSource,
270
+ }));
271
+
272
+ console.log(JSON.stringify(matches, null, 2));
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+
4
+ const SOURCES = {
5
+ semantic: {
6
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/pshi/lark-design-prototype/1.0.4/db71a6cd-33f97519-0d446cd5/semantic.json',
7
+ sha256: '33f975193bcd0b5b12df9003e6f8586bf4662336e0bd5cf73ea1f362cfa4eb50',
8
+ },
9
+ keys: {
10
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/pshi/lark-design-prototype/1.0.4/db71a6cd-33f97519-0d446cd5/keys.json',
11
+ sha256: '0d446cd57b0beaaaa9ba485009b31a73be77d5f87a8481ec032c54f33b3bb471',
12
+ },
13
+ };
14
+
15
+ function usage() {
16
+ return `Usage:
17
+ node token-query.mjs --query "<token name or intent>" [--source semantic|keys] [--limit 5]
18
+
19
+ Fetches the versioned public token resource, verifies SHA-256, and returns compact matches.`;
20
+ }
21
+
22
+ function argValue(name, fallback = '') {
23
+ const index = process.argv.indexOf(name);
24
+ return index === -1 ? fallback : process.argv[index + 1] || fallback;
25
+ }
26
+
27
+ function tokenize(value) {
28
+ return String(value || '')
29
+ .toLowerCase()
30
+ .split(/[\s,,、::;;/|()()\[\]{}'"`<>.!??。_-]+/)
31
+ .map((item) => item.trim())
32
+ .filter(Boolean);
33
+ }
34
+
35
+ function scoreEntry([key, value], query, queryTokens) {
36
+ const normalizedKey = key.toLowerCase();
37
+ const searchText = `${key} ${value?.$description || ''} ${value?.$extensions?.id || ''} ${
38
+ value?.$extensions?.name || ''
39
+ }`.toLowerCase();
40
+ let score = normalizedKey === query ? 100 : normalizedKey.includes(query) ? 40 : 0;
41
+ for (const token of queryTokens) {
42
+ if (normalizedKey.includes(token)) score += 10;
43
+ if (searchText.includes(token)) score += 3;
44
+ }
45
+ return score;
46
+ }
47
+
48
+ const sourceName = argValue('--source', 'semantic');
49
+ const source = SOURCES[sourceName];
50
+ const query = argValue('--query').trim().toLowerCase();
51
+ const limit = Math.max(1, Number.parseInt(argValue('--limit', '5'), 10) || 5);
52
+
53
+ if (!source || !query) {
54
+ console.error(usage());
55
+ process.exit(2);
56
+ }
57
+
58
+ const response = await fetch(source.url);
59
+ if (!response.ok) {
60
+ throw new Error(`Token request failed with HTTP ${response.status}`);
61
+ }
62
+ const text = await response.text();
63
+ const actualHash = createHash('sha256').update(text).digest('hex');
64
+ if (actualHash !== source.sha256) {
65
+ throw new Error(`Token integrity check failed: expected ${source.sha256}, got ${actualHash}`);
66
+ }
67
+
68
+ const queryTokens = tokenize(query);
69
+ const matches = Object.entries(JSON.parse(text))
70
+ .map((entry) => ({ entry, score: scoreEntry(entry, query, queryTokens) }))
71
+ .filter(({ score }) => score > 0)
72
+ .sort((a, b) => b.score - a.score || a.entry[0].localeCompare(b.entry[0]))
73
+ .slice(0, limit)
74
+ .map(({ entry: [key, value] }) => ({ key, ...value, source: source.url }));
75
+
76
+ console.log(JSON.stringify(matches, null, 2));
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ function usage() {
7
+ return `Usage:
8
+ node verify-static-html.mjs <file.html> [--sidebar]
9
+
10
+ Runs token-light pre-browser checks for Lark-style static HTML deliverables.
11
+ Use --sidebar only when the page contains a navigation sidebar. Shadow declarations are reported for contextual review.`;
12
+ }
13
+
14
+ const htmlPath = process.argv[2];
15
+ if (!htmlPath) {
16
+ console.error(usage());
17
+ process.exit(2);
18
+ }
19
+
20
+ const absoluteHtmlPath = path.resolve(htmlPath);
21
+ const html = await fs.readFile(absoluteHtmlPath, 'utf8');
22
+
23
+ const hardFailures = [];
24
+ const warnings = [];
25
+ const passed = [];
26
+
27
+ function pass(name, ok, failMessage, warn = false) {
28
+ if (ok) {
29
+ passed.push(name);
30
+ } else if (warn) {
31
+ warnings.push(failMessage);
32
+ } else {
33
+ hardFailures.push(failMessage);
34
+ }
35
+ }
36
+
37
+ pass(
38
+ 'no gradients',
39
+ !/(linear-gradient|radial-gradient|conic-gradient)/i.test(html),
40
+ 'Page UI contains gradients.',
41
+ );
42
+ const shadows = [...html.matchAll(/box-shadow\s*:\s*([^;}"'<>]+)/gi)]
43
+ .map((match) => match[1].trim())
44
+ .filter((value) => !/^none(?:\s*!important)?$/i.test(value));
45
+ pass(
46
+ 'no shadows requiring contextual review',
47
+ shadows.length === 0,
48
+ 'Shadow declarations found; verify they belong to permitted floating layers, not ordinary cards.',
49
+ true,
50
+ );
51
+ if (process.argv.includes('--sidebar')) {
52
+ pass(
53
+ 'fixed sidebar background',
54
+ html.includes('#f9f9f9'),
55
+ 'Sidebar background #f9f9f9 is missing.',
56
+ );
57
+ pass(
58
+ 'fixed selected sidebar state',
59
+ html.includes('#1f23290d'),
60
+ 'Selected sidebar state #1f23290d is missing.',
61
+ );
62
+ }
63
+ pass('has table', /<table[\s>]/i.test(html), 'No table found.', true);
64
+ pass('has search input', /type=["']search["']/i.test(html), 'No search input found.', true);
65
+ pass(
66
+ 'has local responsive CSS',
67
+ /@media\s*\(/i.test(html),
68
+ 'No responsive media query found.',
69
+ true,
70
+ );
71
+
72
+ const imageRefs = [...html.matchAll(/<img\b[^>]*\bsrc=["']([^"']+)["']/gi)].map(
73
+ (match) => match[1],
74
+ );
75
+ const localImageRefs = imageRefs.filter(
76
+ (src) => !/^(https?:)?\/\//.test(src) && !src.startsWith('data:'),
77
+ );
78
+ const missingImages = [];
79
+ for (const src of localImageRefs) {
80
+ try {
81
+ await fs.access(new URL(src, pathToFileURL(absoluteHtmlPath)));
82
+ } catch {
83
+ missingImages.push(src);
84
+ }
85
+ }
86
+
87
+ pass(
88
+ 'all local images exist',
89
+ missingImages.length === 0,
90
+ `Missing local image assets: ${missingImages.join(', ')}`,
91
+ );
92
+
93
+ const tableHeaders = [...html.matchAll(/<th\b[^>]*>([\s\S]*?)<\/th>/gi)]
94
+ .map((match) => match[1].replace(/<[^>]+>/g, '').trim())
95
+ .filter(Boolean);
96
+
97
+ console.log(
98
+ JSON.stringify(
99
+ {
100
+ file: absoluteHtmlPath,
101
+ passed,
102
+ warnings,
103
+ hardFailures,
104
+ stats: {
105
+ images: imageRefs.length,
106
+ localImages: localImageRefs.length,
107
+ tableHeaders,
108
+ },
109
+ },
110
+ null,
111
+ 2,
112
+ ),
113
+ );
114
+
115
+ if (hardFailures.length > 0) {
116
+ process.exit(1);
117
+ }
@@ -121,6 +121,7 @@ CREATE POLICY "修改本人数据" ON <table>
121
121
  4. CREATE TABLE / CREATE INDEX / ALTER TABLE ADD COLUMN 必须带 `IF NOT EXISTS`,避免重复执行失败。
122
122
  5. 执行前向用户展示目标对象和变更内容;DROP / DROP COLUMN 还要说明数据丢失风险并取得明确授权。
123
123
  6. 执行:`miaoda db sql "<ddl>"`。多条 DDL 是一个逻辑单元时放在一次调用;需要全回滚则显式包事务。
124
+ 7. 已存在的表只能用 `ALTER TABLE` 修改。`CREATE TABLE IF NOT EXISTS` 只会跳过建表,**不会**更新已有列的类型、默认值或约束;执行 `ALTER TABLE` 后再次用 `miaoda db schema get <table>` 回读 DDL,确认目标定义已经改变。
124
125
 
125
126
  ### DDL Do / Don't
126
127
 
@@ -149,6 +150,11 @@ CREATE POLICY "修改本人数据" ON <table>
149
150
  | 标量子查询多行 | `VALUES((SELECT ...))` / `SET col=(SELECT ...)` 必须唯一或 `ORDER BY ... LIMIT 1` |
150
151
  | 系统表查询 | 常规结构查询用 schema 命令;系统表仅限 reference 中列出的白名单 |
151
152
  | 多语句事务误判 | `A; B; C` 不自动包事务,B 失败时 A 不回滚;需要原子性用 `BEGIN/COMMIT` |
153
+ | 时间表达式依赖会话时区 | `timestamptz` 参与日期/小时计算、CHECK 或表达式索引时,显式写 `AT TIME ZONE 'UTC'` 或明确的业务时区;用跨日期边界的时间值验证结果,禁止依赖会话默认时区 |
154
+
155
+ ### 写入前核对全部唯一性
156
+
157
+ 新增或修改 `INSERT`、`UPDATE`、upsert 写入逻辑前,先用 `miaoda db schema get <table>` 读取目标表完整 DDL,列出其中**全部** UNIQUE 约束和唯一索引(含条件索引、表达式索引),逐条检查本次写入是否可能冲突。`ON CONFLICT` 只处理它明确匹配的那一条唯一规则,不能挡住其它唯一索引;发现其它冲突时,应在写入前按业务语义处理,或将 `23505` 转成明确的冲突错误,禁止让它冒成 500。
152
158
 
153
159
  ## Data Types and Design
154
160
 
@@ -59,11 +59,11 @@ const userMap = new Map(users.map(u => [u.id, u]));
59
59
  - N+1 queries: per-item DB calls inside loops — use batch or JOIN
60
60
  - List endpoints without pagination — will break as data grows
61
61
  - OFFSET pagination on large datasets — use cursor pagination (WHERE id > cursor)
62
- - Multiple independent DB queries running serially — use Promise.all
62
+ - Multiple independent DB queries running serially — use Promise.all. The platform connection pool caps real concurrency at **7**; beyond that queries queue. It only pays off for IO-bound queries — CPU-heavy ones (large aggregations, big `generate_series`) still take the same total time, so for those the only fix is fewer/cheaper queries
63
63
  - External API calls without timeout
64
- - WHERE/ORDER BY columns missing indexes — **cross-check with schema definition: for every ORDER BY or WHERE column in service queries, verify a matching index exists in schema.ts**
64
+ - WHERE/ORDER BY columns missing indexes — **cross-check against the live DDL: for every ORDER BY or WHERE column in service queries, verify a matching index exists via `npx -y @lark-apaas/miaoda-data-cli db schema <table>`. Do NOT use `schema.ts` for this — it is a generated mirror that silently drops partial-index `WHERE` clauses and CHECK constraints, so an existing partial index reads as missing (and a partial unique index reads as full-table unique)**
65
65
  - Aggregation done in JS instead of SQL layer — fetch all rows then compute in app is a common anti-pattern
66
- - **Sorting fields used in list/ranking endpoints must have database indexes — read schema.ts and verify**
66
+ - **Sorting fields used in list/ranking endpoints must have database indexes — verify with `db schema <table>` (the live DDL), not `schema.ts`**
67
67
  - **Date-range loop expansion in JS (CRITICAL)**: code shaped like `for (let d = new Date(start); d <= end; d.setDate(d.getDate()+1)) for (item of items) ...` is O(N×days). For 1000 records over 90 days = 90,000 ops per request. Must be pushed to SQL using `generate_series(start, end, '1 day') + unnest(array_field) + GROUP BY ... HAVING COUNT(*) > N` — let the database do the date×row cross product
68
68
  - **Multiple endpoints sharing the same base data should be merged**: e.g. Dashboard frontend calling 4 separate stats endpoints, each internally re-querying the full teacher table to build a lookup map. Merge into a single overview endpoint that runs the 4 stats in parallel internally and shares one base-data query.
69
69
  - **Stats / Dashboard endpoints must have a default time window**: any query that scans the entire fact table for aggregation (`select ... from coursePlan` with no `where`) will degrade as data grows. Add a default `where startDate >= now() - interval '90 day'` (or business-appropriate window) so it can hit `idx_*_start_date`. Without a time window, even with indexes the planner may still seq-scan. If "all history" is required, use a materialized view or pre-aggregated table refreshed on schedule, never a live full-table scan.