@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.
- package/CHANGELOG.md +186 -1
- package/README.md +47 -36
- package/data/engine-verification.json +58 -65
- package/data/patterns.db +0 -0
- package/data/patterns.db.stamp +1 -0
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +198 -98
- package/dist/api/index.mjs +197 -100
- package/dist/{index-6GkHj5yJ.d.mts → index-CoDUfq2P.d.mts} +39 -3
- package/dist/{index-6GkHj5yJ.d.ts → index-CoDUfq2P.d.ts} +39 -3
- package/dist/index.d.mts +87 -13
- package/dist/index.d.ts +87 -13
- package/dist/index.js +261 -144
- package/dist/index.mjs +258 -146
- package/dist/{llm-B5nGz8V1.d.mts → llm-CCCSw-yp.d.ts} +21 -10
- package/dist/{llm-rEdJQScF.d.ts → llm-PxnriEZ_.d.mts} +21 -10
- package/dist/sync/index.d.mts +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +4 -1
- package/dist/sync/index.mjs +4 -1
- package/package.json +16 -16
- package/src/adapters/llm-adapter.ts +64 -42
- package/src/api/engine-filter.ts +37 -0
- package/src/api/llm.ts +54 -64
- package/src/api/patterns.ts +92 -30
- package/src/api/roles.ts +6 -4
- package/src/api/translations.ts +34 -27
- package/src/database/connection.ts +16 -4
- package/src/html-snippets.ts +39 -10
- package/src/index.ts +12 -2
- package/src/registry/patterns-provider.ts +3 -1
- package/src/sync/db-stamp.ts +7 -4
- package/src/sync/markup-attributes.ts +15 -0
- package/src/sync/verify-parses.ts +42 -0
- package/src/types/better-sqlite3.d.ts +2 -0
- package/src/types/index.ts +44 -2
- package/src/sync/span-mask.ts +0 -166
- 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?:
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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?:
|
|
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
|
-
|
|
105
|
-
|
|
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?:
|
|
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
|
-
|
|
130
|
-
|
|
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?:
|
|
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
|
-
|
|
154
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
}
|
package/src/api/patterns.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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
|
|
167
|
+
WHERE (${match.sql}) AND ${runs.sql} AND ${inCategory.sql}
|
|
121
168
|
ORDER BY title
|
|
122
|
-
LIMIT ? OFFSET ?
|
|
123
169
|
`
|
|
124
170
|
)
|
|
125
|
-
.all(
|
|
171
|
+
.all(...match.params, ...runs.params, ...inCategory.params) as CodeExampleRow[];
|
|
126
172
|
|
|
127
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/api/translations.ts
CHANGED
|
@@ -148,8 +148,10 @@ export async function getHighConfidenceTranslations(
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
/**
|