@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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: write-seeder
3
- 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`.'
3
+ 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`.'
4
4
  ---
5
5
 
6
6
  # Warlock — write a seeder
@@ -18,22 +18,21 @@ export default seeder({
18
18
  description: "Insert admin / member / viewer roles",
19
19
  once: true,
20
20
  order: 1,
21
- async run() {
22
- let recordsCreated = 0;
23
-
21
+ async run({ track }) {
24
22
  for (const slug of ["admin", "member", "viewer"]) {
25
23
  const existing = await Role.first({ slug });
26
24
  if (existing) continue;
27
25
 
28
- await Role.create({ slug, name: slug.toUpperCase() });
29
- recordsCreated++;
26
+ // track() records the created row in `seed_records` (same transaction)
27
+ // AND returns its argument so you can chain inline.
28
+ track(await Role.create({ slug, name: slug.toUpperCase() }));
30
29
  }
31
-
32
- return { recordsCreated };
33
30
  },
34
31
  });
35
32
  ```
36
33
 
34
+ `recordsCreated` is **auto-derived from the track count** — you no longer have to count by hand or return a `SeedResult`.
35
+
37
36
  Run them:
38
37
 
39
38
  ```bash
@@ -41,10 +40,14 @@ yarn warlock seed # discover + run all
41
40
  yarn warlock seed --list # show registry, don't run
42
41
  yarn warlock seed --path=src/app/roles/seeds/default-roles.seed.ts # one file
43
42
  yarn warlock seed --fresh # truncate every table first, then run all
43
+ yarn warlock seed --drop # undo every tracked record, reset the log
44
+ yarn warlock seed --drop=default-roles # undo just one seeder's records
44
45
  ```
45
46
 
46
47
  `--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.
47
48
 
49
+ `--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).
50
+
48
51
  ## The `Seeder` type
49
52
 
50
53
  ```ts
@@ -56,15 +59,69 @@ type Seeder = {
56
59
  once?: boolean; // skip if a row exists in the `seeds` table
57
60
  order?: number; // sort key — lower runs first
58
61
  batchSize?: number; // documented; not enforced by the manager
59
- run(): Promise<SeedResult | void>; // your work
62
+ run(ctx: SeedContext): Promise<SeedResult | void>; // your work
60
63
  };
61
64
 
65
+ type SeedContext = {
66
+ track: Track; // register created records for --drop
67
+ now: SeedClock; // injectable clock — () => Date, default () => new Date()
68
+ batchSize: number; // from the seeder's batchSize (0 when unset)
69
+ };
70
+
71
+ type SeedClock = () => Date;
72
+
62
73
  type SeedResult = {
63
- recordsCreated: number; // tracked in the seeds table
74
+ recordsCreated: number; // OPTIONAL fallback when nothing was tracked
64
75
  };
65
76
  ```
66
77
 
67
- 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`).
78
+ `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`).
79
+
80
+ ## `now` — the injectable clock
81
+
82
+ `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.
83
+
84
+ ```ts
85
+ async run({ track, now }) {
86
+ // every timestamp in the run reads from one clock
87
+ track(await User.create({ name: "Admin", created_at: now() }));
88
+ }
89
+ ```
90
+
91
+ 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.
92
+
93
+ ## `batchSize` — bulk-insert hint
94
+
95
+ `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:
96
+
97
+ ```ts
98
+ async run({ track, batchSize }) {
99
+ const rows = buildManyRows();
100
+ track(await User.createMany(rows, { batchSize })); // chunked insert
101
+ }
102
+ ```
103
+
104
+ The manager doesn't act on `batchSize` itself — it only hands the value through; the chunking happens inside `createMany`.
105
+
106
+ ## `track` — register created records
107
+
108
+ `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:
109
+
110
+ ```ts
111
+ track(model) // single model — reads model.getTableName() + model.id
112
+ track([modelA, modelB]) // bulk — array of models
113
+ track("legacy_table", id) // raw escape hatch — table name + id, no model
114
+ ```
115
+
116
+ ```ts
117
+ async run({ track }) {
118
+ const role = track(await Role.create({ slug: "admin" })); // chain inline
119
+ const users = track(await User.createMany([...])); // bulk
120
+ track("audit_log", auditId); // raw
121
+ }
122
+ ```
123
+
124
+ 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.
68
125
 
69
126
  ## Auto-discovery
70
127
 
@@ -108,6 +165,34 @@ Skip-on-`once`:
108
165
 
109
166
  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).
110
167
 
168
+ ## Undo — `seed_records` + `--drop`
169
+
170
+ Every record you `track()` is written to a `seed_records` table (created on first run via `SeedRecordsTableMigration`):
171
+
172
+ | Column | Meaning |
173
+ | ---------- | ---------------------------------------- |
174
+ | `seeder` | Owning seeder's `name` |
175
+ | `table` | Table the tracked record lives in |
176
+ | `recordId` | Primary key of the tracked record |
177
+ | `runAt` | Wall time the reference was recorded |
178
+
179
+ `warlock seed --drop` reads those refs and undoes the seed:
180
+
181
+ ```bash
182
+ yarn warlock seed --drop # undo every tracked record across all seeders
183
+ yarn warlock seed --drop=default-roles # undo just one seeder's records
184
+ ```
185
+
186
+ What it does, inside a single transaction:
187
+
188
+ 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).
189
+ 2. Clears the `seed_records` refs it acted on.
190
+ 3. Resets the matching `seeds`-log rows, so a `once: true` seed **re-runs** on the next `warlock seed`.
191
+
192
+ 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.
193
+
194
+ `--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.
195
+
111
196
  ## Ordering
