@everystack/mcp 0.3.2 → 0.3.3

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 (85) hide show
  1. package/dist/gates/detectors/embedded-data-bundle.d.ts +13 -0
  2. package/dist/gates/detectors/embedded-data-bundle.d.ts.map +1 -0
  3. package/dist/gates/detectors/embedded-data-bundle.js +56 -0
  4. package/dist/gates/detectors/hand-written-migration.d.ts +24 -0
  5. package/dist/gates/detectors/hand-written-migration.d.ts.map +1 -0
  6. package/dist/gates/detectors/hand-written-migration.js +54 -0
  7. package/dist/gates/detectors/secret-in-public-env.d.ts +12 -0
  8. package/dist/gates/detectors/secret-in-public-env.d.ts.map +1 -0
  9. package/dist/gates/detectors/secret-in-public-env.js +38 -0
  10. package/dist/gates/engine.d.ts +28 -0
  11. package/dist/gates/engine.d.ts.map +1 -0
  12. package/dist/gates/engine.js +74 -0
  13. package/dist/gates/registry.d.ts +14 -0
  14. package/dist/gates/registry.d.ts.map +1 -0
  15. package/dist/gates/registry.js +21 -0
  16. package/dist/gates/telemetry.d.ts +47 -0
  17. package/dist/gates/telemetry.d.ts.map +1 -0
  18. package/dist/gates/telemetry.js +121 -0
  19. package/dist/gates/types.d.ts +67 -0
  20. package/dist/gates/types.d.ts.map +1 -0
  21. package/dist/gates/types.js +14 -0
  22. package/dist/governance/cli.d.ts +57 -0
  23. package/dist/governance/cli.d.ts.map +1 -0
  24. package/dist/governance/cli.js +169 -0
  25. package/dist/governance/grounding.d.ts +78 -0
  26. package/dist/governance/grounding.d.ts.map +1 -0
  27. package/dist/governance/grounding.js +299 -0
  28. package/dist/index.cjs +11 -9
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +94 -0
  32. package/dist/project-claude-md.md +30 -16
  33. package/dist/prompts/add-feature.d.ts +3 -0
  34. package/dist/prompts/add-feature.d.ts.map +1 -0
  35. package/dist/prompts/add-feature.js +154 -0
  36. package/dist/prompts/claude-md.d.ts +12 -0
  37. package/dist/prompts/claude-md.d.ts.map +1 -0
  38. package/dist/prompts/claude-md.js +87 -0
  39. package/dist/prompts/debug.d.ts +3 -0
  40. package/dist/prompts/debug.d.ts.map +1 -0
  41. package/dist/prompts/debug.js +129 -0
  42. package/dist/prompts/deploy.d.ts +3 -0
  43. package/dist/prompts/deploy.d.ts.map +1 -0
  44. package/dist/prompts/deploy.js +118 -0
  45. package/dist/prompts/design-schema.d.ts +3 -0
  46. package/dist/prompts/design-schema.d.ts.map +1 -0
  47. package/dist/prompts/design-schema.js +97 -0
  48. package/dist/prompts/governance-setup.d.ts +9 -0
  49. package/dist/prompts/governance-setup.d.ts.map +1 -0
  50. package/dist/prompts/governance-setup.js +76 -0
  51. package/dist/prompts/index.d.ts +3 -0
  52. package/dist/prompts/index.d.ts.map +1 -0
  53. package/dist/prompts/index.js +20 -0
  54. package/dist/prompts/new-app.d.ts +3 -0
  55. package/dist/prompts/new-app.d.ts.map +1 -0
  56. package/dist/prompts/new-app.js +203 -0
  57. package/dist/prompts/runbook.d.ts +12 -0
  58. package/dist/prompts/runbook.d.ts.map +1 -0
  59. package/dist/prompts/runbook.js +70 -0
  60. package/dist/prompts/secure.d.ts +3 -0
  61. package/dist/prompts/secure.d.ts.map +1 -0
  62. package/dist/prompts/secure.js +219 -0
  63. package/dist/resources/index.d.ts +12 -0
  64. package/dist/resources/index.d.ts.map +1 -0
  65. package/dist/resources/index.js +182 -0
  66. package/dist/tools/check-environment.d.ts +25 -0
  67. package/dist/tools/check-environment.d.ts.map +1 -0
  68. package/dist/tools/check-environment.js +281 -0
  69. package/dist/tools/index.d.ts +3 -0
  70. package/dist/tools/index.d.ts.map +1 -0
  71. package/dist/tools/index.js +18 -0
  72. package/dist/tools/project-status.d.ts +28 -0
  73. package/dist/tools/project-status.d.ts.map +1 -0
  74. package/dist/tools/project-status.js +138 -0
  75. package/dist/tools/project-validate.d.ts +19 -0
  76. package/dist/tools/project-validate.d.ts.map +1 -0
  77. package/dist/tools/project-validate.js +323 -0
  78. package/dist/tools/schema-analyze.d.ts +46 -0
  79. package/dist/tools/schema-analyze.d.ts.map +1 -0
  80. package/dist/tools/schema-analyze.js +336 -0
  81. package/package.json +3 -3
  82. package/src/gates/detectors/hand-written-migration.ts +22 -13
  83. package/src/index.ts +1 -1
  84. package/src/prompts/claude-md.ts +4 -2
  85. package/src/resources/project-claude-md.md +30 -16
