@hyperfixi/patterns-reference 3.1.1 → 3.3.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +186 -1
  2. package/README.md +47 -36
  3. package/data/engine-verification.json +58 -65
  4. package/data/patterns.db +0 -0
  5. package/data/patterns.db.stamp +1 -0
  6. package/dist/api/index.d.mts +2 -2
  7. package/dist/api/index.d.ts +2 -2
  8. package/dist/api/index.js +198 -98
  9. package/dist/api/index.mjs +197 -100
  10. package/dist/{index-6GkHj5yJ.d.mts → index-CoDUfq2P.d.mts} +39 -3
  11. package/dist/{index-6GkHj5yJ.d.ts → index-CoDUfq2P.d.ts} +39 -3
  12. package/dist/index.d.mts +87 -13
  13. package/dist/index.d.ts +87 -13
  14. package/dist/index.js +261 -144
  15. package/dist/index.mjs +258 -146
  16. package/dist/{llm-B5nGz8V1.d.mts → llm-CCCSw-yp.d.ts} +21 -10
  17. package/dist/{llm-rEdJQScF.d.ts → llm-PxnriEZ_.d.mts} +21 -10
  18. package/dist/sync/index.d.mts +1 -1
  19. package/dist/sync/index.d.ts +1 -1
  20. package/dist/sync/index.js +4 -1
  21. package/dist/sync/index.mjs +4 -1
  22. package/package.json +16 -16
  23. package/src/adapters/llm-adapter.ts +64 -42
  24. package/src/api/engine-filter.ts +37 -0
  25. package/src/api/llm.ts +54 -64
  26. package/src/api/patterns.ts +92 -30
  27. package/src/api/roles.ts +6 -4
  28. package/src/api/translations.ts +34 -27
  29. package/src/database/connection.ts +16 -4
  30. package/src/html-snippets.ts +39 -10
  31. package/src/index.ts +12 -2
  32. package/src/registry/patterns-provider.ts +3 -1
  33. package/src/sync/db-stamp.ts +7 -4
  34. package/src/sync/markup-attributes.ts +15 -0
  35. package/src/sync/verify-parses.ts +42 -0
  36. package/src/types/better-sqlite3.d.ts +2 -0
  37. package/src/types/index.ts +44 -2
  38. package/src/sync/span-mask.ts +0 -166
  39. package/src/sync/translation-checks.ts +0 -282
package/src/api/llm.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  */
6
6
 
7
7
  import { getDatabase } from '../database/connection';
8
- import type { LLMExample, ConnectionOptions } from '../types';
8
+ import type { LLMExample, ConnectionOptions, EngineCompat, ExampleOptions } from '../types';
9
+ import { exampleCondition } from './engine-filter';
9
10
 
10
11
  // =============================================================================
11
12
  // Database Row Types
@@ -20,8 +21,20 @@ interface LLMExampleRow {
20
21
  quality_score: number;
21
22
  usage_count: number;
22
23
  created_at: string;
24
+ /** The verified engine of the example's pattern (joined from code_examples). */
25
+ engine: string | null;
23
26
  }
24
27
 
28
+ /**
29
+ * Every example getter reads through this join: an example carries its
30
+ * pattern's verified engine, and one whose pattern no engine runs is never
31
+ * served (see engine-filter.ts). `le.` qualifies every column the queries use.
32
+ */
33
+ const EXAMPLES_FROM = `
34
+ SELECT le.*, ce.engine AS engine
35
+ FROM llm_examples le
36
+ JOIN code_examples ce ON ce.id = le.code_example_id`;
37
+
25
38
  // =============================================================================
26
39
  // Query Functions
27
40
  // =============================================================================