112
197
 
113
198
  ```ts
@@ -126,19 +211,34 @@ seeder({
126
211
 
127
212
  The manager sorts by `order` ascending — lower runs first. Seeds without an `order` get sorted to the end (`Number.MAX_SAFE_INTEGER`).
128
213
 
129
- `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.
214
+ ### `dependsOn` — topological order
215
+
216
+ `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.
217
+
218
+ ```ts
219
+ seeder({ name: "roles", order: 20, async run() { /* ... */ } });
220
+ seeder({ name: "admin-user", dependsOn: ["roles"], order: 10, async run() { /* ... */ } });
221
+ // → roles runs FIRST despite its higher order, because admin-user depends on it.
222
+ ```
223
+
224
+ Two ways it fails loudly:
225
+
226
+ - **Unknown dependency** — `dependsOn` names a seeder that isn't registered (or is `enabled: false`, since disabled seeds are filtered out *before* resolution) → `UnknownSeederDependencyError`.
227
+ - **Cycle** — `a → b → a` → `SeederDependencyCycleError` (the message includes the cycle path).
228
+
229
+ Use `dependsOn` to express *hard* ordering constraints and `order` for soft sequencing within a dependency layer.
130
230
 
131
231
  ## Transactions
132
232
 
133
- By default, each seed runs inside a transaction:
233
+ 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:
134
234
 
135
235
  ```ts
136
236
  const result = withTransaction
137
- ? await transaction(async () => seeder.run())
138
- : await seeder.run();
237
+ ? await transaction(runSeeder) // run() + seed_records writes, atomically
238
+ : await runSeeder();
139
239
  ```
140
240
 
141
- 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).
241
+ 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).
142
242
 
143
243
  Within a single seed, multiple model `create()` / `insert()` calls share the same transaction — partial-on-error doesn't apply unless you opt out.
144
244
 
@@ -154,7 +254,7 @@ export default seeder({
154
254
  name: "default-currencies",
155
255
  description: "USD / EUR / SAR / EGP currencies",
156
256
  order: 5,
157
- async run() {
257
+ async run({ track }) {
158
258
  const seeds = [
159
259
  { code: "USD", name: "US Dollar", decimals: 2 },
160
260
  { code: "EUR", name: "Euro", decimals: 2 },
@@ -162,15 +262,11 @@ export default seeder({
162
262
  { code: "EGP", name: "Egyptian Pound", decimals: 2 },
163
263
  ];
164
264
 
165
- let recordsCreated = 0;
166
265
  for (const data of seeds) {
167
266
  const existing = await Currency.first({ code: data.code });
168
267
  if (existing) continue;
169
- await Currency.create(data);
170
- recordsCreated++;
268
+ track(await Currency.create(data)); // count auto-derived from track()
171
269
  }
172
-
173
- return { recordsCreated };
174
270
  },
175
271
  });
176
272
  ```
