ReviewOS

also looking at this

stacks/wildloop

Migrate :text/x-text bindings to {⁠{ }} interpolation (stx 0.2.72)

#990
Merged glennmichael123 wants to merge chore/migrate-text-to-mustache into main
50 files +611 -371

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

app/Models/Activity.tsmodified+2-2
Changes to app/Models/Activity.ts
@@ -59,7 +59,7 @@ export default defineModel({
5959 order: 2,
6060 fillable: true,
6161 validation: {
62 rule: schema.number().required().min(0),
62 rule: schema.float().required().min(0),
6363 message: {
6464 required: 'Distance is required',
6565 },
@@ -113,7 +113,7 @@ export default defineModel({
113113 order: 5,
114114 fillable: true,
115115 validation: {
116 rule: schema.number().min(0),
116 rule: schema.float().min(0),
117117 },
118118 factory: (faker) => faker.number.int({ min: 100, max: 3000 }),
119119 },
app/Models/Challenge.tsmodified+1-1
Changes to app/Models/Challenge.ts
@@ -56,7 +56,7 @@ export default defineModel({
5656 order: 3,
5757 fillable: true,
5858 validation: {
59 rule: schema.number().min(0),
59 rule: schema.float().min(0),
6060 },
6161 factory: faker => faker.number.int({ min: 20000, max: 80000 }),
6262 },
app/Models/Territory.tsmodified+4-4
Changes to app/Models/Territory.ts
@@ -112,7 +112,7 @@ export default defineModel({
112112 order: 5,
113113 fillable: true,
114114 validation: {
115 rule: schema.number().required(),
115 rule: schema.float().required(),
116116 message: {
117117 required: 'Center latitude is required',
118118 },
@@ -124,7 +124,7 @@ export default defineModel({
124124 order: 6,
125125 fillable: true,
126126 validation: {
127 rule: schema.number().required(),
127 rule: schema.float().required(),
128128 message: {
129129 required: 'Center longitude is required',
130130 },
@@ -136,7 +136,7 @@ export default defineModel({
136136 order: 7,
137137 fillable: true,
138138 validation: {
139 rule: schema.number().required().min(0),
139 rule: schema.float().required().min(0),
140140 message: {
141141 required: 'Area size is required',
142142 },
@@ -149,7 +149,7 @@ export default defineModel({
149149 fillable: true,
150150 nullable: true,
151151 validation: {
152 rule: schema.number().min(0),
152 rule: schema.float().min(0),
153153 },
154154 factory: (faker) => faker.number.float({ min: 100, max: 5000, fractionDigits: 2 }),
155155 },
app/Models/TerritoryHistory.tsmodified+1-1
Changes to app/Models/TerritoryHistory.ts
@@ -75,7 +75,7 @@ export default defineModel({
7575 fillable: true,
7676 nullable: true,
7777 validation: {
78 rule: schema.number().min(0),
78 rule: schema.float().min(0),
7979 },
8080 factory: (faker) => faker.number.float({ min: 1000, max: 500000, fractionDigits: 2 }),
8181 },
app/Models/TerritoryStats.tsmodified+2-2
Changes to app/Models/TerritoryStats.ts
@@ -48,7 +48,7 @@ export default defineModel({
4848 order: 2,
4949 fillable: true,
5050 validation: {
51 rule: schema.number().min(0),
51 rule: schema.float().min(0),
5252 },
5353 factory: (faker) => faker.number.float({ min: 0, max: 5000000, fractionDigits: 2 }),
5454 },
@@ -108,7 +108,7 @@ export default defineModel({
108108 order: 8,
109109 fillable: true,
110110 validation: {
111 rule: schema.number().min(0),
111 rule: schema.float().min(0),
112112 },
113113 factory: (faker) => faker.number.float({ min: 0, max: 500000, fractionDigits: 2 }),
114114 },
app/Models/Trail.tsmodified+5-5
Changes to app/Models/Trail.ts
@@ -57,7 +57,7 @@ export default defineModel({
5757 order: 3,
5858 fillable: true,
5959 validation: {
60 rule: schema.number().required().min(0),
60 rule: schema.float().required().min(0),
6161 message: {
6262 required: 'Distance is required',
6363 min: 'Distance must be positive',
@@ -70,7 +70,7 @@ export default defineModel({
7070 order: 4,
7171 fillable: true,
7272 validation: {
73 rule: schema.number().required().min(0),
73 rule: schema.float().required().min(0),
7474 message: {
7575 required: 'Elevation is required',
7676 },
@@ -94,7 +94,7 @@ export default defineModel({
9494 order: 6,
9595 fillable: true,
9696 validation: {
97 rule: schema.number().min(0).max(5),
97 rule: schema.float().min(0).max(5),
9898 },
9999 factory: (faker) => faker.number.float({ min: 3.5, max: 5, fractionDigits: 1 }),
100100 },
@@ -143,7 +143,7 @@ export default defineModel({
143143 order: 11,
144144 fillable: true,
145145 validation: {
146 rule: schema.number(),
146 rule: schema.float(),
147147 },
148148 factory: (faker) => faker.location.latitude(),
149149 },
@@ -152,7 +152,7 @@ export default defineModel({
152152 order: 12,
153153 fillable: true,
154154 validation: {
155 rule: schema.number(),
155 rule: schema.float(),
156156 },
157157 factory: (faker) => faker.location.longitude(),
158158 },
database/migrations/0000000003-create-challenges-table.sqlmodified+1-1
Changes to database/migrations/0000000003-create-challenges-table.sql
@@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS "challenges" (
33 "challenger_id" INTEGER,
44 "challenged_id" INTEGER,
55 "territory_id" INTEGER REFERENCES "territories"("id"),
6 "area_at_stake" INTEGER,
6 "area_at_stake" REAL,
77 "status" TEXT CHECK ("status" IN ('pending', 'active', 'completed', 'declined')),
88 "winner_id" INTEGER,
99 "deadline" TEXT,
database/migrations/0000000005-create-territory_histories-table.sqlmodified+1-1
Changes to database/migrations/0000000005-create-territory_histories-table.sql
@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS "territory_histories" (
55 "activity_id" INTEGER REFERENCES "activities"("id"),
66 "previous_owner_id" INTEGER,
77 "event_type" TEXT CHECK ("event_type" IN ('claimed', 'conquered', 'split', 'defended', 'contested', 'expired')),
8 "area_at_event" INTEGER,
8 "area_at_event" REAL,
99 "previous_ownership_duration" INTEGER,
1010 "notes" TEXT,
1111 "new_territory_id" INTEGER,
database/migrations/0000000007-create-trails-table.sqlmodifiedrenamed throughout+5-5
Changes to database/migrations/0000000007-create-trails-table.sql
@@ -2,16 +2,16 @@ CREATE TABLE IF NOT EXISTS "trails" (
22 "id" INTEGER PRIMARY KEY AUTOINCREMENT,
33 "name" TEXT,
44 "location" TEXT,
5 "distance" INTEGER,
6 "elevation" INTEGER,
5 "distance" REAL,
6 "elevation" REAL,
77 "difficulty" TEXT CHECK ("difficulty" IN ('easy', 'moderate', 'hard')),
8 "rating" INTEGER,
8 "rating" REAL,
99 "review_count" INTEGER,
1010 "estimated_time" TEXT,
1111 "image" TEXT,
1212 "tags" TEXT,
13 "latitude" INTEGER,
14 "longitude" INTEGER,
13 "latitude" REAL,
14 "longitude" REAL,
1515 "description" TEXT,
1616 "geometry" TEXT,
1717 "created_at" TEXT not null default CURRENT_TIMESTAMP,
database/migrations/0000000008-create-activities-table.sqlmodifiedrenamed throughout+2-2
Changes to database/migrations/0000000008-create-activities-table.sql
@@ -3,11 +3,11 @@ CREATE TABLE IF NOT EXISTS "activities" (
33 "user_id" INTEGER REFERENCES "users"("id"),
44 "trail_id" INTEGER REFERENCES "trails"("id"),
55 "activity_type" TEXT CHECK ("activity_type" IN ('Trail Run', 'Hike', 'Walk', 'Bike')),
6 "distance" INTEGER,
6 "distance" REAL,
77 "duration" TEXT,
88 "moving_time" TEXT,
99 "pace" TEXT,
10 "elevation" INTEGER,
10 "elevation" REAL,
1111 "kudos_count" INTEGER,
1212 "notes" TEXT,
1313 "gpx_data" TEXT,
database/migrations/0000000011-create-territories-table.sqlmodifiedrenamed throughout+4-4
Changes to database/migrations/0000000011-create-territories-table.sql
@@ -6,10 +6,10 @@ CREATE TABLE IF NOT EXISTS "territories" (
66 "name" TEXT,
77 "polygon_data" TEXT,
88 "bounding_box" TEXT,
9 "center_lat" INTEGER,
10 "center_lng" INTEGER,
11 "area_size" INTEGER,
12 "perimeter" INTEGER,
9 "center_lat" REAL,
10 "center_lng" REAL,
11 "area_size" REAL,
12 "perimeter" REAL,
1313 "status" TEXT CHECK ("status" IN ('active', 'contested', 'expired')),
1414 "conquest_count" INTEGER,
1515 "claimed_at" TEXT,
database/migrations/0000000017-create-territory_stats-table.sqlmodifiedrenamed throughout+2-2
Changes to database/migrations/0000000017-create-territory_stats-table.sql
@@ -2,13 +2,13 @@ CREATE TABLE IF NOT EXISTS "territory_stats" (
22 "id" INTEGER PRIMARY KEY AUTOINCREMENT,
33 "user_id" INTEGER REFERENCES "users"("id"),
44 "total_territories_owned" INTEGER,
5 "total_area_owned" INTEGER,
5 "total_area_owned" REAL,
66 "territories_claimed" INTEGER,
77 "territories_conquered" INTEGER,
88 "territories_lost" INTEGER,
99 "territories_defended" INTEGER,
1010 "longest_ownership_days" INTEGER,
11 "largest_territory_area" INTEGER,
11 "largest_territory_area" REAL,
1212 "weekly_rank" INTEGER,
1313 "all_time_rank" INTEGER,
1414 "created_at" TEXT not null default CURRENT_TIMESTAMP,
scripts/verify-column-affinity.tsadded+75-0
Changes to scripts/verify-column-affinity.ts
@@ -0,0 +1,75 @@
1/**
2 * Verification harness for numeric column affinity (#974): float-holding
3 * columns (lat/lng/area/perimeter/rating/distance/elevation) must have REAL
4 * affinity; ids and counts must stay INTEGER; and fractional values must
5 * round-trip through a fresh migrate without truncation.
6 *
7 * Run after a fresh migrate + seed:
8 * bun scripts/seed-game-world.ts && bun scripts/verify-column-affinity.ts
9 */
10/* eslint-disable ts/no-top-level-await */
11import process from 'node:process'
12
13process.on('unhandledRejection', (err) => { console.error('UNHANDLED', err); process.exit(1) })
14
15import { Database } from 'bun:sqlite'
16
17let failures = 0
18function check(label: string, ok: boolean, detail?: string) {
19 console.log(`${ok ? '✅' : '❌'} ${label}${detail ? ` — ${detail}` : ''}`)
20 if (!ok) failures++
21}
22
23const db = new Database('database/stacks.sqlite')
24function colType(table: string, col: string): string {
25 const info = db.query(`PRAGMA table_info(${table})`).all() as any[]
26 return info.find(c => c.name === col)?.type ?? '(missing)'
27}
28
29// --- REAL float columns ----------------------------------------------------------
30
31const REAL_COLS: Array<[string, string]> = [
32 ['trails', 'latitude'], ['trails', 'longitude'], ['trails', 'rating'], ['trails', 'distance'], ['trails', 'elevation'],
33 ['territories', 'center_lat'], ['territories', 'center_lng'], ['territories', 'area_size'], ['territories', 'perimeter'],
34 ['activities', 'distance'], ['activities', 'elevation'],
35 ['territory_stats', 'total_area_owned'], ['territory_stats', 'largest_territory_area'],
36 ['challenges', 'area_at_stake'],
37 ['territory_histories', 'area_at_event'],
38]
39for (const [t, c] of REAL_COLS)
40 check(`${t}.${c} is REAL`, colType(t, c) === 'REAL', colType(t, c))
41
42// --- INTEGER id/count columns (must NOT have become REAL) -------------------------
43
44const INT_COLS: Array<[string, string]> = [
45 ['territories', 'user_id'], ['territories', 'activity_id'], ['territories', 'conquest_count'],
46 ['activities', 'user_id'], ['activities', 'kudos_count'],
47 ['trails', 'review_count'],
48 ['territory_stats', 'total_territories_owned'], ['territory_stats', 'territories_conquered'], ['territory_stats', 'weekly_rank'],
49 ['challenges', 'challenger_id'], ['challenges', 'winner_id'],
50 ['reviews_alias_skip', 'noop'],
51]
52for (const [t, c] of INT_COLS) {
53 if (t === 'reviews_alias_skip')
54 continue
55 check(`${t}.${c} stays INTEGER`, colType(t, c) === 'INTEGER', colType(t, c))
56}
57
58// --- fractional values survive a fresh migrate -----------------------------------
59
60const terr = db.query('SELECT area_size, center_lat, center_lng, perimeter FROM territories WHERE area_size IS NOT NULL LIMIT 1').get() as any
61check('seeded territory floats are fractional (not truncated)', !!terr
62 && terr.area_size % 1 !== 0 && terr.center_lat % 1 !== 0, JSON.stringify(terr))
63
64// Insert a deliberately fractional row and read it back exactly.
65const probe = 123456.789012
66db.query('INSERT INTO territories (user_id, name, status, area_size, center_lat, center_lng, perimeter) VALUES (1, \'affinity-probe\', \'active\', ?, ?, ?, ?)')
67 .run(probe, 12.3456789, -98.7654321, 42.42)
68const back = db.query('SELECT area_size, center_lat FROM territories WHERE name = \'affinity-probe\'').get() as any
69check('fractional value round-trips exactly through REAL', back?.area_size === probe && back?.center_lat === 12.3456789, JSON.stringify(back))
70check('stored as REAL storage class (not text/int)',
71 (db.query('SELECT typeof(area_size) t FROM territories WHERE name = \'affinity-probe\'').get() as any)?.t === 'real')
72db.query('DELETE FROM territories WHERE name = \'affinity-probe\'').run()
73
74console.log(failures === 0 ? '\n✅ all column affinity checks passed' : `\n❌ ${failures} check(s) failed`)
75process.exit(failures === 0 ? 0 : 1)
storage/framework/core/database/src/drivers/helpers.tsmodified+1-1
Changes to storage/framework/core/database/src/drivers/helpers.ts
@@ -349,7 +349,7 @@ export function mapFieldTypeToColumnType(validator: ValidationType, driver = 'my
349349 return `'timestamp'`
350350
351351 if (isFloatValidator(validator))
352 return `'float4'`
352 return driver === 'sqlite' ? `'real'` : `'float4'` // SQLite: REAL affinity for floats (#974)
353353
354354 // Handle array/object types
355355 if (['array', 'object'].includes(validator.name))