@warlock.js/core 4.5.0 → 4.6.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 (200) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/esm/ai/src/agent/agent.mjs +126 -7
  3. package/esm/ai/src/agent/agent.mjs.map +1 -1
  4. package/esm/ai/src/agent/signature.mjs +57 -0
  5. package/esm/ai/src/agent/signature.mjs.map +1 -0
  6. package/esm/ai/src/agent/snapshot.mjs +101 -0
  7. package/esm/ai/src/agent/snapshot.mjs.map +1 -0
  8. package/esm/ai/src/ai.mjs +37 -1
  9. package/esm/ai/src/ai.mjs.map +1 -1
  10. package/esm/ai/src/errors/agent-drift-error.mjs +31 -0
  11. package/esm/ai/src/errors/agent-drift-error.mjs.map +1 -0
  12. package/esm/ai/src/errors/index.mjs +2 -0
  13. package/esm/ai/src/errors/planner-drift-error.mjs +33 -0
  14. package/esm/ai/src/errors/planner-drift-error.mjs.map +1 -0
  15. package/esm/ai/src/image/image-cost.mjs +55 -0
  16. package/esm/ai/src/image/image-cost.mjs.map +1 -0
  17. package/esm/ai/src/image/image.mjs +112 -0
  18. package/esm/ai/src/image/image.mjs.map +1 -0
  19. package/esm/ai/src/image/index.mjs +4 -0
  20. package/esm/ai/src/index.mjs +20 -0
  21. package/esm/ai/src/planner/planner-run.mjs +161 -6
  22. package/esm/ai/src/planner/planner-run.mjs.map +1 -1
  23. package/esm/ai/src/planner/planner.mjs +25 -1
  24. package/esm/ai/src/planner/planner.mjs.map +1 -1
  25. package/esm/ai/src/planner/snapshot.mjs +95 -0
  26. package/esm/ai/src/planner/snapshot.mjs.map +1 -0
  27. package/esm/ai/src/rag/hybrid/bm25.mjs +51 -0
  28. package/esm/ai/src/rag/hybrid/bm25.mjs.map +1 -0
  29. package/esm/ai/src/rag/hybrid/hybrid-rank.mjs +29 -0
  30. package/esm/ai/src/rag/hybrid/hybrid-rank.mjs.map +1 -0
  31. package/esm/ai/src/rag/hybrid/rrf.mjs +30 -0
  32. package/esm/ai/src/rag/hybrid/rrf.mjs.map +1 -0
  33. package/esm/ai/src/rag/index.mjs +11 -0
  34. package/esm/ai/src/rag/loaders/errors.mjs +25 -0
  35. package/esm/ai/src/rag/loaders/errors.mjs.map +1 -0
  36. package/esm/ai/src/rag/loaders/index.mjs +7 -0
  37. package/esm/ai/src/rag/loaders/load-html.mjs +138 -0
  38. package/esm/ai/src/rag/loaders/load-html.mjs.map +1 -0
  39. package/esm/ai/src/rag/loaders/load-pdf.mjs +150 -0
  40. package/esm/ai/src/rag/loaders/load-pdf.mjs.map +1 -0
  41. package/esm/ai/src/rag/loaders/load-text.mjs +60 -0
  42. package/esm/ai/src/rag/loaders/load-text.mjs.map +1 -0
  43. package/esm/ai/src/rag/loaders/load-web.mjs +89 -0
  44. package/esm/ai/src/rag/loaders/load-web.mjs.map +1 -0
  45. package/esm/ai/src/rag/store/pg-vector-store.mjs +328 -0
  46. package/esm/ai/src/rag/store/pg-vector-store.mjs.map +1 -0
  47. package/esm/ai/src/rag/transforms/multi-query.mjs +41 -0
  48. package/esm/ai/src/rag/transforms/multi-query.mjs.map +1 -0
  49. package/esm/ai/src/speech/index.mjs +3 -0
  50. package/esm/ai/src/speech/speech.mjs +122 -0
  51. package/esm/ai/src/speech/speech.mjs.map +1 -0
  52. package/esm/ai/src/supervisor/entries.mjs +2 -2
  53. package/esm/ai/src/supervisor/entries.mjs.map +1 -1
  54. package/esm/ai/src/transcribe/audio-input.mjs +84 -0
  55. package/esm/ai/src/transcribe/audio-input.mjs.map +1 -0
  56. package/esm/ai/src/transcribe/index.mjs +4 -0
  57. package/esm/ai/src/transcribe/transcribe.mjs +127 -0
  58. package/esm/ai/src/transcribe/transcribe.mjs.map +1 -0
  59. package/esm/ai-openai/src/image.mjs +5 -0
  60. package/esm/ai-openai/src/index.mjs +3 -0
  61. package/esm/ai-openai/src/sdk.mjs +3 -0
  62. package/esm/ai-openai/src/speech.mjs +5 -0
  63. package/esm/ai-openai/src/transcription.mjs +6 -0
  64. package/esm/ai-openai/src/utils/index.mjs +1 -0
  65. package/esm/ai-openai/src/utils/to-openai-messages.mjs +3 -0
  66. package/esm/cli/commands/doctor/checks/config.check.mjs +42 -0
  67. package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -0
  68. package/esm/cli/commands/doctor/checks/connectors.check.mjs +28 -0
  69. package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -0
  70. package/esm/cli/commands/doctor/checks/health.check.mjs +35 -0
  71. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -0
  72. package/esm/cli/commands/doctor/checks/index.mjs +25 -0
  73. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -0
  74. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +90 -0
  75. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -0
  76. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +69 -0
  77. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -0
  78. package/esm/cli/commands/doctor/checks/routes.check.mjs +30 -0
  79. package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -0
  80. package/esm/cli/commands/doctor/doctor-command.action.mjs +28 -0
  81. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -0
  82. package/esm/cli/commands/doctor/format-report.mjs +44 -0
  83. package/esm/cli/commands/doctor/format-report.mjs.map +1 -0
  84. package/esm/cli/commands/doctor/run-checks.mjs +50 -0
  85. package/esm/cli/commands/doctor/run-checks.mjs.map +1 -0
  86. package/esm/cli/commands/doctor.command.mjs +29 -0
  87. package/esm/cli/commands/doctor.command.mjs.map +1 -0
  88. package/esm/cli/commands/generate/templates/stubs.mjs +6 -8
  89. package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
  90. package/esm/cli/commands/routes/format-routes-table.mjs +129 -0
  91. package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -0
  92. package/esm/cli/commands/routes/route-row.mjs +92 -0
  93. package/esm/cli/commands/routes/route-row.mjs.map +1 -0
  94. package/esm/cli/commands/routes/routes-command.action.mjs +43 -0
  95. package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -0
  96. package/esm/cli/commands/routes.command.mjs +51 -0
  97. package/esm/cli/commands/routes.command.mjs.map +1 -0
  98. package/esm/cli/commands/seed.command.mjs +5 -0
  99. package/esm/cli/commands/seed.command.mjs.map +1 -1
  100. package/esm/cli/framework-cli-commands.mjs +4 -0
  101. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  102. package/esm/config/config-getter.mjs +5 -5
  103. package/esm/config/config-getter.mjs.map +1 -1
  104. package/esm/config/config-loader.mjs +2 -2
  105. package/esm/config/config-loader.mjs.map +1 -1
  106. package/esm/connectors/access-connector.mjs +2 -2
  107. package/esm/connectors/access-connector.mjs.map +1 -1
  108. package/esm/connectors/ai-connector.mjs +2 -2
  109. package/esm/connectors/ai-connector.mjs.map +1 -1
  110. package/esm/connectors/cache-connector.mjs +2 -2
  111. package/esm/connectors/cache-connector.mjs.map +1 -1
  112. package/esm/connectors/database-connector.mjs +2 -2
  113. package/esm/connectors/database-connector.mjs.map +1 -1
  114. package/esm/connectors/herald-connector.mjs +2 -2
  115. package/esm/connectors/herald-connector.mjs.map +1 -1
  116. package/esm/connectors/http-connector.d.mts.map +1 -1
  117. package/esm/connectors/http-connector.mjs +9 -7
  118. package/esm/connectors/http-connector.mjs.map +1 -1
  119. package/esm/connectors/logger-connector.mjs +2 -2
  120. package/esm/connectors/logger-connector.mjs.map +1 -1
  121. package/esm/connectors/mail-connector.mjs +2 -2
  122. package/esm/connectors/mail-connector.mjs.map +1 -1
  123. package/esm/connectors/notifications-connector.mjs +2 -2
  124. package/esm/connectors/notifications-connector.mjs.map +1 -1
  125. package/esm/connectors/socket-connector.mjs +3 -3
  126. package/esm/connectors/socket-connector.mjs.map +1 -1
  127. package/esm/database/index.d.mts +2 -1
  128. package/esm/database/index.mjs +1 -0
  129. package/esm/database/seed-command-action.mjs +44 -4
  130. package/esm/database/seed-command-action.mjs.map +1 -1
  131. package/esm/database/seeds/seed-records-table-migration.mjs +36 -0
  132. package/esm/database/seeds/seed-records-table-migration.mjs.map +1 -0
  133. package/esm/database/seeds/seeder.d.mts +22 -5
  134. package/esm/database/seeds/seeder.d.mts.map +1 -1
  135. package/esm/database/seeds/seeder.errors.d.mts +22 -0
  136. package/esm/database/seeds/seeder.errors.d.mts.map +1 -0
  137. package/esm/database/seeds/seeder.errors.mjs +29 -0
  138. package/esm/database/seeds/seeder.errors.mjs.map +1 -0
  139. package/esm/database/seeds/seeder.mjs.map +1 -1
  140. package/esm/database/seeds/seeders.manager.mjs +90 -11
  141. package/esm/database/seeds/seeders.manager.mjs.map +1 -1
  142. package/esm/database/seeds/types.d.mts +117 -1
  143. package/esm/database/seeds/types.d.mts.map +1 -1
  144. package/esm/database/seeds/utils.mjs +6 -1
  145. package/esm/database/seeds/utils.mjs.map +1 -1
  146. package/esm/dev-server/module-loader.d.mts +12 -0
  147. package/esm/dev-server/module-loader.d.mts.map +1 -1
  148. package/esm/dev-server/module-loader.mjs +30 -2
  149. package/esm/dev-server/module-loader.mjs.map +1 -1
  150. package/esm/http/config.mjs +2 -2
  151. package/esm/http/config.mjs.map +1 -1
  152. package/esm/http/createHttpApplication.mjs +2 -2
  153. package/esm/http/createHttpApplication.mjs.map +1 -1
  154. package/esm/http/health.d.mts +14 -0
  155. package/esm/http/health.d.mts.map +1 -1
  156. package/esm/http/health.mjs +16 -0
  157. package/esm/http/health.mjs.map +1 -1
  158. package/esm/http/middleware/idempotency.middleware.mjs +5 -5
  159. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  160. package/esm/http/middleware/inject-request-context.mjs +2 -2
  161. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  162. package/esm/http/middleware/maintenance.middleware.mjs +4 -4
  163. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  164. package/esm/http/plugins.mjs +9 -9
  165. package/esm/http/plugins.mjs.map +1 -1
  166. package/esm/http/response.mjs +5 -5
  167. package/esm/http/response.mjs.map +1 -1
  168. package/esm/http/server.mjs +4 -4
  169. package/esm/http/server.mjs.map +1 -1
  170. package/esm/index.d.mts +4 -3
  171. package/esm/index.mjs +2 -1
  172. package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +28 -1
  173. package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts.map +1 -1
  174. package/esm/repositories/adapters/cascade/cascade-query-builder.mjs +39 -0
  175. package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
  176. package/esm/repositories/contracts/index.d.mts +1 -1
  177. package/esm/repositories/contracts/query-builder.contract.d.mts +87 -1
  178. package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
  179. package/esm/repositories/index.d.mts +1 -1
  180. package/esm/repositories/repository.manager.d.mts +99 -1
  181. package/esm/repositories/repository.manager.d.mts.map +1 -1
  182. package/esm/repositories/repository.manager.mjs +128 -0
  183. package/esm/repositories/repository.manager.mjs.map +1 -1
  184. package/esm/router/router.d.mts +9 -0
  185. package/esm/router/router.d.mts.map +1 -1
  186. package/esm/router/router.mjs +21 -1
  187. package/esm/router/router.mjs.map +1 -1
  188. package/esm/utils/paths.mjs +2 -2
  189. package/esm/utils/paths.mjs.map +1 -1
  190. package/esm/validation/validateAll.mjs +2 -2
  191. package/esm/validation/validateAll.mjs.map +1 -1
  192. package/llms-full.txt +427 -45
  193. package/llms.txt +4 -2
  194. package/package.json +9 -9
  195. package/skills/README.md +65 -0
  196. package/skills/health-checks/SKILL.md +13 -0
  197. package/skills/use-repository/SKILL.md +38 -1
  198. package/skills/warlock-doctor/SKILL.md +135 -0
  199. package/skills/warlock-routes/SKILL.md +91 -0
  200. package/skills/write-seeder/SKILL.md +143 -45