@@ -188,23 +284,23 @@ export default seeder({
188
284
  name: "default-admin",
189
285
  description: "Insert the seed admin user",
190
286
  once: true, // never re-run
191
- order: 20, // after roles (order: 10)
192
- async run() {
287
+ dependsOn: ["default-roles"], // roles must exist first
288
+ async run({ track }) {
193
289
  const adminRole = await Role.first({ slug: "admin" });
194
- if (!adminRole) throw new Error("admin role missing — run user-roles seed first");
195
-
196
- await User.create({
197
- email: "admin@example.com",
198
- password: "change-me-immediately",
199
- role_id: adminRole.id,
200
- });
201
-
202
- return { recordsCreated: 1 };
290
+ if (!adminRole) throw new Error("admin role missing — run the roles seed first");
291
+
292
+ track(
293
+ await User.create({
294
+ email: "admin@example.com",
295
+ password: "change-me-immediately",
296
+ role_id: adminRole.id,
297
+ }),
298
+ );
203
299
  },
204
300
  });
205
301
  ```
206
302
 
207
- Combining `once: true` + `order` is the canonical "starter data" pattern.
303
+ Combining `once: true` + `dependsOn` is the canonical "starter data" pattern.
208
304
 
209
305
  ### Bulk dev fixtures (no `once`, large batch)
210
306
 
@@ -217,16 +313,15 @@ export default seeder({
217
313
  description: "100 sample products for dev",
218
314
  enabled: process.env.NODE_ENV === "development",
219
315
  order: 100,
220
- async run() {
221
- let recordsCreated = 0;
316
+ async run({ track }) {
222
317
  for (let i = 0; i < 100; i++) {
223
- await Product.create({
224
- name: `Sample ${i}`,
225
- price: Math.random() * 100,
226
- });
227
- recordsCreated++;
318
+ track(
319
+ await Product.create({
320
+ name: `Sample ${i}`,
321
+ price: Math.random() * 100,
322
+ }),
323
+ );
228
324
  }
229
- return { recordsCreated };
230
325
  },
231
326
  });
232
327
  ```
@@ -256,17 +351,20 @@ warlock seed # run all auto-discovered seeds
256
351
  warlock seed --list # show registry (name, order, enabled), don't run
257
352
  warlock seed --path=<file> # run one file by absolute or relative path
258
353
  warlock seed --fresh # truncate every table first, then run all
354
+ warlock seed --drop # undo every tracked record, reset the seeds-log
355
+ warlock seed --drop=<name> # undo just one seeder's tracked records
259
356
  warlock seed --transaction # (default true) — pass --transaction=false to skip wrapping
260
357
  ```
261
358
 
262
- `--fresh` is **destructive** — it truncates all tables, including the `seeds` tracking table. Treat it as "reset the dev DB," not "run pending seeds."
359
+ `--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.
263
360
 
264
361
  ## Gotchas
265
362
 
266
363
  - **The seed file must default-export.** A named export gets the discovery error at boot. Wrap with `export default seeder({...})`.
267
364
  - **`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.
268
- - **`dependsOn` is documented but not enforced yet.** Use `order` until topological sort lands (`TODO` in source).
269
- - **`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.
365
+ - **`--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.
366
+ - **`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.
367
+ - **`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.
270
368
  - **`--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`.
271
369
  - **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.
272
370
  - **`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.