@@ -34,9 +47,10 @@ export async function getLLMExamples(
34
47
  prompt: string,
35
48
  language: string = 'en',
36
49
  limit: number = 5,
37
- options?: ConnectionOptions
50
+ options?: ExampleOptions
38
51
  ): Promise<LLMExample[]> {
39
52
  const db = getDatabase({ ...options, readonly: true });
53
+ const runs = exampleCondition('ce.engine', options?.engine);
40
54
 
41
55
  // Extract keywords from prompt
42
56
  const keywords = extractKeywords(prompt);
@@ -45,44 +59,30 @@ export async function getLLMExamples(
45
59
  // Return top-quality examples as fallback
46
60
  const rows = db
47
61
  .prepare(
48
- `
49
- SELECT * FROM llm_examples
50
- WHERE language = ?
51
- ORDER BY quality_score DESC, usage_count DESC
62
+ `${EXAMPLES_FROM}
63
+ WHERE le.language = ? AND ${runs.sql}
64
+ ORDER BY le.quality_score DESC, le.usage_count DESC
52
65
  LIMIT ?
53
66
  `
54
67
  )
55
- .all(language, limit) as LLMExampleRow[];
56
-
57
- // Track usage
58
- trackUsage(
59
- db,
60
- rows.map(r => r.id)
61
- );
68
+ .all(language, ...runs.params, limit) as LLMExampleRow[];
62
69
 
63
70
  return rows.map(mapRowToLLMExample);
64
71
  }
65
72
 
66
73
  // Build LIKE clauses for keyword matching
67
- const likeClauses = keywords.map(() => '(prompt LIKE ? OR completion LIKE ?)').join(' OR ');
74
+ const likeClauses = keywords.map(() => '(le.prompt LIKE ? OR le.completion LIKE ?)').join(' OR ');
68
75
  const params = keywords.flatMap(k => [`%${k}%`, `%${k}%`]);
69
76
 
70
77
  const rows = db
71
78
  .prepare(
72
- `
73
- SELECT * FROM llm_examples
74
- WHERE language = ? AND (${likeClauses})
75
- ORDER BY quality_score DESC
79
+ `${EXAMPLES_FROM}
80
+ WHERE le.language = ? AND ${runs.sql} AND (${likeClauses})
81
+ ORDER BY le.quality_score DESC
76
82
  LIMIT ?
77
83
  `
78
84
  )
79
- .all(language, ...params, limit) as LLMExampleRow[];
80
-
81
- // Track usage
82
- trackUsage(
83
- db,
84
- rows.map(r => r.id)
85
- );
85
+ .all(language, ...runs.params, ...params, limit) as LLMExampleRow[];
86
86
 
87
87
  return rows.map(mapRowToLLMExample);
88
88
  }
@@ -94,20 +94,20 @@ export async function getExamplesByCommand(
94
94
  command: string,
95
95
  language: string = 'en',
96
96
  limit: number = 5,
97
- options?: ConnectionOptions
97
+ options?: ExampleOptions
98
98
  ): Promise<LLMExample[]> {
99
99
  const db = getDatabase({ ...options, readonly: true });
100
+ const runs = exampleCondition('ce.engine', options?.engine);
100
101
 
101
102
  const rows = db
102
103
  .prepare(
103
- `
104
- SELECT * FROM llm_examples
105
- WHERE language = ? AND completion LIKE ?
106
- ORDER BY quality_score DESC
104
+ `${EXAMPLES_FROM}
105
+ WHERE le.language = ? AND ${runs.sql} AND le.completion LIKE ?
106
+ ORDER BY le.quality_score DESC
107
107
  LIMIT ?
108
108
  `
109
109
  )
110
- .all(language, `%${command}%`, limit) as LLMExampleRow[];
110
+ .all(language, ...runs.params, `%${command}%`, limit) as LLMExampleRow[];
111
111
 
112
112
  return rows.map(mapRowToLLMExample);
113
113
  }
@@ -119,44 +119,50 @@ export async function getHighQualityExamples(
119
119
  language: string = 'en',
120
120
  minQuality: number = 0.8,
121
121
  limit: number = 10,
122
- options?: ConnectionOptions
122
+ options?: ExampleOptions
123
123
  ): Promise<LLMExample[]> {
124
124
  const db = getDatabase({ ...options, readonly: true });
125
+ const runs = exampleCondition('ce.engine', options?.engine);
125
126
 
126
127
  const rows = db
127
128
  .prepare(
128
- `
129
- SELECT * FROM llm_examples
130
- WHERE language = ? AND quality_score >= ?
131
- ORDER BY quality_score DESC, usage_count DESC
129
+ `${EXAMPLES_FROM}
130
+ WHERE le.language = ? AND ${runs.sql} AND le.quality_score >= ?
131
+ ORDER BY le.quality_score DESC, le.usage_count DESC
132
132
  LIMIT ?
133
133
  `
134
134
  )
135
- .all(language, minQuality, limit) as LLMExampleRow[];
135
+ .all(language, ...runs.params, minQuality, limit) as LLMExampleRow[];
136
136
 
137
137
  return rows.map(mapRowToLLMExample);
138
138
  }
139
139
 
140
140
  /**
141
141
  * Get most used examples (popular).
142
+ *
143
+ * @deprecated Only an explicit trackExampleUsage() call counts a use, and nothing
144
+ * makes one, so `usage_count` is 0 on every shipped example and this ranks by
145
+ * quality alone: getHighQualityExamples with no floor. Reads used to count
146
+ * themselves through the read-only handle they open; the write failed and the
147
+ * error was swallowed.
142
148
  */
143
149
  export async function getMostUsedExamples(
144
150
  language: string = 'en',
145
151
  limit: number = 10,
146
- options?: ConnectionOptions
152
+ options?: ExampleOptions
147
153
  ): Promise<LLMExample[]> {
148
154
  const db = getDatabase({ ...options, readonly: true });
155
+ const runs = exampleCondition('ce.engine', options?.engine);
149
156
 
150
157
  const rows = db
151
158
  .prepare(
152
- `
153
- SELECT * FROM llm_examples
154
- WHERE language = ?
155
- ORDER BY usage_count DESC, quality_score DESC
159
+ `${EXAMPLES_FROM}
160
+ WHERE le.language = ? AND ${runs.sql}
161
+ ORDER BY le.usage_count DESC, le.quality_score DESC
156
162
  LIMIT ?
157
163
  `
158
164
  )
159
- .all(language, limit) as LLMExampleRow[];
165
+ .all(language, ...runs.params, limit) as LLMExampleRow[];
160
166
 
161
167
  return rows.map(mapRowToLLMExample);
162
168
  }
@@ -168,7 +174,7 @@ export async function buildFewShotContext(
168
174
  prompt: string,
169
175
  language: string = 'en',
170
176
  numExamples: number = 3,
171
- options?: ConnectionOptions
177
+ options?: ExampleOptions
172
178
  ): Promise<string> {
173
179
  const examples = await getLLMExamples(prompt, language, numExamples, options);
174
180
 
@@ -192,7 +198,8 @@ export async function buildFewShotContext(
192
198
  * Add a new LLM example.
193
199
  */
194
200
  export async function addLLMExample(
195
- example: Omit<LLMExample, 'id' | 'usageCount' | 'createdAt'>,
201
+ // `engine` belongs to the pattern (code_examples.engine), not the example.
202
+ example: Omit<LLMExample, 'id' | 'usageCount' | 'createdAt' | 'engine'>,
196
203
  options?: ConnectionOptions
197
204
  ): Promise<number> {
198
205
  const db = getDatabase(options);
@@ -240,6 +247,7 @@ export async function getLLMStats(options?: ConnectionOptions): Promise<{
240
247
  total: number;
241
248
  byLanguage: Record<string, number>;
242
249
  avgQuality: number;
250
+ /** @deprecated 0 unless something calls trackExampleUsage() (see getMostUsedExamples). */
243
251
  totalUsage: number;
244
252
  }> {
245
253
  const db = getDatabase({ ...options, readonly: true });
@@ -321,25 +329,6 @@ function extractKeywords(prompt: string): string[] {
321
329
  .filter(word => word.length > 2 && !stopWords.has(word));
322
330
  }
323
331
 
324
- /**
325
- * Track usage of examples.
326
- */
327
- function trackUsage(db: any, ids: number[]): void {
328
- if (ids.length === 0) return;
329
-
330
- try {
331
- const stmt = db.prepare(`
332
- UPDATE llm_examples SET usage_count = usage_count + 1 WHERE id = ?
333
- `);
334
-
335
- for (const id of ids) {
336
- stmt.run(id);
337
- }
338
- } catch {
339
- // Silently ignore tracking errors
340
- }
341
- }
342
-
343
332
  /**
344
333
  * Map database row to LLMExample type.
345
334
  */
@@ -353,5 +342,6 @@ function mapRowToLLMExample(row: LLMExampleRow): LLMExample {
353
342
  qualityScore: row.quality_score,
354
343
  usageCount: row.usage_count,
355
344
  createdAt: new Date(row.created_at),
345
+ engine: (row.engine as EngineCompat | null) ?? null,
356
346
  };
357
347
  }
@@ -5,6 +5,8 @@
5
5
  */
6
6
 
7
7
  import { getDatabase } from '../database/connection';
8
+ import { hyperscriptBodies } from '../sync/markup-attributes';
9
+ import { runsOn } from './engine-filter';
8
10
  import type {
9
11
  Pattern,
10
12
  SearchOptions,
@@ -24,6 +26,7 @@ interface CodeExampleRow {
24
26
  description: string | null;
25
27
  feature: string | null;
26
28
  engine: string | null;
29
+ translatable: number;
27
30
  source_url: string | null;
28
31
  created_at: string;
29
32
  }
@@ -43,7 +46,7 @@ export async function getPatternById(
43
46
  const row = db
44
47
  .prepare(
45
48
  `
46
- SELECT id, title, raw_code, description, feature, engine, created_at
49
+ SELECT id, title, raw_code, description, feature, engine, translatable, created_at
47
50
  FROM code_examples
48
51
  WHERE id = ?
49
52
  `
@@ -64,7 +67,7 @@ export async function getPatternsByCategory(
64
67
  const rows = db
65
68
  .prepare(
66
69
  `
67
- SELECT id, title, raw_code, description, feature, engine, created_at
70
+ SELECT id, title, raw_code, description, feature, engine, translatable, created_at
68
71
  FROM code_examples
69
72
  WHERE feature = ?
70
73
  ORDER BY title
@@ -87,7 +90,7 @@ export async function getPatternsByCommand(
87
90
  const rows = db
88
91
  .prepare(
89
92
  `
90
- SELECT id, title, raw_code, description, feature, engine, created_at
93
+ SELECT id, title, raw_code, description, feature, engine, translatable, created_at
91
94
  FROM code_examples
92
95
  WHERE raw_code LIKE ?
93
96
  ORDER BY title
@@ -102,29 +105,101 @@ export async function getPatternsByCommand(
102
105
  }
103
106
 
104
107
  /**
105
- * Search patterns by text query.
108
+ * The `engine` search option as SQL: omitted → no filter (a catalog lists
109
+ * every pattern, with its verdict); otherwise the patterns that run on it.
110
+ * (It used to be declared on SearchOptions and silently ignored.)
106
111
  */
107
- export async function searchPatterns(
108
- query: string,
109
- searchOptions: SearchOptions = {},
110
- connOptions?: ConnectionOptions
111
- ): Promise<Pattern[]> {
112
+ function engineFilter(searchOptions: SearchOptions): { sql: string; params: string[] } {
113
+ return searchOptions.engine === undefined
114
+ ? { sql: '1 = 1', params: [] }
115
+ : runsOn('engine', searchOptions.engine);
116
+ }
117
+
118
+ type Condition = { sql: string; params: string[] };
119
+
120
+ /**
121
+ * The `language` search option as a predicate: the pattern has a translation in
122
+ * that language that parses (`verified_parses`, measured at sync), or it carries
123
+ * no hyperscript at all — markup the runtime reads as it is, the same in every
124
+ * language (sse/ws wiring, a static component template). Such a row can never
125
+ * "verify", so a parse-only rule would drop it from every language.
126
+ */
127
+ function usableIn(
128
+ db: ReturnType<typeof getDatabase>,
129
+ language: string
130
+ ): (pattern: Pattern) => boolean {
131
+ const rows = db
132
+ .prepare(
133
+ 'SELECT code_example_id AS id FROM pattern_translations WHERE language = ? AND verified_parses = 1'
134
+ )
135
+ .all(language) as Array<{ id: string }>;
136
+ const verified = new Set(rows.map(row => row.id));
137
+ return pattern => verified.has(pattern.id) || hyperscriptBodies(pattern.rawCode).length === 0;
138
+ }
139
+
140
+ /**
141
+ * The patterns matching `match` and every search option, one page of them.
142
+ *
143
+ * `category`, `difficulty` and `language` used to be declared on SearchOptions
144
+ * and silently ignored. `difficulty` is inferred from the code and `language`
145
+ * reads the markup, so those two filter in JS — and paging runs after them, so a
146
+ * page is always a page of MATCHING patterns (the table is a few hundred rows).
147
+ */
148
+ function queryPatterns(
149
+ searchOptions: SearchOptions,
150
+ defaultLimit: number,
151
+ connOptions: ConnectionOptions | undefined,
152
+ match: Condition = { sql: '1 = 1', params: [] }
153
+ ): Pattern[] {
112
154
  const db = getDatabase({ ...connOptions, readonly: true });
113
- const { limit = 50, offset = 0 } = searchOptions;
155
+ const { limit = defaultLimit, offset = 0, category, difficulty, language } = searchOptions;
156
+ const runs = engineFilter(searchOptions);
157
+ const inCategory: Condition =
158
+ category === undefined
159
+ ? { sql: '1 = 1', params: [] }
160
+ : { sql: 'feature = ?', params: [category] };
114
161
 
115
162
  const rows = db
116
163
  .prepare(
117
164
  `
118
- SELECT id, title, raw_code, description, feature, engine, created_at
165
+ SELECT id, title, raw_code, description, feature, engine, translatable, created_at
119
166
  FROM code_examples
120
- WHERE title LIKE ? OR raw_code LIKE ? OR description LIKE ?
167
+ WHERE (${match.sql}) AND ${runs.sql} AND ${inCategory.sql}
121
168
  ORDER BY title
122
- LIMIT ? OFFSET ?
123
169
  `
124
170
  )
125
- .all(`%${query}%`, `%${query}%`, `%${query}%`, limit, offset) as CodeExampleRow[];
171
+ .all(...match.params, ...runs.params, ...inCategory.params) as CodeExampleRow[];
126
172
 
127
- return rows.map(mapRowToPattern);
173
+ const inLanguage = language === undefined ? null : usableIn(db, language);
174
+ return rows
175
+ .map(mapRowToPattern)
176
+ .filter(pattern => difficulty === undefined || pattern.difficulty === difficulty)
177
+ .filter(pattern => inLanguage === null || inLanguage(pattern))
178
+ .slice(offset, offset + limit);
179
+ }
180
+
181
+ /**
182
+ * Search patterns by text query: title, code or description — and, when a
183
+ * `language` is named, that language's translation, so a query in the language
184
+ * finds a pattern by its own code there.
185
+ */
186
+ export async function searchPatterns(
187
+ query: string,
188
+ searchOptions: SearchOptions = {},
189
+ connOptions?: ConnectionOptions
190
+ ): Promise<Pattern[]> {
191
+ const like = `%${query}%`;
192
+ const inTranslation: Condition =
193
+ searchOptions.language === undefined
194
+ ? { sql: '', params: [] }
195
+ : {
196
+ sql: ' OR id IN (SELECT code_example_id FROM pattern_translations WHERE language = ? AND hyperscript LIKE ?)',
197
+ params: [searchOptions.language, like],
198
+ };
199
+ return queryPatterns(searchOptions, 50, connOptions, {
200
+ sql: `title LIKE ? OR raw_code LIKE ? OR description LIKE ?${inTranslation.sql}`,
201
+ params: [like, like, like, ...inTranslation.params],
202
+ });
128
203
  }
129
204
 
130
205
  /**
@@ -134,21 +209,7 @@ export async function getAllPatterns(
134
209
  searchOptions: SearchOptions = {},
135
210
  connOptions?: ConnectionOptions
136
211
  ): Promise<Pattern[]> {
137
- const db = getDatabase({ ...connOptions, readonly: true });
138
- const { limit = 1000, offset = 0 } = searchOptions;
139
-
140
- const rows = db
141
- .prepare(
142
- `
143
- SELECT id, title, raw_code, description, feature, engine, created_at
144
- FROM code_examples
145
- ORDER BY title
146
- LIMIT ? OFFSET ?
147
- `
148
- )
149
- .all(limit, offset) as CodeExampleRow[];
150
-
151
- return rows.map(mapRowToPattern);
212
+ return queryPatterns(searchOptions, 1000, connOptions);
152
213
  }
153
214
 
154
215
  /**
@@ -253,6 +314,7 @@ function mapRowToPattern(row: CodeExampleRow): Pattern {
253
314
  tags: extractTags(row.raw_code),
254
315
  difficulty: inferDifficulty(row.raw_code),
255
316
  engine: (row.engine as EngineCompat) || null,
317
+ translatable: row.translatable !== 0,
256
318
  createdAt: new Date(row.created_at),
257
319
  };
258
320
  }
package/src/api/roles.ts CHANGED
@@ -35,6 +35,7 @@ interface CodeExampleRow {
35
35
  description: string | null;
36
36
  feature: string | null;
37
37
  engine: string | null;
38
+ translatable: number;
38
39
  created_at: string;
39
40
  }
40
41
 
@@ -75,7 +76,7 @@ export async function getPatternsByRole(
75
76
  const rows = db
76
77
  .prepare(
77
78
  `
78
- SELECT DISTINCT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.created_at
79
+ SELECT DISTINCT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.translatable, ce.created_at
79
80
  FROM code_examples ce
80
81
  INNER JOIN pattern_roles pr ON ce.id = pr.code_example_id
81
82
  WHERE pr.role = ?
@@ -105,7 +106,7 @@ export async function getPatternsByRoles(
105
106
  const rows = db
106
107
  .prepare(
107
108
  `
108
- SELECT DISTINCT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.created_at
109
+ SELECT DISTINCT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.translatable, ce.created_at
109
110
  FROM code_examples ce
110
111
  INNER JOIN pattern_roles pr ON ce.id = pr.code_example_id
111
112
  WHERE pr.role IN (${placeholders})
@@ -121,7 +122,7 @@ export async function getPatternsByRoles(
121
122
  const rows = db
122
123
  .prepare(
123
124
  `
124
- SELECT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.created_at
125
+ SELECT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.translatable, ce.created_at
125
126
  FROM code_examples ce
126
127
  WHERE (
127
128
  SELECT COUNT(DISTINCT pr.role)
@@ -149,7 +150,7 @@ export async function getPatternsByRoleValue(
149
150
  const rows = db
150
151
  .prepare(
151
152
  `
152
- SELECT DISTINCT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.created_at
153
+ SELECT DISTINCT ce.id, ce.title, ce.raw_code, ce.description, ce.feature, ce.engine, ce.translatable, ce.created_at
153
154
  FROM code_examples ce
154
155
  INNER JOIN pattern_roles pr ON ce.id = pr.code_example_id
155
156
  WHERE pr.role = ? AND pr.role_value LIKE ?
@@ -374,6 +375,7 @@ function mapRowToPattern(row: CodeExampleRow): Pattern {
374
375
  tags: extractTags(row.raw_code),
375
376
  difficulty: inferDifficulty(row.raw_code),
376
377
  engine: (row.engine as EngineCompat) || null,
378
+ translatable: row.translatable !== 0,
377
379
  createdAt: new Date(row.created_at),
378
380
  };
379
381
  }
@@ -148,8 +148,10 @@ export async function getHighConfidenceTranslations(
148
148
  }
149
149
 
150
150
  /**
151
- * Verify a translation parses correctly.
152
- * Note: This requires @lokascript/semantic to be available.
151
+ * Re-measure whether a translation parses, record the result, and return it.
152
+ * Uses the same definition sync-translations writes `verified_parses` with
153
+ * (src/sync/verify-parses.ts): every hyperscript body of the row parses in its
154
+ * language. Note: This requires @lokascript/semantic to be available.
153
155
  */
154
156
  export async function verifyTranslation(
155
157
  translation: Translation,
@@ -159,38 +161,43 @@ export async function verifyTranslation(
159
161
  let errorMessage: string | null = null;
160
162
  let confidence = translation.confidence;
161
163
 
162
- try {
163
- // Dynamic import to avoid bundling issues
164
- const { canParse, parse } = await import('@lokascript/semantic');
164
+ const db = getDatabase(options);
165
+ const example = db
166
+ .prepare('SELECT translatable FROM code_examples WHERE id = ?')
167
+ .get(translation.codeExampleId) as { translatable: number } | undefined;
165
168
 
166
- if (canParse(translation.hyperscript, translation.language)) {
167
- parse(translation.hyperscript, translation.language);
168
- parseSuccess = true;
169
- } else {
170
- errorMessage = 'canParse returned false';
171
- }
169
+ try {
170
+ // Dynamic import: the parser is only loaded by callers that verify.
171
+ const { verifyParses } = await import('../sync/verify-parses');
172
+ parseSuccess = verifyParses(
173
+ translation.hyperscript,
174
+ translation.language,
175
+ example?.translatable !== 0
176
+ );
177
+ if (!parseSuccess) errorMessage = 'the semantic parser rejected it (or it has no hyperscript)';
172
178
  } catch (e) {
173
179
  errorMessage = (e as Error).message;
174
180
  }
175
181
 
176
- // Update database
177
- const db = getDatabase(options);
178
- db.prepare(
182
+ // The flag and its test record land together. (The INSERT used to name
183
+ // columns pattern_tests does not have, so it threw AFTER the UPDATE had
184
+ // already flipped the flag.)
185
+ db.transaction(() => {
186
+ db.prepare(
187
+ `
188
+ UPDATE pattern_translations
189
+ SET verified_parses = ?, updated_at = datetime('now')
190
+ WHERE id = ?
179
191
  `
180
- UPDATE pattern_translations
181
- SET verified_parses = ?, updated_at = datetime('now')
182
- WHERE id = ?
183
- `
184
- ).run(parseSuccess ? 1 : 0, translation.id);
185
-
186
- // Record test result
187
- db.prepare(
192
+ ).run(parseSuccess ? 1 : 0, translation.id);
193
+ db.prepare(
194
+ `
195
+ INSERT INTO pattern_tests
196
+ (code_example_id, language, test_type, success, error_message, test_date)
197
+ VALUES (?, ?, 'parse', ?, ?, datetime('now'))
188
198
  `
189
- INSERT INTO pattern_tests
190
- (code_example_id, language, test_date, parse_success, error_message)
191
- VALUES (?, ?, datetime('now'), ?, ?)
192
- `
193
- ).run(translation.codeExampleId, translation.language, parseSuccess ? 1 : 0, errorMessage);
199
+ ).run(translation.codeExampleId, translation.language, parseSuccess ? 1 : 0, errorMessage);
200
+ })();
194
201
 
195
202
  return {
196
203
  translation,
@@ -36,6 +36,9 @@ let currentDbPath: string | null = null;
36
36
  let openFileId: string | null = null;
37
37
  let lastIdentityCheckAt = 0;
38
38
 
39
+ /** Whether the cached handle was opened read-only (it then cannot serve a write). */
40
+ let openReadonly = false;
41
+
39
42
  /** Re-stat at most this often, so a bulk query loop pays one syscall, not thousands. */
40
43
  const IDENTITY_TTL_MS = 1000;
41
44
 
@@ -63,12 +66,20 @@ export function getDefaultDbPath(): string {
63
66
  */
64
67
  export function getDatabase(options: ConnectionOptions = {}): InstanceType<typeof Database> {
65
68
  const dbPath = options.dbPath ?? DEFAULT_DB_PATH;
69
+ const readonly = options.readonly ?? false;
70
+
71
+ // A read-only handle cannot serve a write. The read APIs open with
72
+ // `readonly: true`, so without this a caller that read first and then called
73
+ // `verifyTranslation()` got the cached read-only handle and "attempt to write
74
+ // a readonly database". A writable handle serves reads fine, so only this
75
+ // direction reopens.
76
+ const canServe = !openReadonly || readonly;
66
77
 
67
78
  // Reuse the open handle only if it is reading the same PATH *and* the same FILE. The
68
79
  // second half matters because `populate` swaps the file underneath us: without it a
69
80
  // long-lived consumer serves pre-populate rows indefinitely, which looks like a parser
70
81
  // bug and is not one. Behind a TTL so a query loop doesn't stat per row.
71
- if (dbInstance !== null && currentDbPath === dbPath) {
82
+ if (dbInstance !== null && currentDbPath === dbPath && canServe) {
72
83
  const now = Date.now();
73
84
  if (now - lastIdentityCheckAt < IDENTITY_TTL_MS) {
74
85
  return dbInstance;
@@ -88,9 +99,8 @@ export function getDatabase(options: ConnectionOptions = {}): InstanceType<typeo
88
99
  dbInstance.close();
89
100
  }
90
101
 
91
- dbInstance = new Database(dbPath, {
92
- readonly: options.readonly ?? false,
93
- });
102
+ dbInstance = new Database(dbPath, { readonly });
103
+ openReadonly = readonly;
94
104
 
95
105
  // Enable foreign keys
96
106
  dbInstance.pragma('foreign_keys = ON');
@@ -111,6 +121,7 @@ export function closeDatabase(): void {
111
121
  currentDbPath = null;
112
122
  openFileId = null;
113
123
  lastIdentityCheckAt = 0;
124
+ openReadonly = false;
114
125
  }
115
126
  }
116
127
 
@@ -122,6 +133,7 @@ export function resetConnection(): void {
122
133
  currentDbPath = null;
123
134
  openFileId = null;
124
135
  lastIdentityCheckAt = 0;
136
+ openReadonly = false;
125
137
  }
126
138
 
127
139
  /**