@iceinvein/agent-skills 0.1.39 → 0.2.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 (148) hide show
  1. package/README.md +18 -2
  2. package/dist/cli/index.js +105 -28
  3. package/package.json +1 -1
  4. package/skills/index.json +14 -2
  5. package/skills/magpie/README.md +2 -1
  6. package/skills/magpie/SKILL.md +138 -45
  7. package/skills/magpie/bin/magpie.ts +43 -0
  8. package/skills/magpie/fixtures/example-pr/brief.json +18 -0
  9. package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
  10. package/skills/magpie/fixtures/fake-gh.sh +14 -0
  11. package/skills/magpie/package.json +1 -1
  12. package/skills/magpie/references/peer-review.md +7 -2
  13. package/skills/magpie/references/scout.md +90 -0
  14. package/skills/magpie/references/specialists.md +117 -6
  15. package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
  16. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
  17. package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
  18. package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
  19. package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
  20. package/skills/magpie/scripts/__tests__/gh.test.ts +90 -0
  21. package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
  22. package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
  23. package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
  24. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
  25. package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
  26. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +173 -1
  27. package/skills/magpie/scripts/__tests__/render-findings.test.ts +151 -1
  28. package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
  29. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
  30. package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
  31. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +207 -7
  32. package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
  33. package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
  34. package/skills/magpie/scripts/diff-chunks.ts +28 -0
  35. package/skills/magpie/scripts/findings-files.ts +32 -0
  36. package/skills/magpie/scripts/gh.ts +68 -14
  37. package/skills/magpie/scripts/git-diff.ts +111 -0
  38. package/skills/magpie/scripts/path-filter.ts +9 -5
  39. package/skills/magpie/scripts/preview-cmd.ts +11 -1
  40. package/skills/magpie/scripts/refresh.ts +32 -3
  41. package/skills/magpie/scripts/render-cmd.ts +35 -11
  42. package/skills/magpie/scripts/render-findings.ts +78 -2
  43. package/skills/magpie/scripts/render-progress.ts +6 -1
  44. package/skills/magpie/scripts/setup-cmd.ts +38 -1
  45. package/skills/magpie/scripts/shard.ts +171 -0
  46. package/skills/magpie/scripts/status-cmd.ts +7 -4
  47. package/skills/magpie/scripts/types.ts +50 -0
  48. package/skills/magpie/skill.json +2 -2
  49. package/skills/magpie/templates/styles.css +66 -0
  50. package/skills/migrate/README.md +194 -0
  51. package/skills/migrate/SKILL.md +197 -0
  52. package/skills/migrate/bin/migrate +15 -0
  53. package/skills/migrate/bin/migrate.ts +309 -0
  54. package/skills/migrate/biome.json +35 -0
  55. package/skills/migrate/bun.lock +24 -0
  56. package/skills/migrate/docs/architecture.md +294 -0
  57. package/skills/migrate/docs/reference.md +590 -0
  58. package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
  59. package/skills/migrate/fixtures/tiny-express/app.js +29 -0
  60. package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
  61. package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
  62. package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
  63. package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
  64. package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
  65. package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
  66. package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
  67. package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
  68. package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
  69. package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
  70. package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
  71. package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
  72. package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
  73. package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
  74. package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
  75. package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
  76. package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
  77. package/skills/migrate/install.sh +68 -0
  78. package/skills/migrate/package.json +17 -0
  79. package/skills/migrate/references/phases/enumerate.md +291 -0
  80. package/skills/migrate/references/phases/extract.md +652 -0
  81. package/skills/migrate/references/phases/parity.md +275 -0
  82. package/skills/migrate/references/phases/probe.md +135 -0
  83. package/skills/migrate/references/phases/queue.md +242 -0
  84. package/skills/migrate/references/phases/seam.md +416 -0
  85. package/skills/migrate/references/recipes/README.md +116 -0
  86. package/skills/migrate/references/recipes/aspnet.md +287 -0
  87. package/skills/migrate/references/run-ops.md +280 -0
  88. package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
  89. package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
  90. package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
  91. package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
  92. package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
  93. package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
  94. package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
  95. package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
  96. package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
  97. package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
  98. package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
  99. package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
  100. package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
  101. package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
  102. package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
  103. package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
  104. package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
  105. package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
  106. package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
  107. package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
  108. package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
  109. package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
  110. package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
  111. package/skills/migrate/scripts/census-cmd.ts +109 -0
  112. package/skills/migrate/scripts/census.ts +342 -0
  113. package/skills/migrate/scripts/check-cmd.ts +24 -0
  114. package/skills/migrate/scripts/check.ts +376 -0
  115. package/skills/migrate/scripts/citations.ts +92 -0
  116. package/skills/migrate/scripts/config.ts +237 -0
  117. package/skills/migrate/scripts/ids.ts +31 -0
  118. package/skills/migrate/scripts/import-cmd.ts +141 -0
  119. package/skills/migrate/scripts/init-cmd.ts +118 -0
  120. package/skills/migrate/scripts/leaks.ts +184 -0
  121. package/skills/migrate/scripts/lock.ts +188 -0
  122. package/skills/migrate/scripts/paths.ts +103 -0
  123. package/skills/migrate/scripts/phase-cmd.ts +63 -0
  124. package/skills/migrate/scripts/phases.ts +113 -0
  125. package/skills/migrate/scripts/queue-cmd.ts +98 -0
  126. package/skills/migrate/scripts/queue.ts +258 -0
  127. package/skills/migrate/scripts/report-cmd.ts +47 -0
  128. package/skills/migrate/scripts/report.ts +131 -0
  129. package/skills/migrate/scripts/reset-cmd.ts +120 -0
  130. package/skills/migrate/scripts/status-cmd.ts +52 -0
  131. package/skills/migrate/scripts/store.ts +159 -0
  132. package/skills/migrate/scripts/types.ts +137 -0
  133. package/skills/migrate/scripts/validate.ts +221 -0
  134. package/skills/migrate/skill.json +33 -0
  135. package/skills/migrate/templates/config.toml +27 -0
  136. package/skills/migrate/templates/queue-item.md +17 -0
  137. package/skills/migrate/tsconfig.json +18 -0
  138. package/skills/migrate/uninstall.sh +31 -0
  139. package/skills/sluice/SKILL.md +82 -0
  140. package/skills/sluice/references/deep-channel.md +94 -0
  141. package/skills/sluice/references/finish.md +35 -0
  142. package/skills/sluice/references/intent.md +29 -0
  143. package/skills/sluice/references/review.md +42 -0
  144. package/skills/sluice/references/root-cause.md +38 -0
  145. package/skills/sluice/references/show-or-say.md +36 -0
  146. package/skills/sluice/references/test-first.md +35 -0
  147. package/skills/sluice/references/verify.md +26 -0
  148. package/skills/sluice/skill.json +32 -0
