@timekast/factory 1.23.1 → 1.25.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.
@@ -12,15 +12,26 @@
12
12
  * `src/` + boilerplate deps are the derived project's responsibility
13
13
  * post-bootstrap, and that the patch channel for `src/` is EPIC 2 (agentic
14
14
  * update, not yet available).
15
+ * - MISSING INITIAL MIGRATION: a derivative that ships a Drizzle schema and no
16
+ * generated `.sql`, so its `vercel-build` (`pnpm db:migrate && pnpm build`)
17
+ * would migrate an empty database. INFORMATIONAL — see below.
15
18
  *
16
19
  * No lockfile (pre-CLI repo) → delegate to the "run factory:update" advice, exit
17
20
  * 0, never abort (same pattern as `status` / the §8 edge case).
21
+ *
22
+ * 🔴 EVERY CHECK HERE IS ADVISORY. `runDoctor` always exits 0 by contract; the
23
+ * sibling entrypoint {@link runLauncherDoctor} is the one that throws `CLIError(1)`
24
+ * on any `status: 'fail'`. A report-only finding therefore belongs in
25
+ * `computeDoctor`/`renderDoctor` and NOWHERE ELSE — moving it to the launcher would
26
+ * turn a notice into a blocked command through the back door. If it ever has to
27
+ * live there, it goes as `warn` (`LauncherCheckStatus`), which never trips the exit.
18
28
  */
19
29
  import { existsSync, readFileSync, statSync } from 'node:fs';
20
30
  import path from 'node:path';
21
31
  import { CLIError } from '../lib/cli-error.js';
22
32
  import { collectClaudePaths } from '../lib/claude-paths.js';
23
33
  import { CLAUDE_MD_FILE } from '../lib/constants.js';
34
+ import { MIGRATIONS_DIR, SCHEMA_DIR, needsInitialMigration, readMigrationFacts, } from '../lib/initial-migration.js';
24
35
  import { hasLockfile, normalizeThenHash, readLockfile } from '../lib/lockfile.js';
25
36
  import { runLauncherPreflight, } from '../lib/preflight.js';
26
37
  import { detectRepo } from '../lib/repo-detection.js';
@@ -46,6 +57,18 @@ export const A1_WARNING = 'Aviso de seguridad (A1): el boilerplate `src/` y sus
46
57
  'dependencia) NO llega por este canal. El canal de parches de `src/` es EPIC 2 ' +
47
58
  '(update agentico con merge inteligente), aún no disponible. Mantén `src/` y tus ' +
48
59
  'dependencias al día por tu cuenta mientras tanto.';
60
+ /**
61
+ * The literal notice for a derivative whose schema has no generated migration.
62
+ * Named constant (never an inline string — CODING.md §5) and built from the SAME
63
+ * directory constants the provision step uses, so a path can never drift between
64
+ * what `provision` writes and what `doctor` reports. Exported for the tests.
65
+ */
66
+ export const MISSING_INITIAL_MIGRATION_NOTICE = `⚠ Falta la migración inicial: hay schema en \`${SCHEMA_DIR}/\` y ningún \`.sql\` en ` +
67
+ `\`${MIGRATIONS_DIR}/\`.\n` +
68
+ ' El deploy corre `pnpm db:migrate && pnpm build`, así que el primer build migraría una ' +
69
+ 'base vacía. Genera la migración con `pnpm db:generate` y commitéala antes del próximo ' +
70
+ 'deploy.\n' +
71
+ ' (Aviso informativo: `factory doctor` no bloquea — sale con éxito de todos modos.)';
49
72
  /** Parse `doctor`'s argv slice into flags. Unknown flags are ignored. */
50
73
  export function parseDoctorFlags(argv) {
51
74
  return {
@@ -114,6 +137,10 @@ export function computeDoctor(deps = {}) {
114
137
  // `doctor` from a subdir of a managed repo reports the real state instead of
115
138
  // "no lockfile" (consistency with `add`/`update`). Tests inject `rootDir`.
116
139
  const rootDir = deps.rootDir ?? detectRepo(process.cwd()).repoRoot ?? process.cwd();
140
+ // Initial-migration gap (GAP-002). Delegated WHOLE to the provision predicate —
141
+ // facts read from disk, decision (Factory exception included) taken there. Doctor
142
+ // does not re-derive "is this the Factory": one copy of that rule, in one place.
143
+ const missingInitialMigration = needsInitialMigration(readMigrationFacts(rootDir));
117
144
  if (!hasLockfile(rootDir)) {
118
145
  return {
119
146
  orphans: [],
@@ -121,6 +148,9 @@ export function computeDoctor(deps = {}) {
121
148
  unimportedRules: [],
122
149
  claudeMdMissing: false,
123
150
  a1Warning: A1_WARNING,
151
+ // Always false on this branch, and by the predicate rather than by a literal:
152
+ // no lockfile IS the Factory signal, which `needsInitialMigration` short-circuits.
153
+ missingInitialMigration,
124
154
  noLockfile: true,
125
155
  beta: false,
126
156
  };
@@ -174,6 +204,7 @@ export function computeDoctor(deps = {}) {
174
204
  unimportedRules,
175
205
  claudeMdMissing,
176
206
  a1Warning: A1_WARNING,
207
+ missingInitialMigration,
177
208
  noLockfile: false,
178
209
  beta,
179
210
  };
@@ -181,6 +212,8 @@ export function computeDoctor(deps = {}) {
181
212
  /** Render the doctor result to the console. The A1 notice is ALWAYS last. */
182
213
  function renderDoctor(result) {
183
214
  if (result.noLockfile) {
215
+ // Nothing to say about migrations here: no lockfile IS the Factory signal, so
216
+ // `missingInitialMigration` is false by construction on this branch.
184
217
  console.log('Sin lockfile detectado.');
185
218
  console.log('Ejecuta `factory:update` para inicializar el tracking de versión.');
186
219
  // The A1 notice is emitted even without a lockfile (always present).
@@ -223,6 +256,12 @@ function renderDoctor(result) {
223
256
  }
224
257
  }
225
258
  }
259
+ // Initial-migration gap (GAP-002): reported, never enforced. It sits outside the
260
+ // orphans/conflicts block because it is about the derivative's `src/`, not about
261
+ // brain drift — a repo with a spotless brain can still be missing its `0000`.
262
+ if (result.missingInitialMigration) {
263
+ console.log(`\n${MISSING_INITIAL_MIGRATION_NOTICE}`);
264
+ }
226
265
  // The A1 security notice is always emitted at the end (design §1 + §11).
227
266
  console.log(`\n${result.a1Warning}`);
228
267
  }