@@ -0,0 +1,336 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Extract a brace-balanced block starting from an opening brace.
5
+ */
6
+ function extractBraceBlock(source, openIndex) {
7
+ let depth = 0;
8
+ for (let i = openIndex; i < source.length; i++) {
9
+ if (source[i] === '{')
10
+ depth++;
11
+ if (source[i] === '}')
12
+ depth--;
13
+ if (depth === 0)
14
+ return source.slice(openIndex + 1, i);
15
+ }
16
+ return source.slice(openIndex + 1);
17
+ }
18
+ /**
19
+ * Parse pgTable declarations from a TypeScript source file.
20
+ * Uses regex + brace-depth extraction to handle nested objects (e.g., { withTimezone: true }).
21
+ */
22
+ function parseTables(source, file) {
23
+ const tables = [];
24
+ // Find each pgTable declaration
25
+ const headerRegex = /export\s+const\s+(\w+)\s*=\s*pgTable\(\s*['"]([^'"]+)['"]\s*,\s*\{/g;
26
+ let match;
27
+ while ((match = headerRegex.exec(source)) !== null) {
28
+ const exportName = match[1];
29
+ const dbName = match[2];
30
+ // The opening brace position is at the end of the match minus 1
31
+ const openBrace = match.index + match[0].length - 1;
32
+ const columnsBlock = extractBraceBlock(source, openBrace);
33
+ const columns = parseColumns(columnsBlock);
34
+ tables.push({ name: dbName, exportName, columns, file });
35
+ }
36
+ return tables;
37
+ }
38
+ function parseColumns(block) {
39
+ const columns = [];
40
+ // Match: columnName: type('db_name', ...optional args...)...chain...
41
+ // The type call may have extra args after the string (e.g., timestamp('x', { withTimezone: true }))
42
+ const colRegex = /(\w+)\s*:\s*(\w+)\(\s*['"]([^'"]+)['"][^)]*\)([\s\S]*?)(?=\n\s*\w+\s*:|$)/g;
43
+ let match;
44
+ while ((match = colRegex.exec(block)) !== null) {
45
+ const name = match[1];
46
+ const type = match[2];
47
+ const dbName = match[3];
48
+ const rest = match[4];
49
+ const modifiers = [];
50
+ if (rest.includes('.primaryKey()'))
51
+ modifiers.push('primaryKey');
52
+ if (rest.includes('.notNull()'))
53
+ modifiers.push('notNull');
54
+ if (rest.includes('.unique()'))
55
+ modifiers.push('unique');
56
+ if (rest.includes('.defaultNow()'))
57
+ modifiers.push('defaultNow');
58
+ if (rest.includes('.defaultRandom()'))
59
+ modifiers.push('defaultRandom');
60
+ if (rest.includes('.default('))
61
+ modifiers.push('default');
62
+ if (rest.includes('.references('))
63
+ modifiers.push('references');
64
+ columns.push({ name, dbName, type, modifiers });
65
+ }
66
+ return columns;
67
+ }
68
+ /**
69
+ * Parse relations() declarations from source.
70
+ */
71
+ function parseRelations(source) {
72
+ const relations = [];
73
+ // Match: export const nameRelations = relations(tableName, ({ one, many }) => ({
74
+ const relBlockRegex = /export\s+const\s+\w+\s*=\s*relations\(\s*(\w+)\s*,\s*\(\s*\{[^}]*\}\s*\)\s*=>\s*\(\{([\s\S]*?)\}\)\s*\)/g;
75
+ let blockMatch;
76
+ while ((blockMatch = relBlockRegex.exec(source)) !== null) {
77
+ const table = blockMatch[1];
78
+ const body = blockMatch[2];
79
+ // Match individual relations: name: one(target, { fields: [...], references: [...] })
80
+ const relRegex = /(\w+)\s*:\s*(one|many)\(\s*(\w+)(?:\s*,\s*\{([^}]*)\})?\s*\)/g;
81
+ let relMatch;
82
+ while ((relMatch = relRegex.exec(body)) !== null) {
83
+ const name = relMatch[1];
84
+ const type = relMatch[2];
85
+ const target = relMatch[3];
86
+ const opts = relMatch[4] || '';
87
+ const fields = [];
88
+ const references = [];
89
+ // Parse fields: [table.column]
90
+ const fieldsMatch = opts.match(/fields:\s*\[([^\]]+)\]/);
91
+ if (fieldsMatch) {
92
+ fields.push(...fieldsMatch[1].split(',').map((f) => f.trim().replace(/.*\./, '')));
93
+ }
94
+ const refsMatch = opts.match(/references:\s*\[([^\]]+)\]/);
95
+ if (refsMatch) {
96
+ references.push(...refsMatch[1].split(',').map((r) => r.trim().replace(/.*\./, '')));
97
+ }
98
+ const nameMatch = opts.match(/relationName:\s*['"]([^'"]+)['"]/);
99
+ relations.push({
100
+ name,
101
+ table: target,
102
+ type,
103
+ fields,
104
+ references,
105
+ ...(nameMatch ? { relationName: nameMatch[1] } : {}),
106
+ });
107
+ }
108
+ }
109
+ return relations;
110
+ }
111
+ /**
112
+ * Parse handler configuration from the server handler/plugin file.
113
+ */
114
+ function parseHandlerConfig(source) {
115
+ const config = {};
116
+ // exposedTables
117
+ const exposedMatch = source.match(/exposedTables:\s*\[([\s\S]*?)\]/);
118
+ if (exposedMatch) {
119
+ config.exposedTables = exposedMatch[1]
120
+ .match(/['"]([^'"]+)['"]/g)
121
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
122
+ }
123
+ // hiddenColumns
124
+ const hiddenMatch = source.match(/hiddenColumns:\s*\{([\s\S]*?)\}/);
125
+ if (hiddenMatch) {
126
+ config.hiddenColumns = {};
127
+ const entries = hiddenMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
128
+ for (const entry of entries) {
129
+ config.hiddenColumns[entry[1]] = entry[2]
130
+ .match(/['"]([^'"]+)['"]/g)
131
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
132
+ }
133
+ }
134
+ // protectedFields
135
+ const protectedMatch = source.match(/protectedFields:\s*\{([\s\S]*?)\}/);
136
+ if (protectedMatch) {
137
+ config.protectedFields = {};
138
+ const entries = protectedMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
139
+ for (const entry of entries) {
140
+ config.protectedFields[entry[1]] = entry[2]
141
+ .match(/['"]([^'"]+)['"]/g)
142
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
143
+ }
144
+ }
145
+ // rowOwnership — scan for entries after the rowOwnership key
146
+ if (/rowOwnership\s*:/.test(source)) {
147
+ config.rowOwnership = {};
148
+ // Extract everything from rowOwnership: { to the matching closing }
149
+ const startIdx = source.indexOf('rowOwnership');
150
+ if (startIdx !== -1) {
151
+ // Find the opening brace
152
+ const braceStart = source.indexOf('{', startIdx);
153
+ if (braceStart !== -1) {
154
+ // Find the matching closing brace (handle one level of nesting)
155
+ let depth = 0;
156
+ let braceEnd = braceStart;
157
+ for (let i = braceStart; i < source.length; i++) {
158
+ if (source[i] === '{')
159
+ depth++;
160
+ if (source[i] === '}')
161
+ depth--;
162
+ if (depth === 0) {
163
+ braceEnd = i;
164
+ break;
165
+ }
166
+ }
167
+ const block = source.slice(braceStart + 1, braceEnd);
168
+ const entryRegex = /(\w+):\s*\{\s*column:\s*['"]([^'"]+)['"]\s*,\s*userField:\s*['"]([^'"]+)['"]\s*\}/g;
169
+ const entries = block.matchAll(entryRegex);
170
+ for (const entry of entries) {
171
+ config.rowOwnership[entry[1]] = { column: entry[2], userField: entry[3] };
172
+ }
173
+ }
174
+ }
175
+ }
176
+ // softDelete
177
+ const softDeleteMatch = source.match(/softDelete:\s*\{[\s\S]*?tables:\s*\[([\s\S]*?)\]/);
178
+ if (softDeleteMatch) {
179
+ config.softDelete = {
180
+ tables: softDeleteMatch[1]
181
+ .match(/['"]([^'"]+)['"]/g)
182
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [],
183
+ };
184
+ }
185
+ // pgSettings
186
+ config.pgSettings = /pgSettings\s*:/.test(source);
187
+ // publicRoutes
188
+ const publicRoutesMatch = source.match(/publicRoutes:\s*\[([\s\S]*?)\]/);
189
+ if (publicRoutesMatch) {
190
+ config.publicRoutes = publicRoutesMatch[1]
191
+ .match(/['"]([^'"]+)['"]/g)
192
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
193
+ }
194
+ // publicRpc
195
+ const publicRpcMatch = source.match(/publicRpc:\s*\[([\s\S]*?)\]/);
196
+ if (publicRpcMatch) {
197
+ config.publicRpc = publicRpcMatch[1]
198
+ .match(/['"]([^'"]+)['"]/g)
199
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
200
+ }
201
+ return config;
202
+ }
203
+ export function analyzeSchema(projectPath) {
204
+ const issues = [];
205
+ const suggestions = [];
206
+ const allTables = [];
207
+ const allRelations = [];
208
+ // Find schema files
209
+ const dbDir = join(projectPath, 'db');
210
+ const schemaFiles = [];
211
+ if (existsSync(dbDir)) {
212
+ try {
213
+ const files = readdirSync(dbDir);
214
+ for (const f of files) {
215
+ if (f.endsWith('.ts') && !f.endsWith('.test.ts') && !f.startsWith('seed')) {
216
+ schemaFiles.push(join(dbDir, f));
217
+ }
218
+ }
219
+ }
220
+ catch {
221
+ // ignore read errors
222
+ }
223
+ }
224
+ if (schemaFiles.length === 0) {
225
+ return {
226
+ tables: [],
227
+ relations: [],
228
+ handlerConfig: null,
229
+ issues: ['No schema files found in db/ directory'],
230
+ suggestions: ['Create db/schema.ts with your Drizzle pgTable definitions'],
231
+ };
232
+ }
233
+ // Parse each schema file
234
+ for (const file of schemaFiles) {
235
+ try {
236
+ const source = readFileSync(file, 'utf-8');
237
+ const relativePath = file.replace(projectPath + '/', '');
238
+ allTables.push(...parseTables(source, relativePath));
239
+ allRelations.push(...parseRelations(source));
240
+ }
241
+ catch {
242
+ issues.push(`Failed to read ${file}`);
243
+ }
244
+ }
245
+ // Find and parse handler config
246
+ let handlerConfig = null;
247
+ const handlerCandidates = [
248
+ 'server/plugins/api.ts',
249
+ 'server/api.ts',
250
+ 'server/handler.ts',
251
+ 'server/index.ts',
252
+ ];
253
+ for (const candidate of handlerCandidates) {
254
+ const fullPath = join(projectPath, candidate);
255
+ if (existsSync(fullPath)) {
256
+ try {
257
+ const source = readFileSync(fullPath, 'utf-8');
258
+ if (source.includes('createHandler')) {
259
+ handlerConfig = parseHandlerConfig(source);
260
+ break;
261
+ }
262
+ }
263
+ catch {
264
+ // continue to next candidate
265
+ }
266
+ }
267
+ }
268
+ // Cross-reference analysis
269
+ if (handlerConfig?.exposedTables) {
270
+ const tableNames = allTables.map((t) => t.name);
271
+ for (const exposed of handlerConfig.exposedTables) {
272
+ // Check both db name and export name
273
+ const found = allTables.some((t) => t.name === exposed || t.exportName === exposed);
274
+ if (!found) {
275
+ issues.push(`exposedTables includes '${exposed}' but no matching pgTable definition found in schema`);
276
+ }
277
+ }
278
+ // Tables defined but not exposed
279
+ for (const table of allTables) {
280
+ if (!handlerConfig.exposedTables.includes(table.name) &&
281
+ !handlerConfig.exposedTables.includes(table.exportName)) {
282
+ suggestions.push(`Table '${table.name}' is defined in schema but not in exposedTables — intentional?`);
283
+ }
284
+ }
285
+ }
286
+ // Check for soft delete columns
287
+ if (handlerConfig?.softDelete?.tables) {
288
+ for (const tableName of handlerConfig.softDelete.tables) {
289
+ const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
290
+ if (table) {
291
+ const hasDeletedAt = table.columns.some((c) => c.name === 'deletedAt' || c.dbName === 'deleted_at');
292
+ if (!hasDeletedAt) {
293
+ issues.push(`Table '${tableName}' is in softDelete.tables but has no deletedAt column`);
294
+ }
295
+ }
296
+ }
297
+ }
298
+ // Check for rowOwnership references
299
+ if (handlerConfig?.rowOwnership) {
300
+ for (const [tableName, ownership] of Object.entries(handlerConfig.rowOwnership)) {
301
+ const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
302
+ if (table) {
303
+ const hasCol = table.columns.some((c) => c.name === ownership.column || c.dbName === ownership.column);
304
+ if (!hasCol) {
305
+ issues.push(`rowOwnership for '${tableName}' references column '${ownership.column}' which doesn't exist`);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ // pgSettings check
311
+ if (!handlerConfig?.pgSettings && allTables.length > 0) {
312
+ suggestions.push('No pgSettings configured — RLS policies cannot read JWT claims without it');
313
+ }
314
+ // Tables without primary keys
315
+ for (const table of allTables) {
316
+ const hasPk = table.columns.some((c) => c.modifiers.includes('primaryKey'));
317
+ if (!hasPk) {
318
+ // Could be a composite primary key (defined in table config, not column)
319
+ suggestions.push(`Table '${table.name}' — no single-column primaryKey() detected (may use composite key)`);
320
+ }
321
+ }
322
+ // Tables with timestamps
323
+ for (const table of allTables) {
324
+ const hasCreatedAt = table.columns.some((c) => c.dbName === 'created_at');
325
+ if (!hasCreatedAt) {
326
+ suggestions.push(`Table '${table.name}' has no created_at column — recommended for audit`);
327
+ }
328
+ }
329
+ return {
330
+ tables: allTables,
331
+ relations: allRelations,
332
+ handlerConfig,
333
+ issues,
334
+ suggestions,
335
+ };
336
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Governance layer that governs how any agent builds everystack — grounding, cheat gates, and Model-aware tooling over MCP",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -40,8 +40,8 @@
40
40
  "tsx": "4.21.0",
41
41
  "typescript": "5.9.3",
42
42
  "zod": "3.25.67",
43
- "@everystack/cli": "0.3.14",
44
- "@everystack/model": "0.3.4"
43
+ "@everystack/cli": "0.3.27",
44
+ "@everystack/model": "0.3.6"
45
45
  },
46
46
  "scripts": {
47
47
  "test": "jest",
@@ -1,14 +1,23 @@
1
1
  /**
2
2
  * Cheat: hand-writing a migration or editing the generated schema.
3
3
  *
4
- * In v3 the schema and its migrations are *generated* from the Models
5
- * (`db:generate`). A hand-written `.sql` migration or a hand-edit to the generated
6
- * `db/schema.ts` drifts the database from the Models — the migration the CLI would
7
- * write next is no longer a clean no-op. Pure path check; no IO.
4
+ * Under the migration-authority workflow nobody authors migrations at all:
5
+ * the Models declare the state, `db:sync` moves a dev database to it,
6
+ * protected stages take fingerprint-verified plans (`db:plan` `db:apply`),
7
+ * and CI (`db:check`) refuses generated artifacts that don't match
8
+ * regeneration. A hand-written `.sql` migration or a hand-edit to
9
+ * `db/schema.generated.ts` drifts the database from the Models. Pure path
10
+ * check; no IO.
8
11
  *
9
- * Note the layer split: SQL under `db/sql/` is the DERIVED layer (functions,
10
- * views, matviews) and is legitimately authored — it deploys via `db:reconcile`,
11
- * not through migrations, so this gate deliberately does not match it.
12
+ * The homes that ARE legitimately authored this gate deliberately matches
13
+ * none of them:
14
+ * - `db/models/` the Models themselves (the source of truth).
15
+ * - `db/sql/` — the DERIVED layer (functions/views/matviews), deployed
16
+ * via `db:reconcile`/`db:sync`.
17
+ * - `db/backfills/` — one-shot DATA jobs, run via `db:backfill` (content-
18
+ * addressed, own record, never a schema side effect).
19
+ * - `db/schema.ts` — the thin hand-maintained barrel that re-exports the
20
+ * generated schema plus package-owned schemas.
12
21
  */
13
22
 
14
23
  import { relative } from 'path';
@@ -16,8 +25,8 @@ import type { CheatGate, ToolCallContext } from '../types.js';
16
25
 
17
26
  /** A `.sql` file under a migrations/drizzle directory — these are generated, never authored. */
18
27
  const MIGRATION_SQL = /(?:^|\/)(?:drizzle|migrations)\/[^/]+\.sql$/i;
19
- /** The generated Drizzle schema mirror. */
20
- const GENERATED_SCHEMA = /(?:^|\/)(?:db|drizzle)\/schema\.ts$/;
28
+ /** The generated drizzle schema artifact (any location — monorepos relocate it via --schema-out). */
29
+ const GENERATED_SCHEMA = /schema\.generated\.tsx?$/;
21
30
 
22
31
  function rel(ctx: ToolCallContext, p: string): string {
23
32
  try {
@@ -32,15 +41,15 @@ export const handWrittenMigration: CheatGate = {
32
41
  tier: 'framework',
33
42
  severity: 'deny',
34
43
  guide:
35
- 'Schema changes have two homes, neither is a hand-written migration: tables/constraints/authz are generated from your Models (edit the Model, run db:generate); functions, views, and matviews are authored in db/sql/ and DEPLOYED with db:reconcile no migration file either way.',
36
- conform: 'everystack db:generate (tables) · everystack db:reconcile (functions/views/matviews in db/sql)',
37
- verify: 'everystack db:generate produces no diff AND everystack db:reconcile --check exits 0',
44
+ 'Nobody authors migrations. Schema changes have three homes: tables/constraints/authz are declared in db/models/ (edit the Model, then `everystack db:sync` on dev — protected stages take `db:plan` → `db:apply`); functions/views/matviews are authored in db/sql/ (deployed by db:reconcile/db:sync); one-shot DATA moves are authored in db/backfills/*.sql (run via db:backfill, never as a schema side effect). No migration file, ever — and generated artifacts (schema.generated.ts) are never edited.',
45
+ conform: 'edit db/models/ then `everystack db:sync` (dev) or `db:plan` → `db:apply` (protected) · db/sql/ for derived objects · db/backfills/ for data moves',
46
+ verify: 'everystack db:check passes (declared state composes; generated artifacts match regeneration) AND everystack db:fingerprint reports MATCH',
38
47
  detect(ctx: ToolCallContext): string | null {
39
48
  if (ctx.tool !== 'Write' && ctx.tool !== 'Edit') return null;
40
49
  const p = ctx.filePath;
41
50
  if (!p) return null;
42
51
  if (MIGRATION_SQL.test(p)) return `${rel(ctx, p)} is a SQL migration being written by hand`;
43
- if (GENERATED_SCHEMA.test(p)) return `${rel(ctx, p)} is generated from your Models`;
52
+ if (GENERATED_SCHEMA.test(p)) return `${rel(ctx, p)} is a generated artifact (compiled from your Models — db:check refuses hand edits)`;
44
53
  return null;
45
54
  },
46
55
  };
package/src/index.ts CHANGED
@@ -71,7 +71,7 @@ const INSTRUCTIONS = [
71
71
  '1. Read everystack://core for architecture and conventions.',
72
72
  '2. Read everystack://security before any deployment or auth guidance.',
73
73
  '3. Load detail resources on demand when the user asks about specific features.',
74
- '4. Data lives in PostgreSQL via Models/Modules and is served through the API — never bundle large computed data into the app. Schema changes have two homes: tables/authz are generated from Models (`db:generate`); functions, views, and matviews are authored in `db/sql/` and deployed with `db:reconcile` never a hand-written migration either way. Reuse `@everystack/ui` components; never put secret values behind `EXPO_PUBLIC_*`.',
74
+ '4. Data lives in PostgreSQL via Models/Modules and is served through the API — never bundle large computed data into the app. NOBODY AUTHORS MIGRATIONS. Schema work has three homes: tables/constraints/authz are DECLARED in `db/models/` (edit the Model, then `everystack db:sync` moves the dev database to the checkout — verified by fingerprint); functions/views/matviews are authored in `db/sql/` (deployed by `db:reconcile`/`db:sync`); one-shot DATA moves are authored in `db/backfills/*.sql` (run via `db:backfill`, never as a schema side effect). Protected stages never take a sync: `db:plan` mints a reviewable, fingerprint-pinned edge and `db:apply` verifies at both ends — the checkout must descend from the commit declaring the target\'s state ("rebase first"), and destructive plans are confirmed, snapshotted, and approver-gated (`db:approvers`). CI runs `everystack db:check` (the merged declared state must compose; generated artifacts must match regeneration byte-for-byte). Per-branch dev databases: `db:template:refresh` + `db:branch`; deployed feature stages fork data with `db:fork`. Reuse `@everystack/ui` components; never put secret values behind `EXPO_PUBLIC_*`.',
75
75
  '5. When the user wants to start a new project, run check_environment (phase "local" for dev, "deploy" for deployment) to verify prerequisites.',
76
76
  '6. When the user needs to interact with deployed infrastructure, guide them to use the everystack CLI.',
77
77
  ].join('\n');
@@ -61,8 +61,10 @@ export function registerClaudeMdPrompt(server: McpServer): void {
61
61
  '',
62
62
  'Reconcile the existing CLAUDE.md against three things, and propose a concrete diff:',
63
63
  '1. **Framework drift** — does it teach superseded ways (e.g. `drizzle-kit generate`,',
64
- ' hand-written migrations, "pass your Drizzle schema") instead of the v3 Model flow',
65
- ' (`defineModel` → `everystack db:generate`)? Suggest the current conventions.',
64
+ ' hand-written migrations, a migration journal, "pass your Drizzle schema") instead of',
65
+ ' the migration-authority flow (`defineModel` in db/models/ → `everystack db:sync` on',
66
+ ' dev; `db:plan` → `db:apply` on protected stages; `db:check` in CI; data moves in',
67
+ ' db/backfills/ via `db:backfill`)? Suggest the current conventions.',
66
68
  '2. **Project drift** — do the Structure / Commands / package list match what is actually',
67
69
  ' installed now (new packages, a new tier, a new models/ dir)? Suggest updates. Also',
68
70
  ' check the operations manual: if docs/RUNBOOK.md is missing, or stale per',
@@ -19,12 +19,19 @@ These are enforced (everystack cheat gates) and load-bearing. Do not work around
19
19
  - **Data lives in PostgreSQL, served through the API — never bundle data into the app.** A
20
20
  large `.json`/`.csv` of computed data in the bundle is wrong; model it and serve it, or
21
21
  render an empty state if it does not exist yet.
22
- - **Schema changes have two homes; neither is a hand-written migration.** Tables and authz
23
- are generated from Models: declare with `defineModel` (in `db/models/`), run
24
- `everystack db:generate` never hand-write a SQL migration, never edit the generated
25
- `db/schema.ts`; after any change `db:generate` must be a clean no-op. Functions, views,
26
- and matviews are **authored** in `db/sql/` and **deployed** with `everystack db:reconcile`
27
- edit the SQL file, reconcile, done; `db:reconcile --check` must exit 0.
22
+ - **Nobody authors migrations. Schema work has three homes.** Tables and authz are DECLARED
23
+ in `db/models/` with `defineModel` edit the Model, then `everystack db:sync` moves the
24
+ dev database to your checkout (state + authz + derived, fingerprint-verified). Functions,
25
+ views, and matviews are **authored** in `db/sql/` and deployed by `db:reconcile`/`db:sync`.
26
+ One-shot DATA moves are authored in `db/backfills/*.sql` and run via `everystack
27
+ db:backfill` deliberately, never as a schema side effect. Never hand-write a SQL
28
+ migration, never edit `db/schema.generated.ts`; `everystack db:check` must pass (the CI
29
+ gate: the declared state composes, generated artifacts match regeneration).
30
+ - **Protected stages take plans, not syncs.** `everystack db:plan` mints a reviewable edge
31
+ pinned by fingerprints at both ends; `db:apply` verifies before and after, requires the
32
+ checkout to descend from the commit declaring the target's state ("rebase first"), and
33
+ gates destructive plans: `--confirm` always, snapshot always, the stage's approver set
34
+ (`db:approvers`) when declared.
28
35
  - **Authorization is declared, not hand-written.** Use `can()` abilities on the Model; they
29
36
  compile to RLS + grants. Never hand-write `CREATE POLICY`/`GRANT`. RLS is required.
30
37
  - **Reuse `@everystack/ui`.** Do not hand-roll a component that already exists there. Style
@@ -36,10 +43,11 @@ These are enforced (everystack cheat gates) and load-bearing. Do not work around
36
43
  ## Start Here
37
44
 
38
45
  - `db/models/` — `defineModel` tables (the source of truth for schema + authz)
39
- - `db/sql/` — functions, views, matviews (authored SQL; deploys via `db:reconcile`)
46
+ - `db/sql/` — functions, views, matviews (authored SQL; deploys via `db:reconcile`/`db:sync`)
47
+ - `db/backfills/` — one-shot data jobs (authored SQL; run via `db:backfill`, own record)
40
48
  - `app/` — Expo Router pages (screens, navigation, API routes)
41
49
  - `server/` — Lambda handlers (api.ts, worker.ts, image.ts)
42
- - `db/schema.generated.ts`, `drizzle/` — generated artifacts (do not edit by hand)
50
+ - `db/schema.generated.ts` — generated artifact (never edit; `db:check` refuses drift)
43
51
  - `lib/` — shared code (auth context, API client)
44
52
  - `sst.config.ts` — AWS infrastructure definition
45
53
  - `docs/RUNBOOK.md` — how to operate this app; regenerate with `everystack runbook`
@@ -54,9 +62,12 @@ These are enforced (everystack cheat gates) and load-bearing. Do not work around
54
62
  pnpm install # Install dependencies
55
63
  pnpm dev # Start the Expo dev server
56
64
  pnpm test # Run all tests (TDD)
57
- everystack db:generate # Models next migration (data + authz)
58
- everystack db:reconcile --apply # Deploy functions/views/matviews from db/sql (no migrations)
59
- everystack db:migrate # Apply migrations on the deployed Lambda
65
+ everystack db:sync # Dev edit loop: the database follows your checkout (state + authz + derived)
66
+ everystack db:check # CI gate: declared state composes; generated artifacts match regeneration
67
+ everystack db:plan | db:apply # Protected stages: reviewable, fingerprint-verified edges
68
+ everystack db:branch # Per-git-branch dev DB from the seeded template (db:template:refresh builds it)
69
+ everystack db:backfill --apply # Run one-shot data jobs from db/backfills/ (own record)
70
+ everystack db:generate # Models → the diff as SQL (review surface)
60
71
  everystack db:seed # Seed the database (dev only)
61
72
  everystack deploy --stage dev # Deploy infrastructure (SST)
62
73
  everystack update --channel production # OTA update (no redeploy)
@@ -69,12 +80,15 @@ everystack bundle:audit <url> # Audit a deployed bundle (weight +
69
80
  ### Models are the source of truth (v3)
70
81
 
71
82
  Declare tables with `defineModel` (`field`, `can`, relations). A package's full DB slice is a
72
- `defineModule`; the app composes Modules. `everystack db:generate` compiles them to one
73
- migration (schema + RLS + grants); `deriveHandlerConfig(models)` derives the API config. You
83
+ `defineModule`; the app composes Modules. The database's own schema is the authority — the
84
+ live state is content-addressed (`db:fingerprint`), and every apply verifies against it. You
74
85
  never hand-write migrations, RLS, or handler access-control — they are derived, so they cannot
75
- drift. The derived layer (functions, views, matviews) is the exception that proves the rule:
76
- that SQL is authored, in `db/sql/`, and *deployed* with `db:reconcile` — hand-edits straight
77
- against the database surface as drift, and a comment-only edit is a no-op.
86
+ drift; `deriveHandlerConfig(models)` derives the API config. The derived layer (functions,
87
+ views, matviews) is the exception that proves the rule: that SQL is authored, in `db/sql/`,
88
+ and *deployed* by reconcile — hand-edits straight against the database surface as drift, and a
89
+ comment-only edit is a no-op. Contraction is a two-step ceremony: `field.deprecated()` first
90
+ (the column stays readable, new writes are rejected, generated types strike it through), the
91
+ physical drop later — a destructive plan, confirmed, snapshotted, approver-gated.
78
92
 
79
93
  ### Security over all else
80
94