package/llms-full.txt CHANGED
@@ -2376,6 +2376,19 @@ health.removeCheck("db"); // unregister later if needed
2376
2376
 
2377
2377
  A check returns `boolean | Promise<boolean>`. **A thrown error counts as a failed check** (it's surfaced in the `checks` map + the 503, not logged — probes poll often, so a failure is a normal signal, not an error event). Keep checks cheap and fast; they run on every `/ready` poll.
2378
2378
 
2379
+ ### Routes-registered readiness signal
2380
+
2381
+ A booted HTTP app that ends up with **zero routes** almost always means a route module failed to register silently — without this check it would 404 every request while reporting ready. `health.addRoutesRegisteredCheck(getRouteCount)` registers a `"routes"` check that fails while the count is `0`:
2382
+
2383
+ ```ts
2384
+ import { health, router } from "@warlock.js/core";
2385
+
2386
+ health.addRoutesRegisteredCheck(() => router.routeCount());
2387
+ // pass a second arg to rename the check, e.g. "http-routes"
2388
+ ```
2389
+
2390
+ The count is passed as a getter so the registry stays decoupled from the router. Apps with no HTTP surface simply never register it.
2391
+
2379
2392
  ## Graceful shutdown (request draining)
2380
2393
 
2381
2394
  On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks → connectors in reverse priority**. The HTTP connector's teardown drains instead of dropping:
@@ -6362,7 +6375,7 @@ Rule of thumb: transformers are for **pure, deterministic** transforms of the ro
6362
6375
 
6363
6376
  ---
6364
6377
  name: use-repository
6365
- description: 'Subclass `RepositoryManager` for data access — declare `source`, `filterBy`, `defaultOptions`, then call `list()`/`listCached()`/`find()`/`create()`/`update()`/`delete()` (and the active/cached/cursor variants). Triggers: `RepositoryManager`, `FilterRules`, `RepositoryOptions`, `.list`, `.listCached`, `.find`, `.findCached`, `.create`, `.update`, `.delete`, `simpleSelectColumns`; "create a repository", "filter rules for a list endpoint", "cursor vs page pagination", "cached vs uncached read"; typical import `import { RepositoryManager } from "@warlock.js/core"`. Skip: cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; use-case pipelines — `@warlock.js/core/write-use-case/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing libs `typeorm` Repository, `prisma.client.<model>`, `@nestjs/typeorm`.'
6378
+ description: 'Subclass `RepositoryManager` for data access — declare `source`, `filterBy`, `defaultOptions`, then call `list()`/`listCached()`/`find()`/`create()`/`update()`/`delete()`, the active/cached/cursor variants, and the `filterBy`-aware aggregates `sum()`/`avg()`/`min()`/`max()`/`groupBy()`/`aggregate()`. Triggers: `RepositoryManager`, `FilterRules`, `RepositoryOptions`, `.list`, `.listCached`, `.find`, `.findCached`, `.create`, `.update`, `.delete`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.aggregate`, `simpleSelectColumns`; "create a repository", "filter rules for a list endpoint", "cursor vs page pagination", "cached vs uncached read", "sum/avg/group-by with filters"; typical import `import { RepositoryManager } from "@warlock.js/core"`. Skip: cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; use-case pipelines — `@warlock.js/core/write-use-case/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing libs `typeorm` Repository, `prisma.client.<model>`, `@nestjs/typeorm`.'
6366
6379
  ---
6367
6380
 
6368
6381
  # Warlock — use a repository
@@ -6553,6 +6566,43 @@ await repo.countActive(options?);
6553
6566
  await repo.countCached(options?);
6554
6567
  ```
6555
6568
 
6569
+ ### Aggregation
6570
+
6571
+ ```ts
6572
+ await repo.sum(field, options?); // → number
6573
+ await repo.avg(field, options?); // → number
6574
+ await repo.min(field, options?); // → number
6575
+ await repo.max(field, options?); // → number
6576
+
6577
+ await repo.groupBy<R>(fields, aggregates, options?); // → R[]
6578
+ await repo.aggregate<R>(aggregates, options?); // → R | null
6579
+ ```
6580
+
6581
+ Every aggregate runs `applyOptionsToQuery(prepareOptions(options))` **first** — exactly like `count()` — so `filterBy` (and its operator-injection guard), `where`, scopes, and the rest of the repository options scope the aggregate before it executes. Pass the same filter inputs you'd pass to `list()`:
6582
+
6583
+ ```ts
6584
+ // filtered scalar aggregates — the filterBy rules apply
6585
+ const paidTotal = await ordersRepository.sum("total", { status: "paid" });
6586
+ const avgTicket = await ordersRepository.avg("total", { status: "paid" });
6587
+
6588
+ // group-by + aggregate expressions (use the cascade $agg helpers)
6589
+ import { $agg } from "@warlock.js/cascade";
6590
+
6591
+ const byStatus = await ordersRepository.groupBy(
6592
+ "status",
6593
+ { count: $agg.count(), total: $agg.sum("total") },
6594
+ { paidAfter: "2026-01-01" }, // a filterBy key
6595
+ );
6596
+
6597
+ // one summary row over the whole filtered set (null when empty)
6598
+ const summary = await ordersRepository.aggregate(
6599
+ { total: $agg.sum("total"), avg: $agg.avg("total") },
6600
+ { status: "paid" },
6601
+ );
6602
+ ```
6603
+
6604
+ `fields` accepts a string, a `string[]`, or a column map; `aggregates` is a map of result alias → aggregate expression. Because filters are applied first, you never have to re-implement `filterBy` logic for a stats endpoint — the same inputs scope the count *and* the sums.
6605
+
6556
6606
  ### Chunking
6557
6607
 
6558
6608
  ```ts
@@ -7185,6 +7235,240 @@ Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasM
7185
7235
  - [`send-response/SKILL.md`](../send-response/SKILL.md) — the full Response helper surface.
7186
7236
 
7187
7237
 
7238
+ ## warlock-doctor `@warlock.js/core/warlock-doctor/SKILL.md`
7239
+
7240
+ ---
7241
+ name: warlock-doctor
7242
+ description: 'Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `yarn warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.'
7243
+ ---
7244
+
7245
+ # Warlock — `warlock doctor`
7246
+
7247
+ `warlock doctor` is a read-only preflight. It boots the app far enough to introspect it — loads every config file and bootstrap code so routes and connectors register — but **starts no connectors**, so it never opens a database, cache, or socket connection. It then runs a set of checks and prints a grouped pass / warn / fail report.
7248
+
7249
+ ```bash
7250
+ yarn warlock doctor
7251
+ ```
7252
+
7253
+ ```
7254
+ ✓ routes: 42 registered
7255
+ ✓ config: required sections present (app, http)
7256
+ ✓ connectors: 3 registered, active: database, cache
7257
+ ⚠ optional-peers: not installed → unavailable: sharp (image processing); redis (Redis cache driver)
7258
+ ✓ health: liveness /health + readiness /ready registered
7259
+ ✓ release-hygiene: package.json and CHANGELOG agree on 4.6.0
7260
+
7261
+ Summary: 5 ok, 1 warn, 0 fail
7262
+ ```
7263
+
7264
+ ## What it checks
7265
+
7266
+ The default set is `defaultDoctorChecks` (in registration order — runtime-surface checks first, release hygiene last):
7267
+
7268
+ | Check | `ok` | `warn` | `fail` |
7269
+ | --- | --- | --- | --- |
7270
+ | `routes` | ≥ 1 route registered | 0 routes (a route module likely failed to load) | — |
7271
+ | `config` | required sections (`app`, `http`) present | — | a required section is missing |
7272
+ | `connectors` | manager enumerable; reports registered + active set | — | (only if the probe itself throws) |
7273
+ | `optional-peers` | every known optional peer installed | a peer is missing → its feature is unavailable | — |
7274
+ | `health` | `/health` + `/ready` will be exposed | `http.health.enabled = false` (probes off) | — |
7275
+ | `release-hygiene` | `package.json` version matches the top `## x.y.z` CHANGELOG heading | no `CHANGELOG.md`, or no parseable heading | version ≠ top heading |
7276
+
7277
+ Optional peers are resolved from the **consuming app's** `node_modules` (via `createRequire(process.cwd())`), so the report reflects what *your* project has installed, not core's own deps.
7278
+
7279
+ ## Exit code
7280
+
7281
+ ```
7282
+ warn only → exit 0 (warnings never fail the command)
7283
+ any fail → exit 1 (so CI / a release script can gate on it)
7284
+ ```
7285
+
7286
+ `doctor` itself can never crash: a check that throws is caught by the runner and recorded as a `fail` (detail `check threw: <message>`), not re-thrown.
7287
+
7288
+ ## The contract
7289
+
7290
+ ```ts
7291
+ type CheckStatus = "ok" | "warn" | "fail";
7292
+
7293
+ type CheckResult = {
7294
+ name: string; // stable check name, e.g. "routes"
7295
+ status: CheckStatus;
7296
+ detail: string; // one-line, user-facing explanation
7297
+ };
7298
+
7299
+ type DoctorCheck = {
7300
+ name: string;
7301
+ run: () => CheckResult | Promise<CheckResult>; // MUST be read-only
7302
+ };
7303
+
7304
+ type DoctorReport = {
7305
+ results: CheckResult[]; // in registration order
7306
+ summary: Record<CheckStatus, number>; // counts per status
7307
+ hasFailures: boolean; // true iff any fail
7308
+ exitCode: 0 | 1; // 1 when hasFailures, else 0
7309
+ };
7310
+ ```
7311
+
7312
+ A `DoctorCheck.run` must be **read-only** — it may introspect the router, connectors, config, and `package.json`, but must never mutate state or open a connection. It may be sync or async, and may throw (the runner degrades a throw to a `fail`).
7313
+
7314
+ ## Running checks yourself
7315
+
7316
+ The runner and formatter are exported, so you can assemble your own check set — e.g. an app-specific preflight script or a custom CLI command:
7317
+
7318
+ ```ts
7319
+ import {
7320
+ runChecks,
7321
+ formatReportLines,
7322
+ printReport,
7323
+ defaultDoctorChecks,
7324
+ type DoctorCheck,
7325
+ } from "@warlock.js/core";
7326
+
7327
+ const migrationsCheck: DoctorCheck = {
7328
+ name: "migrations",
7329
+ async run() {
7330
+ const pending = await countPendingMigrations(); // your read-only probe
7331
+
7332
+ return pending === 0
7333
+ ? { name: "migrations", status: "ok", detail: "no pending migrations" }
7334
+ : { name: "migrations", status: "warn", detail: `${pending} pending` };
7335
+ },
7336
+ };
7337
+
7338
+ const report = await runChecks([...defaultDoctorChecks, migrationsCheck]);
7339
+
7340
+ printReport(report); // colored, to stdout
7341
+ const lines = formatReportLines(report); // plain strings — snapshot-friendly
7342
+
7343
+ if (report.hasFailures) process.exit(report.exitCode);
7344
+ ```
7345
+
7346
+ `formatReportLines` returns plain (uncolored) `<symbol> <name>: <detail>` lines plus a `Summary: …` line — handy for tests. `printReport` is the colored stdout wrapper; the structured `DoctorReport` stays the source of truth for the exit code.
7347
+
7348
+ ## Patterns
7349
+
7350
+ ### CI / pre-release gate
7351
+
7352
+ ```bash
7353
+ yarn warlock doctor || exit 1 # non-zero exit fails the job
7354
+ ```
7355
+
7356
+ A red `release-hygiene` line catches the classic "bumped `package.json` but forgot the CHANGELOG heading" mistake before a publish.
7357
+
7358
+ ### Diagnosing a silent 404 surface
7359
+
7360
+ A `⚠ routes: 0 routes registered` line is the tell that a route module threw on import or registration and the failure was surfaced (not swallowed). Re-run `warlock dev` and read the boot error.
7361
+
7362
+ ## Gotchas
7363
+
7364
+ - **`doctor` starts no connectors.** It intentionally does not open DB / cache / socket connections — the `connectors` check reports the *registered* set and which are already active, without forcing any up. Don't expect it to detect a down database.
7365
+ - **Optional-peers resolves from the project, not core.** A peer installed only inside core's own `node_modules` still reports as missing — the probe uses `process.cwd()`.
7366
+ - **Warnings don't fail the command.** Only a `fail` flips the exit code. Missing optional peers and disabled health endpoints are deliberate-choice `warn`s.
7367
+ - **Custom checks must stay read-only.** `run()` runs against a booted-but-not-connected app; mutating state or opening a connection breaks the contract and can hang the command.
7368
+
7369
+ ## See also
7370
+
7371
+ - [`health-checks/SKILL.md`](../health-checks/SKILL.md) — the live `/health` + `/ready` HTTP probes and the `health` registry the `health` doctor check reports on.
7372
+ - [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — authoring a command + its `preload` plan, the shape `doctorCommand` is built from.
7373
+ - [`add-connector/SKILL.md`](../add-connector/SKILL.md) — connector registration, which the `connectors` check enumerates.
7374
+ - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — project layout the checks assume.
7375
+
7376
+
7377
+ ## warlock-routes `@warlock.js/core/warlock-routes/SKILL.md`
7378
+
7379
+ ---
7380
+ name: warlock-routes
7381
+ description: 'Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Triggers: `warlock routes`, `routesCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI"; run as `yarn warlock routes`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.'
7382
+ ---
7383
+
7384
+ # Warlock — `warlock routes`
7385
+
7386
+ `warlock routes` lists every registered HTTP route as a table. It's the read-only sibling of [`warlock doctor`](../warlock-doctor/SKILL.md): it boots the app far enough to register route modules — but **starts no connectors**, so it never opens a database, cache, or socket connection.
7387
+
7388
+ ```bash
7389
+ yarn warlock routes
7390
+ ```
7391
+
7392
+ ```
7393
+ METHOD PATH NAME ACTION MW SOURCE
7394
+ GET /users users.list index 2 app/users/routes.ts
7395
+ POST /users users.create store 2 app/users/routes.ts
7396
+ GET /users/:id users.get show 2 app/users/routes.ts
7397
+ DELETE /users/:id users.delete destroy 3 app/users/routes.ts
7398
+
7399
+ 4 routes (2 GET · 1 POST · 1 DELETE)
7400
+ ```
7401
+
7402
+ The `METHOD` column is verb-colored (GET green, POST blue, PUT/PATCH yellow, DELETE red, `all`/OPTIONS/HEAD dim). Rows sort by path, then by HTTP-method order within a path.
7403
+
7404
+ ## Columns
7405
+
7406
+ | Column | Meaning |
7407
+ | -------- | ----------------------------------------------------------------------------- |
7408
+ | `METHOD` | HTTP verb; a wildcard `all` route lists as `ALL`. |
7409
+ | `PATH` | Full request path (group prefix already folded in). |
7410
+ | `NAME` | Route name, or `—` when unnamed. |
7411
+ | `ACTION` | Handler function name (`anonymous` for an unnamed handler). |
7412
+ | `MW` | Count of middleware attached to the route. |
7413
+ | `SOURCE` | Source file the route registered from, or `—`. |
7414
+
7415
+ ## Filters
7416
+
7417
+ Optional, case-insensitive, AND-combined:
7418
+
7419
+ ```bash
7420
+ yarn warlock routes --method GET # -m exact HTTP method
7421
+ yarn warlock routes --path /users # -p path substring
7422
+ yarn warlock routes --name users # -n route-name substring
7423
+ yarn warlock routes -m POST -p /users
7424
+ ```
7425
+
7426
+ ## JSON output
7427
+
7428
+ `--json` (`-j`) emits the normalized rows instead of the table — for `jq`, a CI diff, or a generated API map. Filters apply before serialization.
7429
+
7430
+ ```bash
7431
+ yarn warlock routes --json
7432
+ ```
7433
+
7434
+ ```json
7435
+ [
7436
+ { "method": "GET", "path": "/users", "name": "users.list", "action": "index", "middleware": 2, "source": "app/users/routes.ts" }
7437
+ ]
7438
+ ```
7439
+
7440
+ ## Patterns
7441
+
7442
+ ### Audit which routes are guarded
7443
+
7444
+ ```bash
7445
+ yarn warlock routes --json | jq '[.[] | select(.middleware == 0)]'
7446
+ ```
7447
+
7448
+ Surfaces public routes (no middleware) — a quick check that auth-protected paths actually carry a guard.
7449
+
7450
+ ### Confirm a route registered
7451
+
7452
+ ```bash
7453
+ yarn warlock routes --name users.create
7454
+ ```
7455
+
7456
+ An empty result means the route isn't registered — re-run `warlock dev` and read the boot error (the route-module loader is fail-loud, so a throwing route file aborts boot rather than being silently dropped).
7457
+
7458
+ ## Gotchas
7459
+
7460
+ - **No connectors are started.** The list reflects what's *registered*, not what would connect. It never opens a DB/cache/socket.
7461
+ - **An empty table is a tell.** `No routes registered` is the same signal `warlock doctor`'s `routes` check warns on — a route module likely failed to load.
7462
+ - **`MW` counts, it doesn't name.** It's a "is this route guarded?" signal; for the actual chain, read the route definition.
7463
+ - **`ACTION` is the handler function name.** A controller method shows its method name; an inline anonymous handler shows `anonymous`.
7464
+
7465
+ ## See also
7466
+
7467
+ - [`warlock-doctor/SKILL.md`](../warlock-doctor/SKILL.md) — the read-only diagnostics sibling; its `routes` check warns when this table would be empty.
7468
+ - [`register-route/SKILL.md`](../register-route/SKILL.md) — defining, naming, and grouping the routes this command lists.
7469
+ - [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — the command + `preload` shape `routesCommand` is built from.
7470
+
7471
+
7188
7472
  ## wire-socket `@warlock.js/core/wire-socket/SKILL.md`
7189
7473
 
7190
7474
  ---
@@ -7994,7 +8278,7 @@ export const optionalAuth: Middleware = async (request, response) => {
7994
8278
 
7995
8279
  ---
7996
8280
  name: write-seeder
7997
- description: 'Author a seed file under `src/app/<module>/seeds/<name>.ts` using the `seeder()` factory — `name`, `dependsOn`, `once`, `order`, `batchSize`, `run()`. Auto-discovered by `warlock seed`; tracked in a `seeds` table. Triggers: `seeder`, `Seeder`, `SeedResult`, `SeedersManager`, `warlock seed`, `--fresh`, `--list`, `--path`; "seed default roles", "one-time data migration", "auto-discovered seeds", "order seeds by dependency"; typical import `import { seeder } from "@warlock.js/core"`. Skip: module folder layout — `@warlock.js/core/create-module/SKILL.md`; repository CRUD — `@warlock.js/core/use-repository/SKILL.md`; CLI flags — `@warlock.js/core/write-cli-command/SKILL.md`; competing patterns: hand-rolled `node scripts/seed.js`, `typeorm-seeding`.'
8281
+ description: 'Author a seed file under `src/app/<module>/seeds/<name>.ts` using the `seeder()` factory — `name`, `dependsOn`, `once`, `order`, `batchSize`, `run({ track, now, batchSize })`. Auto-discovered by `warlock seed`; tracked in a `seeds` table; per-record refs in `seed_records` so `warlock seed --drop` can undo a seed. Triggers: `seeder`, `Seeder`, `SeedResult`, `SeedContext`, `SeedClock`, `track`, `now`, `batchSize`, `SeedersManager`, `warlock seed`, `--fresh`, `--drop`, `--list`, `--path`; "seed default roles", "undo a seed", "one-time data migration", "auto-discovered seeds", "order seeds by dependency", "deterministic seed timestamps", "inject a seed clock"; typical import `import { seeder } from "@warlock.js/core"`. Skip: module folder layout — `@warlock.js/core/create-module/SKILL.md`; repository CRUD — `@warlock.js/core/use-repository/SKILL.md`; CLI flags — `@warlock.js/core/write-cli-command/SKILL.md`; competing patterns: hand-rolled `node scripts/seed.js`, `typeorm-seeding`.'
7998
8282
  ---
7999
8283
 
8000
8284
  # Warlock — write a seeder
@@ -8012,22 +8296,21 @@ export default seeder({
8012
8296
  description: "Insert admin / member / viewer roles",
8013
8297
  once: true,
8014
8298
  order: 1,
8015
- async run() {
8016
- let recordsCreated = 0;
8017
-
8299
+ async run({ track }) {
8018
8300
  for (const slug of ["admin", "member", "viewer"]) {
8019
8301
  const existing = await Role.first({ slug });
8020
8302
  if (existing) continue;
8021
8303
 
8022
- await Role.create({ slug, name: slug.toUpperCase() });
8023
- recordsCreated++;
8304
+ // track() records the created row in `seed_records` (same transaction)
8305
+ // AND returns its argument so you can chain inline.
8306
+ track(await Role.create({ slug, name: slug.toUpperCase() }));
8024
8307
  }
8025
-
8026
- return { recordsCreated };
8027
8308
  },
8028
8309
  });
8029
8310
  ```
8030
8311
 
8312
+ `recordsCreated` is **auto-derived from the track count** — you no longer have to count by hand or return a `SeedResult`.
8313
+
8031
8314
  Run them:
8032
8315
 
8033
8316
  ```bash
@@ -8035,10 +8318,14 @@ yarn warlock seed # discover + run all
8035
8318
  yarn warlock seed --list # show registry, don't run
8036
8319
  yarn warlock seed --path=src/app/roles/seeds/default-roles.seed.ts # one file
8037
8320
  yarn warlock seed --fresh # truncate every table first, then run all
8321
+ yarn warlock seed --drop # undo every tracked record, reset the log
8322
+ yarn warlock seed --drop=default-roles # undo just one seeder's records
8038
8323
  ```
8039
8324
 
8040
8325
  `--fresh` truncates **every** table in the DB (`datasource.driver.truncateTable(table, { cascade: true })`), including the `seeds` tracking table. After `--fresh`, `once: true` seeds will run again.
8041
8326
 
8327
+ `--drop` is the surgical alternative: it deletes only the rows a seeder **tracked**, in reverse run-order, then resets the matching `seeds`-log rows so `once: true` seeds re-run. See [Undo — `seed_records` + `--drop`](#undo--seed_records----drop).
8328
+
8042
8329
  ## The `Seeder` type
8043
8330
 
8044
8331
  ```ts
@@ -8050,15 +8337,69 @@ type Seeder = {
8050
8337
  once?: boolean; // skip if a row exists in the `seeds` table
8051
8338
  order?: number; // sort key — lower runs first
8052
8339
  batchSize?: number; // documented; not enforced by the manager
8053
- run(): Promise<SeedResult | void>; // your work
8340
+ run(ctx: SeedContext): Promise<SeedResult | void>; // your work
8341
+ };
8342
+
8343
+ type SeedContext = {
8344
+ track: Track; // register created records for --drop
8345
+ now: SeedClock; // injectable clock — () => Date, default () => new Date()
8346
+ batchSize: number; // from the seeder's batchSize (0 when unset)
8054
8347
  };
8055
8348
 
8349
+ type SeedClock = () => Date;
8350
+
8056
8351
  type SeedResult = {
8057
- recordsCreated: number; // tracked in the seeds table
8352
+ recordsCreated: number; // OPTIONAL fallback when nothing was tracked
8058
8353
  };
8059
8354
  ```
8060
8355
 
8061
- Returning a `SeedResult` is optional — the manager uses `recordsCreated` only to populate metadata in the `seeds` table. If you don't return anything, the seed still counts as run (the row is inserted with `recordsCreated: 0`).
8356
+ `run` now receives a `{ track, now, batchSize }` context. Declaring the parameter is optional — an existing **zero-arg `run()` keeps working unchanged** (it just ignores the context). Returning a `SeedResult` is also optional: the manager prefers the track count and only falls back to `result.recordsCreated` when you tracked nothing. A seed that neither tracks nor returns still counts as run (the `seeds` row is inserted with `recordsCreated: 0`).
8357
+
8358
+ ## `now` — the injectable clock
8359
+
8360
+ `now` is a `SeedClock` (`() => Date`) the seed should read **instead of** an ambient `new Date()` / `dayjs()`. It defaults to `() => new Date()`, and the `SeedersManager` reads the **same clock** for its own seeds-log timestamps (`createdAt` / `firstRunAt` / `lastRunAt`) — so seed data and the tracking row never disagree.
8361
+
8362
+ ```ts
8363
+ async run({ track, now }) {
8364
+ // every timestamp in the run reads from one clock
8365
+ track(await User.create({ name: "Admin", created_at: now() }));
8366
+ }
8367
+ ```
8368
+
8369
+ Inject a fixed clock for deterministic tests or historical back-fills — pass `clock` to `SeedersManager` (`new SeedersManager({ clock: () => new Date("2020-01-01") })`) or, programmatically, to `seedCommandAction(options, { clock })`. Pinning the clock makes every timestamp in the run — seed data *and* the `seeds` log — resolve to that one value.
8370
+
8371
+ ## `batchSize` — bulk-insert hint
8372
+
8373
+ `batchSize` surfaces the seeder's own `batchSize` field into the run context (`0` when unset), so a seed can forward it straight to `Model.createMany(rows, { batchSize })` without re-reading its config:
8374
+
8375
+ ```ts
8376
+ async run({ track, batchSize }) {
8377
+ const rows = buildManyRows();
8378
+ track(await User.createMany(rows, { batchSize })); // chunked insert
8379
+ }
8380
+ ```
8381
+
8382
+ The manager doesn't act on `batchSize` itself — it only hands the value through; the chunking happens inside `createMany`.
8383
+
8384
+ ## `track` — register created records
8385
+
8386
+ `track` is how a seed tells the framework which rows it created, so `--drop` can undo them. It has three forms, and **every form returns its first argument** so you can wrap a `create()` call inline:
8387
+
8388
+ ```ts
8389
+ track(model) // single model — reads model.getTableName() + model.id
8390
+ track([modelA, modelB]) // bulk — array of models
8391
+ track("legacy_table", id) // raw escape hatch — table name + id, no model
8392
+ ```
8393
+
8394
+ ```ts
8395
+ async run({ track }) {
8396
+ const role = track(await Role.create({ slug: "admin" })); // chain inline
8397
+ const users = track(await User.createMany([...])); // bulk
8398
+ track("audit_log", auditId); // raw
8399
+ }
8400
+ ```
8401
+
8402
+ Tracked refs are written to the `seed_records` table **inside the same transaction** the seed runs in — if the seed throws, the refs roll back with the data. Only the **last run's** refs are kept: re-running a seed drops its prior refs first, so undo stays bounded to what currently exists.
8062
8403
 
8063
8404
  ## Auto-discovery
8064
8405
 
@@ -8102,6 +8443,34 @@ Skip-on-`once`:
8102
8443
 
8103
8444
  Use `once: true` for irreversible "starter" data (the framework's default `roles`, fixed enum tables) and leave it off for repeatable seeds (dev fixtures you re-run after rolling the DB).
8104
8445
 
8446
+ ## Undo — `seed_records` + `--drop`
8447
+
8448
+ Every record you `track()` is written to a `seed_records` table (created on first run via `SeedRecordsTableMigration`):
8449
+
8450
+ | Column | Meaning |
8451
+ | ---------- | ---------------------------------------- |
8452
+ | `seeder` | Owning seeder's `name` |
8453
+ | `table` | Table the tracked record lives in |
8454
+ | `recordId` | Primary key of the tracked record |
8455
+ | `runAt` | Wall time the reference was recorded |
8456
+
8457
+ `warlock seed --drop` reads those refs and undoes the seed:
8458
+
8459
+ ```bash
8460
+ yarn warlock seed --drop # undo every tracked record across all seeders
8461
+ yarn warlock seed --drop=default-roles # undo just one seeder's records
8462
+ ```
8463
+
8464
+ What it does, inside a single transaction:
8465
+
8466
+ 1. Deletes the tracked records in **reverse run-order**, and within each seed in **reverse insertion-order** (highest `seed_records.id` first — so children created after their parents are removed first).
8467
+ 2. Clears the `seed_records` refs it acted on.
8468
+ 3. Resets the matching `seeds`-log rows, so a `once: true` seed **re-runs** on the next `warlock seed`.
8469
+
8470
+ Only **tracked** rows are deleted — records you created without `track()` (or rows added by hand) are untouched. A seed whose `run()` never calls `track()` can't be undone with `--drop`; it has nothing to delete and its log row stays.
8471
+
8472
+ `--drop` vs `--fresh`: `--fresh` is the blunt reset (truncates *every* table); `--drop` is surgical (only what a seed tracked). Reach for `--drop` when you want to re-run one seeder without nuking the whole DB.
8473
+
8105
8474
  ## Ordering
8106
8475
 
8107
8476
  ```ts
@@ -8120,19 +8489,34 @@ seeder({
8120
8489
 
8121
8490
  The manager sorts by `order` ascending — lower runs first. Seeds without an `order` get sorted to the end (`Number.MAX_SAFE_INTEGER`).
8122
8491
 
8123
- `dependsOn: ["user-roles"]` is on the `Seeder` type but **not yet resolved** by the manager (there's an explicit `TODO: Handle dependsOn resolution` in the source — topological sort isn't implemented). For now, rely on `order` to express dependencies. The `dependsOn` field stays available for forward-compat.
8492
+ ### `dependsOn` — topological order
8493
+
8494
+ `dependsOn: ["user-roles"]` is **resolved** by the manager: it topologically sorts seeders so every dependency runs before its dependents, *layered over* the numeric `order` tie-break. Among seeders whose dependencies are already satisfied, the lowest `order` (then registration order) runs first.
8495
+
8496
+ ```ts
8497
+ seeder({ name: "roles", order: 20, async run() { /* ... */ } });
8498
+ seeder({ name: "admin-user", dependsOn: ["roles"], order: 10, async run() { /* ... */ } });
8499
+ // → roles runs FIRST despite its higher order, because admin-user depends on it.
8500
+ ```
8501
+
8502
+ Two ways it fails loudly:
8503
+
8504
+ - **Unknown dependency** — `dependsOn` names a seeder that isn't registered (or is `enabled: false`, since disabled seeds are filtered out *before* resolution) → `UnknownSeederDependencyError`.
8505
+ - **Cycle** — `a → b → a` → `SeederDependencyCycleError` (the message includes the cycle path).
8506
+
8507
+ Use `dependsOn` to express *hard* ordering constraints and `order` for soft sequencing within a dependency layer.
8124
8508
 
8125
8509
  ## Transactions
8126
8510
 
8127
- By default, each seed runs inside a transaction:
8511
+ By default, each seed runs inside a transaction. The manager wraps `run({ track })` **and** the write of its tracked `seed_records` refs in one transaction:
8128
8512
 
8129
8513
  ```ts
8130
8514
  const result = withTransaction
8131
- ? await transaction(async () => seeder.run())
8132
- : await seeder.run();
8515
+ ? await transaction(runSeeder) // run() + seed_records writes, atomically
8516
+ : await runSeeder();
8133
8517
  ```
8134
8518
 
8135
- If your `run()` throws, the transaction rolls back. The CLI takes `--transaction` to flip this — pass `--transaction=false` for large seeds where you want each insert committed individually (avoid the rollback-on-failure cost; accept partial state on error).
8519
+ If your `run()` throws, the transaction rolls back — including the `seed_records` refs, so a failed seed leaves no dangling track rows. The CLI takes `--transaction` to flip this — pass `--transaction=false` for large seeds where you want each insert committed individually (avoid the rollback-on-failure cost; accept partial state on error).
8136
8520
 
8137
8521
  Within a single seed, multiple model `create()` / `insert()` calls share the same transaction — partial-on-error doesn't apply unless you opt out.
8138
8522
 
@@ -8148,7 +8532,7 @@ export default seeder({
8148
8532
  name: "default-currencies",
8149
8533
  description: "USD / EUR / SAR / EGP currencies",
8150
8534
  order: 5,
8151
- async run() {
8535
+ async run({ track }) {
8152
8536
  const seeds = [
8153
8537
  { code: "USD", name: "US Dollar", decimals: 2 },
8154
8538
  { code: "EUR", name: "Euro", decimals: 2 },
@@ -8156,15 +8540,11 @@ export default seeder({
8156
8540
  { code: "EGP", name: "Egyptian Pound", decimals: 2 },
8157
8541
  ];
8158
8542
 
8159
- let recordsCreated = 0;
8160
8543
  for (const data of seeds) {
8161
8544
  const existing = await Currency.first({ code: data.code });
8162
8545
  if (existing) continue;
8163
- await Currency.create(data);
8164
- recordsCreated++;
8546
+ track(await Currency.create(data)); // count auto-derived from track()
8165
8547
  }
8166
-
8167
- return { recordsCreated };
8168
8548
  },
8169
8549
  });
8170
8550
  ```
@@ -8182,23 +8562,23 @@ export default seeder({
8182
8562
  name: "default-admin",
8183
8563
  description: "Insert the seed admin user",
8184
8564
  once: true, // never re-run
8185
- order: 20, // after roles (order: 10)
8186
- async run() {
8565
+ dependsOn: ["default-roles"], // roles must exist first
8566
+ async run({ track }) {
8187
8567
  const adminRole = await Role.first({ slug: "admin" });
8188
- if (!adminRole) throw new Error("admin role missing — run user-roles seed first");
8189
-
8190
- await User.create({
8191
- email: "admin@example.com",
8192
- password: "change-me-immediately",
8193
- role_id: adminRole.id,
8194
- });
8568
+ if (!adminRole) throw new Error("admin role missing — run the roles seed first");
8195
8569
 
8196
- return { recordsCreated: 1 };
8570
+ track(
8571
+ await User.create({
8572
+ email: "admin@example.com",
8573
+ password: "change-me-immediately",
8574
+ role_id: adminRole.id,
8575
+ }),
8576
+ );
8197
8577
  },
8198
8578
  });
8199
8579
  ```
8200
8580
 
8201
- Combining `once: true` + `order` is the canonical "starter data" pattern.
8581
+ Combining `once: true` + `dependsOn` is the canonical "starter data" pattern.
8202
8582
 
8203
8583
  ### Bulk dev fixtures (no `once`, large batch)
8204
8584
 
@@ -8211,16 +8591,15 @@ export default seeder({
8211
8591
  description: "100 sample products for dev",
8212
8592
  enabled: process.env.NODE_ENV === "development",
8213
8593
  order: 100,
8214
- async run() {
8215
- let recordsCreated = 0;
8594
+ async run({ track }) {
8216
8595
  for (let i = 0; i < 100; i++) {
8217
- await Product.create({
8218
- name: `Sample ${i}`,
8219
- price: Math.random() * 100,
8220
- });
8221
- recordsCreated++;
8596
+ track(
8597
+ await Product.create({
8598
+ name: `Sample ${i}`,
8599
+ price: Math.random() * 100,
8600
+ }),
8601
+ );
8222
8602
  }
8223
- return { recordsCreated };
8224
8603
  },
8225
8604
  });
8226
8605
  ```
@@ -8250,17 +8629,20 @@ warlock seed # run all auto-discovered seeds
8250
8629
  warlock seed --list # show registry (name, order, enabled), don't run
8251
8630
  warlock seed --path=<file> # run one file by absolute or relative path
8252
8631
  warlock seed --fresh # truncate every table first, then run all
8632
+ warlock seed --drop # undo every tracked record, reset the seeds-log
8633
+ warlock seed --drop=<name> # undo just one seeder's tracked records
8253
8634
  warlock seed --transaction # (default true) — pass --transaction=false to skip wrapping
8254
8635
  ```
8255
8636
 
8256
- `--fresh` is **destructive** — it truncates all tables, including the `seeds` tracking table. Treat it as "reset the dev DB," not "run pending seeds."
8637
+ `--fresh` is **destructive** — it truncates all tables, including the `seeds` tracking table. Treat it as "reset the dev DB," not "run pending seeds." `--drop` is the surgical undo — only tracked rows, scoped to one seeder if you name it.
8257
8638
 
8258
8639
  ## Gotchas
8259
8640
 
8260
8641
  - **The seed file must default-export.** A named export gets the discovery error at boot. Wrap with `export default seeder({...})`.
8261
8642
  - **`once: true` only works if the seed run succeeds.** A throw rolls back the transaction including the `seeds` table insert — the next run will retry.
8262
- - **`dependsOn` is documented but not enforced yet.** Use `order` until topological sort lands (`TODO` in source).
8263
- - **`batchSize` is documented on the type but unused by the manager.** It's a forward-compat field. If you need batch inserts, batch inside `run()` directly.
8643
+ - **`--drop` only undoes what you `track()`.** A seed that never calls `track()` has nothing in `seed_records`, so `--drop` can't reverse it. Track every row you want to be able to undo.
8644
+ - **`dependsOn` is resolved** (topological sort over `order`). An unknown dependency throws `UnknownSeederDependencyError`; a cycle throws `SeederDependencyCycleError`. A `dependsOn` pointing at a `enabled: false` seeder errors, because disabled seeds are filtered out before resolution.
8645
+ - **`batchSize` is surfaced into the run context, not acted on by the manager.** Set `batchSize` on the seeder and the manager hands the value through as `run({ batchSize })` (defaulting to `0`); forward it to `Model.createMany(rows, { batchSize })` to get chunked inserts. The manager itself never chunks — it only relays the value.
8264
8646
  - **`--fresh` truncates `cascade: true`.** Foreign-key chains delete with their parents. If you have data outside the seeded scope that you want to keep, do NOT run `--fresh`.
8265
8647
  - **Seeds aren't migrations.** Schema changes go in `migrations/`. Seeds populate data into a schema that already exists. Mixing the two (creating a table in a seed) confuses the lifecycle.
8266
8648
  - **`enabled: false` skips the seed but it's still in the registry.** `warlock seed --list` shows it as disabled. Re-enable by flipping the flag, not by deleting the file.