@@ -0,0 +1,590 @@
1
+ # migrate reference
2
+
3
+ Everything you need to drive the CLI without reading its source: the file
4
+ formats it accepts, the grammars inside them, what each gate enforces, and what
5
+ the exit codes mean.
6
+
7
+ For what the tool is and how to install it, see [../README.md](../README.md).
8
+ For how it is built and how to extend it, see [architecture.md](architecture.md).
9
+
10
+ ## Conventions
11
+
12
+ **Exit codes.** Every command uses the same four.
13
+
14
+ | Code | Meaning |
15
+ |---|---|
16
+ | `0` | Success |
17
+ | `1` | A content or domain failure in a well-formed request. The request was serviceable and the answer is no: a gate found violations, a census does not balance, a queue file is unparseable. |
18
+ | `2` | A malformed or unusable request. The command could not begin: a missing flag value, an unknown phase, a file that is absent or not valid JSON, no store above the cwd, a config that will not load. |
19
+ | `3` | The store lock is unavailable: another process holds it, a holder looks stale, or the lock file has failed to parse across five consecutive reads, and force-unlocking was not requested. `import`, `census`, `phase --status`, and `reset` can return this; retry, or pass `--force-unlock` once you have confirmed no other agent is writing. |
20
+
21
+ The split matters because an orchestrating agent should be able to tell "your
22
+ generator is broken" from "your numbers are wrong" from "try again" without
23
+ parsing stderr.
24
+
25
+ **Field names are `snake_case` on disk** (`found_by`, `in_ledger`,
26
+ `as_requirements`, `owner_signed`, `parity_exclusion`). The TypeScript types
27
+ mirror them exactly so there is no serialization layer.
28
+
29
+ **Nothing writes into the source tree.** Every writer refuses a path that
30
+ resolves inside `source.path`, following symlinks and case-insensitive volumes,
31
+ and exits 2.
32
+
33
+ **The store lock.** `import`, `census`, `phase --status`, and `reset` each hold
34
+ one lock (`.migrate/.lock`) across their read-modify-write of the store, so two
35
+ agents writing at once cannot silently drop each other's rows. A waiting caller polls
36
+ with backoff for up to 30 seconds by default. A holder confirmed no longer
37
+ running is reported as stale rather than waited out further; so is a lock file
38
+ that fails to parse on five consecutive reads (a lock file that is merely
39
+ missing, or momentarily empty between creation and its holder record being
40
+ written, does not count toward that). `--force-unlock` removes a lock believed
41
+ stale, after you have confirmed no other agent is actually writing. A lock
42
+ failure is exit `3`, not `1` or `2`, because the request itself is fine and
43
+ would likely succeed on retry.
44
+
45
+ ## Batch files
46
+
47
+ `migrate import` is the only supported way rows enter `elements.jsonl`,
48
+ `requirements.jsonl`, or `deltas.jsonl`. `census.jsonl` rows are written
49
+ directly by `migrate census` instead (see Census records, below), and
50
+ `capabilities.jsonl`, `seam.json`, and `seam.md` have no CLI writer that
51
+ authors their content at all; they are hand-written, and `migrate reset
52
+ --phase seam` only clears or deletes them (see Seam artifacts, below).
53
+ `migrate import` takes one JSON file holding an envelope and an array of
54
+ rows:
55
+
56
+ ```json
57
+ {
58
+ "batch": "b-routes-code-001",
59
+ "phase": "enumerate",
60
+ "rows": [ ... ]
61
+ }
62
+ ```
63
+
64
+ - `batch` is your own id for this batch. It is written onto every row and
65
+ recorded in `phases.json`, which is what makes a crashed run resumable.
66
+ - `phase` is one of `probe`, `enumerate`, `seam`, `extract`, `parity`, `queue`,
67
+ `adjudicate`, `handoff`.
68
+ - Do not put a `batch` field on the rows themselves. The importer sets it.
69
+
70
+ **Import is all or nothing.** One invalid row means nothing is written, because
71
+ a partially-written batch is a store nobody can reason about on resume: the
72
+ batch id would claim rows that are not all there.
73
+
74
+ **Re-importing the same batch is safe.** Rows upsert by id, keeping their
75
+ original position, so a re-run after a crash updates rather than duplicates. A
76
+ repeated id *within a single batch* is rejected, since that is an authoring
77
+ error that would silently discard a row.
78
+
79
+ ### Elements
80
+
81
+ `migrate import elements batch.json`
82
+
83
+ ```json
84
+ {
85
+ "id": "route-get-api-users",
86
+ "surface": "routes",
87
+ "element": "GET /api/users",
88
+ "found_by": ["code", "nav"],
89
+ "disposition": { "kind": "unaccounted" },
90
+ "refs": [
91
+ { "kind": "src", "path": "Controllers/UsersController.cs", "lines": [42, 58] }
92
+ ],
93
+ "lens": "code",
94
+ "notes": ""
95
+ }
96
+ ```
97
+
98
+ | Field | Rule |
99
+ |---|---|
100
+ | `id` | `<singular>-<slug>`. The singular comes from the surface name, which by default is the surface with a trailing `s` stripped, overridable per surface via `[surfaces.singular]` in config. The slug is lowercase kebab-case. |
101
+ | `surface` | Must be one of the types declared in `[surfaces].types`. |
102
+ | `element` | Free text naming the thing. Non-empty. |
103
+ | `found_by` | One or more of `code`, `nav`, `docs`, `runtime`. |
104
+ | `lens` | The single lens that produced this row, same vocabulary. |
105
+ | `disposition` | See below. Starts `unaccounted`. |
106
+ | `refs` | Array of refs, may be empty. |
107
+ | `notes` | Free text, may be empty. |
108
+
109
+ Because element ids derive from the surface, a surface that is already singular
110
+ but ends in `s` (for example `status`) needs an explicit
111
+ `[surfaces.singular]` override, or ids will be built from `statu`.
112
+
113
+ ### Requirements
114
+
115
+ `migrate import reqs batch.json`
116
+
117
+ ```json
118
+ {
119
+ "id": "UM-001",
120
+ "cap": "user-management",
121
+ "requirement": "User must provide a valid email and password to log in",
122
+ "actors": "User",
123
+ "objects": "Credentials",
124
+ "rules": "Email validated per RFC 5322, password minimum 8 characters",
125
+ "origin": "intended",
126
+ "confidence": { "kind": "confirmed" },
127
+ "citations": [
128
+ { "kind": "ledger", "id": "route-post-api-login" },
129
+ { "kind": "src", "path": "Controllers/AuthController.cs", "lines": [20, 35] }
130
+ ],
131
+ "parity": { "kind": "rubric", "level": "high" }
132
+ }
133
+ ```
134
+
135
+ | Field | Rule |
136
+ |---|---|
137
+ | `id` | Free-form, conventionally `<NS>-<number>`. Must be unique across the store. |
138
+ | `cap` | Must match a `slug` in `capabilities.jsonl`. |
139
+ | `requirement` | Non-empty. |
140
+ | `actors`, `objects`, `rules` | Free text, default to `-` if omitted. |
141
+ | `origin` | `intended` or `accidental-candidate`. |
142
+ | `confidence` | See below. |
143
+ | `citations` | **At least one required.** This is the never-fabricate rule made structural. |
144
+ | `parity` | See below, or `null` while unplanned. |
145
+
146
+ ### Deltas
147
+
148
+ `migrate import deltas batch.json`
149
+
150
+ ```json
151
+ {
152
+ "id": "delta-multi-tenancy",
153
+ "scope": "All database tables",
154
+ "rationale": "SaaS model requires tenant isolation",
155
+ "parity_exclusion": "Schema comparisons ignore the TenantId column",
156
+ "validation": "Cross-tenant leak tests prove isolation",
157
+ "owner_signed": null
158
+ }
159
+ ```
160
+
161
+ `id` must start with `delta-`. All four text fields are required and non-empty.
162
+ `owner_signed` is a date string or `null`; the gate fails while any delta is
163
+ unsigned, which is what stops exclusions accreting silently.
164
+
165
+ ### Capabilities
166
+
167
+ `capabilities.jsonl` has **no import path in this milestone**. Write it
168
+ directly, one JSON object per line:
169
+
170
+ ```json
171
+ {"slug": "user-management", "title": "User Management", "ns": "UM", "elements": []}
172
+ ```
173
+
174
+ Because hand-editing is the only route, the gate checks for duplicate slugs
175
+ explicitly.
176
+
177
+ ### Seam artifacts
178
+
179
+ `seam.json` and `seam.md` have **no import path either, and no gate at
180
+ all**: `check.ts` contains no mention of either file, so nothing checks
181
+ their shape, their content, or that they even exist. Both are hand-written
182
+ run-level records of how the seam phase (`references/phases/seam.md`)
183
+ reached its partition, trusted entirely on the strength of whoever wrote
184
+ them, the same as `parity-basis.md` in phase 0.
185
+
186
+ `seam.md` is prose: every validator's script and its raw output, verbatim,
187
+ so a reviewer can retrace exactly what ran and what it found. `seam.json`
188
+ is the structured summary of that same run: which validators ran, the
189
+ modularity figure, and the resulting status. Its shape is derived from
190
+ what `seam.md` actually instructs recording, not from a separate schema:
191
+
192
+ ```json
193
+ {
194
+ "validators": {
195
+ "schema-clustering": { "ran": false, "reason": "no relational schema" },
196
+ "call-graph": { "ran": false, "reason": "code is not statically parseable" },
197
+ "change-coupling": { "ran": false, "reason": "no VCS history" },
198
+ "surface-affinity": { "ran": true, "modularity": 0.5 }
199
+ },
200
+ "agreement": ["surface-affinity"],
201
+ "modularity": 0.5,
202
+ "status": "accepted"
203
+ }
204
+ ```
205
+
206
+ A validator that did not run names why, in the same free-text style as a
207
+ lens's `not-applicable` reason in `enumerate.md`. `agreement` names which
208
+ validators' partitions were accepted together (two, under the ordinary
209
+ triangulation rule) or the single validator licensed by the one-validator
210
+ exception when fewer than two could run at all. An escalated run instead
211
+ of an accepted one looks like this, `modularity` reflecting the best
212
+ candidate considered rather than an accepted one:
213
+
214
+ ```json
215
+ {
216
+ "validators": {
217
+ "schema-clustering": { "ran": false, "reason": "no relational schema" },
218
+ "call-graph": { "ran": false, "reason": "code is not statically parseable" },
219
+ "change-coupling": { "ran": false, "reason": "no VCS history" },
220
+ "surface-affinity": { "ran": true, "modularity": 0.21 }
221
+ },
222
+ "agreement": [],
223
+ "modularity": 0.21,
224
+ "status": "escalated",
225
+ "queue": "q-seam-low-modularity"
226
+ }
227
+ ```
228
+
229
+ Nothing parses or checks either shape above; both are illustrative of the
230
+ convention, not a contract any command enforces. Treat what you write here
231
+ with the same care as `capabilities.jsonl` gets from the gate that does
232
+ exist, since here there is no gate at all standing behind it.
233
+
234
+ ## Grammars
235
+
236
+ These discriminated unions appear inside rows. The `kind` field selects the
237
+ variant; unknown kinds are rejected.
238
+
239
+ **Ref** (used by `refs` and `citations`)
240
+
241
+ ```json
242
+ { "kind": "src", "path": "Controllers/Auth.cs", "lines": [20, 35] }
243
+ { "kind": "ledger", "id": "route-post-api-login" }
244
+ { "kind": "doc", "path": "docs/user-guide.pdf", "note": "page 37" }
245
+ { "kind": "observed", "host": "legacy.example.com", "path": "/Settings", "behavior": "toggle renders" }
246
+ ```
247
+
248
+ `lines` is optional; when present it must be `[start, end]` with `start <= end`.
249
+ Only `src` refs are resolved against the source tree, and only by the
250
+ `citations` gate (on by default; skipped under `--no-citations`). A `ledger`
251
+ entry inside a requirement's `citations` is resolved by the `refs` gate,
252
+ which checks it names a real element. The same `{"kind": "ledger", ...}`
253
+ shape inside an element's own `refs` is a different case entirely: nothing
254
+ checks it, on either end, at any phase. `refs` and `citations` share this
255
+ grammar but not this enforcement; `references/phases/enumerate.md`'s
256
+ Procedure (step 4) and `references/phases/seam.md` explain why an
257
+ element's `refs` is exempt, and what depends on it anyway.
258
+
259
+ **Disposition** (on elements)
260
+
261
+ ```json
262
+ { "kind": "unaccounted" }
263
+ { "kind": "mapped", "fr": "UM-001" }
264
+ { "kind": "out-of-scope", "queue": "q-legacy-admin-tool" }
265
+ ```
266
+
267
+ **Confidence** (on requirements)
268
+
269
+ ```json
270
+ { "kind": "confirmed" }
271
+ { "kind": "inferred" }
272
+ { "kind": "queued", "queue": "q-invoice-batch-scope" }
273
+ ```
274
+
275
+ **Parity** (on requirements, or `null`)
276
+
277
+ ```json
278
+ { "kind": "golden-master", "ref": "tests/parity/users/create.test.ts" }
279
+ { "kind": "differential", "ref": "tests/parity/users/list.test.ts" }
280
+ { "kind": "rubric", "level": "high" }
281
+ { "kind": "rubric", "level": "moderate", "queue": "q-parity-um-042" }
282
+ ```
283
+
284
+ `rubric:high` needs no queue id. `moderate`, `low` and `unknown` each require
285
+ one, which is how a sub-high confidence claim stays attached to an owner
286
+ decision.
287
+
288
+ **Queue ids** are `q-` followed by a lowercase kebab-case slug. This is a format
289
+ constraint rather than a blocklist: an uppercase letter, a stray space or
290
+ trailing punctuation are all rejected because none produces a well-formed slug.
291
+
292
+ ## Census records
293
+
294
+ `migrate census record.json` takes a single JSON object, not an envelope. A
295
+ census record is a lens closing its own arithmetic, and the CLI checks the sum
296
+ rather than trusting it.
297
+
298
+ Re-recording the same subject **replaces** its record rather than adding a
299
+ second, so the gate never sees two answers for one subject. Subject identity is
300
+ `lens:<surface>`, `attribute:<subject>`, `rule-sweep:<subject>` or
301
+ `closer:<closer>`.
302
+
303
+ Within `skipped` and `queued`, duplicates are rejected, otherwise an imbalanced
304
+ record could be padded into passing by repeating an entry. The two compare
305
+ differently, because they hold different kinds of string. A `skipped` element
306
+ name is free text, so it is compared after trimming and case folding, and
307
+ `" ORDERS "` beside `"orders"` is refused as the same entry twice. A `queued`
308
+ id has a format, `q-` followed by a lowercase slug, and a case or whitespace
309
+ variant of one never reaches the duplicate check at all: it is rejected a step
310
+ earlier as malformed, by index (`queued[1] must be a valid queue id: q-
311
+ followed by a lowercase slug`). Every id that does reach the duplicate check is
312
+ therefore already canonical, and comparing those exactly is enough. A `skipped`
313
+ element whose text matches a `queued` id is caught by its own separate check,
314
+ on the same normalized form the `skipped` side uses.
315
+
316
+ **`phase` is required on every kind**, one of the eight phase names (see below).
317
+ A `lens` record must declare `enumerate`; a `closer` record must declare
318
+ `extract`. Those are the only two phases the run-state gate ever looks in: a
319
+ lens's `batch` is checked against `phases.enumerate.batches` and a closer's
320
+ against `phases.extract.batches`, both hardcoded in `check.ts`, never read off
321
+ the record's own `phase` value. `validateCensus` rejects any other phase
322
+ declared on those two kinds, by name, so a record cannot name a batch the gate
323
+ will never find; the writer and the gate agree by construction rather than by
324
+ convention that happens to hold. `attribute` and `rule-sweep` records still
325
+ require a non-empty `phase`, but its value is free, since the gate never
326
+ cross-checks either kind against a batch list.
327
+
328
+ **Recording a census commits its batch before writing the census row, not
329
+ after.** `migrate census` calls `recordBatch` first, then writes
330
+ `census.jsonl`. If the process is interrupted in between, the batch is
331
+ committed with no census row behind it. That is the safer of the two possible
332
+ half-writes: an orphan batch entry is inert, since nothing treats
333
+ `phases.json`'s batch list as meaningful except as corroboration for a census
334
+ row that is also expected to exist. The other order would be worse: an orphan
335
+ census row naming a batch that was never actually committed would defeat the
336
+ exact guarantee the run-state gate exists to provide.
337
+
338
+ **`directions` (on `lens` and `attribute`) maps each direction name to an object,
339
+ not a bare number:** `{ "count": <non-negative integer>, "evidence": "<the
340
+ command or method that produced this count>" }`. The old `{"ddl": 43}` shape from
341
+ Milestone 1 no longer validates; it is rejected by name, pointing at the
342
+ bare-count field, so a hand-edited record written against the old shape fails
343
+ loudly rather than silently passing with `evidence` absent. A record needs at
344
+ least two independent directions; the lens contract does not admit a
345
+ single-direction enumeration.
346
+
347
+ ### lens
348
+
349
+ One per declared surface. This is the record the coverage claim rests on.
350
+
351
+ ```json
352
+ {
353
+ "kind": "lens",
354
+ "surface": "tables",
355
+ "phase": "enumerate",
356
+ "directions": {
357
+ "ddl": { "count": 43, "evidence": "grep CREATE TABLE across *.sql" },
358
+ "orm": { "count": 40, "evidence": "grep DbSet<> in the DbContext" }
359
+ },
360
+ "total": 45,
361
+ "in_ledger": 44,
362
+ "added": 1,
363
+ "skipped": [{ "element": "__EFMigrationsHistory", "reason": "framework-owned" }],
364
+ "queued": ["q-table-ownership-personbookinggroup"],
365
+ "batch": "b-tables-census-001"
366
+ }
367
+ ```
368
+
369
+ Balance: `total == in_ledger + added + skipped.length + queued.length`.
370
+
371
+ Additionally, `in_ledger + added` is **reconciled against the store**: it must
372
+ equal the number of elements actually carrying that surface. `total` counts what
373
+ exists in the legacy source and cannot be corroborated, but the claim about how
374
+ many rows reached the ledger is directly countable, so it is counted.
375
+
376
+ `total` is also **bounded by the directions**: `max(directions) <= total <=
377
+ sum(directions)`, since a deduped union can never be smaller than its largest
378
+ input or larger than their concatenation. In the example, `max(43, 40) = 43` and
379
+ `sum(43, 40) = 83`, so any `total` from 43 through 83 is arithmetically possible;
380
+ 45 is. This bounds `total` without corroborating it: nothing on this side of the
381
+ source can confirm the legacy system really has exactly 45 tables.
382
+
383
+ ### attribute
384
+
385
+ One per subject with sub-elements: a table's columns, a report's parameters, a
386
+ screen's fields.
387
+
388
+ ```json
389
+ {
390
+ "kind": "attribute",
391
+ "surface": "tables",
392
+ "subject": "table-roster-days",
393
+ "phase": "extract",
394
+ "directions": {
395
+ "ddl": { "count": 14, "evidence": "column list from CREATE TABLE" },
396
+ "entity": { "count": 13, "evidence": "properties on the EF entity class" }
397
+ },
398
+ "total": 15,
399
+ "behavioral": 7,
400
+ "explained": 6,
401
+ "queued": ["q-ros-007"],
402
+ "batch": "b-attr-001"
403
+ }
404
+ ```
405
+
406
+ Balance: `explained + queued.length == behavioral`. `total` is bounded by
407
+ `directions` the same way as `lens`, above.
408
+
409
+ ### rule-sweep
410
+
411
+ One per capability, recording a search for code-enforced rules that no CRUD
412
+ requirement captured.
413
+
414
+ ```json
415
+ {
416
+ "kind": "rule-sweep",
417
+ "subject": "user-management",
418
+ "phase": "extract",
419
+ "probes": 4,
420
+ "found": 2,
421
+ "as_requirements": 2,
422
+ "queued": [],
423
+ "batch": "b-rules-001"
424
+ }
425
+ ```
426
+
427
+ Balance: `found == as_requirements + queued.length`. `rule-sweep` has no
428
+ `directions` field and no bound.
429
+
430
+ ### closer
431
+
432
+ One per declared closer in `[closers].set`.
433
+
434
+ ```json
435
+ {
436
+ "kind": "closer",
437
+ "closer": "read-write-symmetry",
438
+ "phase": "extract",
439
+ "checked": 34,
440
+ "findings": 3,
441
+ "fixed": 2,
442
+ "queued": ["q-sym-001"],
443
+ "batch": "b-closer-001"
444
+ }
445
+ ```
446
+
447
+ Balance: `findings == fixed + queued.length`. `checked` is informational.
448
+ `closer` has no `directions` field and no bound.
449
+
450
+ ## Queue items
451
+
452
+ `migrate queue add item.md` takes a markdown file whose stem matches its `id`.
453
+
454
+ ```markdown
455
+ ---
456
+ id: q-invoice-batch-scope
457
+ severity: moderate
458
+ status: open
459
+ ---
460
+
461
+ ## Evidence
462
+
463
+ Route POST /api/invoice/batch found in InvoiceController.cs:215-240.
464
+ Prod logs show 3 invocations in 6 months.
465
+
466
+ ## Options
467
+
468
+ (a) Replicate as-is. (b) Harden it. (c) Mark out of scope.
469
+
470
+ ## Recommendation
471
+
472
+ Recommend (c); usage suggests it is deprecated.
473
+ ```
474
+
475
+ - `severity` is `critical`, `moderate` or `minor`. `queue list` sorts by that
476
+ order, then by id.
477
+ - `status` is `open` or `adjudicated`. An adjudicated item requires a `ruling`.
478
+ - All three sections must be present and non-empty. Missing and empty produce
479
+ distinguishable errors.
480
+ - Headings must be exactly level two at the start of a line. `### Options` is not
481
+ a heading match, and a duplicate heading is an error rather than a silent
482
+ first-wins.
483
+ - Headings inside fenced code blocks are ignored, so Evidence can quote code
484
+ containing `##` lines. An unclosed fence is a loud error.
485
+ - BOM and CRLF are handled.
486
+
487
+ ## The ten gates
488
+
489
+ `migrate check` reports violations grouped by gate, always in this order. Every
490
+ message names the specific offending row, path or id; there is no aggregate
491
+ "check failed".
492
+
493
+ The summary line is always printed, passing or failing:
494
+
495
+ ```
496
+ 612/612 mapped, 0 out-of-scope, 0 unaccounted
497
+ ```
498
+
499
+ | Gate | Enforces |
500
+ |---|---|
501
+ | `coverage` | Every element has a terminal disposition. An `unaccounted` element is a violation naming its id and surface. |
502
+ | `census` | Every declared surface has a lens record and every declared closer has a closer record; every record is validated and balances; `in_ledger + added` matches the real element count for that surface; for `lens` and `attribute` records, `total` is bounded by `max(directions) <= total <= sum(directions)`. A row that fails validation is named by line number, and excluded from the arithmetic checks above, but still registers the surface or closer it names so this gate does not also claim that surface's lens never ran. |
503
+ | `refs` | Referential integrity: a `mapped` disposition resolves to a real requirement, a queue id resolves to a real queue file, a requirement's `cap` resolves to a capability, a `ledger` citation resolves to a real element. Also catches duplicate requirement ids, capability slugs and element ids. |
504
+ | `queue` | Queue files parse and satisfy the grammar above. |
505
+ | `deltas` | No delta is left unsigned. |
506
+ | `parity` | Every requirement whose confidence is not `queued` carries a parity plan. |
507
+ | `citations` | **On by default; opt out with `--no-citations`.** Every `src` citation resolves against the source tree, with line ranges inside the file. Symlinks are followed and checked, so a link out of the tree is rejected. The old `--citations` flag is still accepted and silently ignored. |
508
+ | `leaks` | **Opt-in, `--leaks`.** No value from `.migrate/.env` appears in a committed artifact or anywhere in git history. Messages name the variable and file, never the value. |
509
+ | `source` | The source checkout has no uncommitted changes, when it is a git repo. |
510
+ | `run-state` | Every phase through the checked terminus must be `done` in `phases.json`; a phase `done` while its immediate predecessor is still `pending` fails regardless of terminus; a lens or closer census naming a `batch` that `phases.json` never recorded committing (in `enumerate` or `extract` respectively) fails by name. |
511
+
512
+ **`check` is strict mid-run by design.** The census gate wants a record for
513
+ every declared surface and closer, so it does not pass until a run is finished.
514
+ Grouping by gate is what lets you tell an expected mid-run gap from a real
515
+ defect.
516
+
517
+ **`--phase <p>` narrows only the run-state gate, not the other nine.** Without
518
+ `--phase`, `run-state` requires every phase through `handoff` to be `done`, so
519
+ exit 0 means the whole migration is complete. With `--phase enumerate`, it
520
+ requires only `probe` and `enumerate` to be `done`, which is the mid-run
521
+ posture: a coverage or census gap past that point still fails on its own gate,
522
+ exactly as it would without `--phase`, because those nine gates read the store,
523
+ not the phase you named. Verified against a fresh store: `check --phase probe`
524
+ reports one `run-state` violation (`probe`); plain `check` reports eight, one
525
+ per phase.
526
+
527
+ **What `check` alone still cannot tell you.** `run-state` reads `phases.json`,
528
+ which a command sets on request; an agent that runs `migrate phase <p>
529
+ --status done` across every phase over an all-zero census still reaches exit
530
+ 0. What changed from Milestone 1 is that doing nothing no longer does. Use
531
+ `migrate status` for a plainer read of what has actually run.
532
+
533
+ ## Command details
534
+
535
+ **`migrate init --source <path> --scope <text> --name <target>`**
536
+ Optional: `--source-stack`, `--target-stack`, `--basis <runnable|source-only>`.
537
+ Creates `.migrate/` and `.migrate/queue/`, writes `config.toml`, and makes sure
538
+ `.migrate/.env` is gitignored: it appends the entry to an existing `.gitignore`
539
+ exactly once, and **creates a `.gitignore` containing it when the target has
540
+ none**. It says on stdout when it changed something (`init: appended
541
+ .migrate/.env to <path>` or `init: created <path> with .migrate/.env`), so a
542
+ file written outside `.migrate/` is never written in silence. A target whose
543
+ `.gitignore` already lists the entry gets neither line, because nothing was
544
+ written.
545
+
546
+ Refuses an existing config at 1, and a source path that is missing or not a
547
+ directory at 2. Refuses at 2, before creating anything at all, if any of its
548
+ three write targets (`config.toml`, `queue/`, `.gitignore`) resolves inside
549
+ `source.path`; a refusal leaves the tree exactly as it found it. Values you
550
+ pass are escaped, so a scope containing quotes or backslashes round-trips
551
+ intact.
552
+
553
+ `vcs` is the only thing `init` detects, from the presence of `.git` in the
554
+ source. `stack` is not detected: it is whatever `--source-stack` supplied, and
555
+ `unknown` otherwise. Detecting the stack is the probing agent's job; see
556
+ `references/phases/probe.md`.
557
+
558
+ **`migrate phase [<name>] [--status <s>]`**
559
+ With no arguments, prints all eight phases: status and batch count, one line
560
+ each, reading is the default so an orchestrator resuming a run sees where it
561
+ stopped before it moves anything. With `<name>` alone, prints just that
562
+ phase's line. With `<name> --status <s>`, sets that phase's status; `<s>` is
563
+ one of `pending`, `running`, `blocked`, `done`. An unknown phase name or status
564
+ value exits 2, naming the valid set. The write path takes the store lock and
565
+ accepts `--force-unlock`; a lock failure exits 3. Unlike `init`, `phase`
566
+ resolves its store root the same way `check` does, by searching upward from
567
+ the cwd, not by trusting the cwd itself.
568
+
569
+ **`migrate reset --phase <phase>`**
570
+ Clears only what that phase owns: `enumerate` clears elements and lens census
571
+ records; `seam` clears capabilities and deletes `seam.json` and `seam.md`;
572
+ `extract` clears requirements, the attribute, rule-sweep and closer census
573
+ records, and returns every element disposition to `unaccounted`; `parity`
574
+ clears deltas and nulls every requirement's parity. Every phase, including the
575
+ four with nothing else to clear, also sets its own status back to `pending` and
576
+ empties its `batches` list. **Queue items are never cleared by any phase.**
577
+
578
+ The whole mutation runs inside the store lock, the same way `import` and
579
+ `census` do and for the same reason: it is a read-modify-write over whole store
580
+ files. A lock failure exits 3, and `--force-unlock` removes a lock believed
581
+ stale before retrying.
582
+
583
+ **`migrate report [--out <dir>]`**
584
+ Writes `ledger.md`, `requirements.md` and `queue.md`, defaulting to
585
+ `docs/migrate/`. Generated files, each carrying a banner saying so. Cell content
586
+ is escaped so free text containing pipes or newlines cannot break a table.
587
+
588
+ **`migrate status`**
589
+ Read-only. Phase state, store counts, the gate summary line, and a resume
590
+ pointer naming the first non-done phase and its last recorded batch.
@@ -0,0 +1,39 @@
1
+ # Ground truth for tiny-express
2
+
3
+ Twelve elements across all eight default surfaces. Any enumeration run over
4
+ this fixture must find exactly these, and the census must balance at these
5
+ totals: 3 routes, 2 tables, 1 job, 1 report, 1 screen, 1 integration, 1
6
+ workflow, 2 settings.
7
+
8
+ | surface | id | element |
9
+ | --- | --- | --- |
10
+ | routes | route-get-api-users | GET /api/users |
11
+ | routes | route-post-api-users | POST /api/users |
12
+ | routes | route-get-api-users-id-welcome | GET /api/users/:id/welcome |
13
+ | tables | table-users | users |
14
+ | tables | table-audit-log | audit_log |
15
+ | jobs | job-purge-audit-log | nightly purge of audit_log |
16
+ | reports | report-daily-users | daily-users report |
17
+ | screens | screen-users | users list screen |
18
+ | integrations | integration-mailer | call to mailer service |
19
+ | workflows | workflow-welcome-email | welcome email workflow |
20
+ | settings | setting-welcome-email-enabled | welcomeEmailEnabled |
21
+ | settings | setting-max-users-per-page | maxUsersPerPage |
22
+
23
+ ## Element-to-element touches
24
+
25
+ Five of these elements touch another element already in this table, the
26
+ kind of touch `references/phases/enumerate.md`'s Procedure (step 4) tells a
27
+ lens to record as a `{"kind": "ledger", "id": ...}` ref, and the only edge
28
+ data `references/phases/seam.md`'s surface-affinity clustering has to
29
+ build a graph from. Kept here as prose, not as a fourth table column,
30
+ because unlike the census columns above nothing parses this: the e2e test
31
+ records these refs by hand against the ids above, the same way a real
32
+ lens would, rather than by reading this section.
33
+
34
+ - `route-get-api-users` reads `table-users`.
35
+ - `route-post-api-users` writes `table-users`.
36
+ - `job-purge-audit-log` purges `table-audit-log`.
37
+ - `report-daily-users` queries `table-users`.
38
+ - `workflow-welcome-email` spans `route-post-api-users`,
39
+ `route-get-api-users-id-welcome`, and `setting-welcome-email-enabled`.
@@ -0,0 +1,29 @@
1
+ const express = require('express')
2
+ const settings = require('./settings.json')
3
+ const app = express()
4
+
5
+ // Workflow step 1: a signup stores a pending welcome email.
6
+ const pendingWelcomes = new Map()
7
+
8
+ app.get('/api/users', (req, res) => res.json([].slice(0, settings.maxUsersPerPage)))
9
+
10
+ app.post('/api/users', (req, res) => {
11
+ const id = Date.now()
12
+ pendingWelcomes.set(id, req.body.email)
13
+ res.status(201).json({ id })
14
+ })
15
+
16
+ // Workflow step 2: the welcome route consumes the state step 1 stored, then
17
+ // calls the mailer service to actually send it.
18
+ app.get('/api/users/:id/welcome', (req, res) => {
19
+ const email = pendingWelcomes.get(Number(req.params.id))
20
+ if (settings.welcomeEmailEnabled && email) {
21
+ fetch('https://mailer.example.com/send', {
22
+ method: 'POST',
23
+ body: JSON.stringify({ to: email }),
24
+ })
25
+ }
26
+ res.json({ sent: Boolean(email) })
27
+ })
28
+
29
+ module.exports = app
@@ -0,0 +1,6 @@
1
+ const cron = require('node-cron')
2
+
3
+ // Nightly purge of audit_log rows older than 30 days.
4
+ cron.schedule('0 2 * * *', () => {
5
+ console.log('purge audit_log rows older than 30 days')
6
+ })
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "daily-users",
3
+ "description": "Count of active users, run once a day",
4
+ "query": "SELECT COUNT(*) FROM users WHERE is_active = 1",
5
+ "schedule": "daily"
6
+ }