@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,652 @@
1
+ # Phase 3: Extract
2
+
3
+ ## Purpose
4
+
5
+ Mine functional requirements out of every capability's elements, keeping a
6
+ citation trail back to the evidence, and give every element a terminal
7
+ disposition. Exit condition: every capability has been mined, every element
8
+ carries `disposition.kind` of `mapped` or `out-of-scope` (never
9
+ `unaccounted`), every declared closer in `[closers].set` has one `closer`
10
+ census record, and `migrate phase extract --status done` has run.
11
+
12
+ ## Inputs
13
+
14
+ - `config.toml`: `[closers].set` (the closers you owe a census record,
15
+ independent of how many you happen to find), `source.stack` (whether a
16
+ recipe supplies attribute directions the way it supplies lens directions
17
+ in enumerate; contract-only applies here exactly as it does there).
18
+ - The store: `capabilities.jsonl` (the fanout unit; every element inside one
19
+ capability's `elements` array is this pass's scope), `elements.jsonl`
20
+ (every row still `unaccounted` after enumerate is what you are mining),
21
+ `requirements.jsonl` (what already exists, for resuming without
22
+ re-mining), `phases.json` (committed batches, for resuming).
23
+ - The source checkout (read-only): the citation gate resolves every `src`
24
+ citation against it.
25
+
26
+ ## Procedure
27
+
28
+ **Extract's contract, stated once.** For each capability, read every
29
+ element it owns, decide what functional requirement that element's evidence
30
+ actually supports, write one requirement per distinct behavior with at
31
+ least one citation, and write back a terminal disposition onto every
32
+ element the requirement accounts for. Nothing here is optional: an element
33
+ extract does not dispose of stays `unaccounted` forever, since disposition
34
+ has exactly one writer (below), and a requirement with no citation cannot
35
+ be imported at all.
36
+
37
+ 1. **Fan out one agent per capability.** A capability is the unit; an agent
38
+ mining `user-management` never needs to know what `invoicing` looks
39
+ like. See `references/run-ops.md` for dispatch and checkpoint mechanics;
40
+ this manual does not restate them.
41
+
42
+ 2. **Confidence tiers, and the evidence trust order that sets them.** Every
43
+ requirement's `confidence.kind` is `confirmed`, `inferred`, or `queued`.
44
+ When two lenses' evidence disagree about what the source actually does,
45
+ trust them in this order, strongest first: **`runtime` > `code` > `nav`
46
+ > `docs`**. Runtime is what the system actually did when observed;
47
+ `code` is the logic that decides what it does, but a path can be dead or
48
+ never actually reached; `nav` shows what is exposed, not what it does
49
+ once reached; `docs` is aspirational and goes stale fastest of the four.
50
+ Assign the tier from what the trusted evidence actually shows, not from
51
+ how confident you feel: **confirmed** when the highest-trust evidence
52
+ available is unambiguous and nothing contradicts it; **inferred** when
53
+ the evidence shows part of the behavior but a piece is genuinely
54
+ unobservable from here (a call to something outside the citable source);
55
+ **queued** when the evidence conflicts or is too thin to call either way
56
+ and an owner has to.
57
+
58
+ 3. **Origin tagging is a second, independent judgment, not a restatement of
59
+ confidence.** `origin` is `intended` or `accidental-candidate`.
60
+ Confidence asks "how sure am I this is what happens"; origin asks
61
+ "does this read as deliberate, or as something nobody meant to build
62
+ this way." A requirement can be fully confirmed and still
63
+ accidental-candidate: the evidence can be completely unambiguous about
64
+ *what* happens while leaving open *whether it should*.
65
+
66
+ A worked example, run against a real store. `Controllers/AuthController.cs`
67
+ has a `Login` method that validates credentials, a `GetUsers` method with
68
+ no `[Authorize]` attribute and no auth check of any kind, and a
69
+ `ResetPassword` method whose comment says only "no test coverage in the
70
+ legacy system; behavior beyond 'a link is emailed' is not observable from
71
+ this method alone":
72
+
73
+ ```json
74
+ [
75
+ {
76
+ "id": "UM-001",
77
+ "cap": "user-management",
78
+ "requirement": "User must provide a valid email and password to log in",
79
+ "actors": "User",
80
+ "objects": "Credentials",
81
+ "rules": "Email must contain @; password minimum 8 characters",
82
+ "origin": "intended",
83
+ "confidence": { "kind": "confirmed" },
84
+ "citations": [
85
+ { "kind": "ledger", "id": "route-post-api-login" },
86
+ { "kind": "src", "path": "Controllers/AuthController.cs", "lines": [9, 25] }
87
+ ],
88
+ "parity": null
89
+ },
90
+ {
91
+ "id": "UM-002",
92
+ "cap": "user-management",
93
+ "requirement": "Any caller can list every user, with no authentication check",
94
+ "actors": "Any caller",
95
+ "objects": "User list",
96
+ "rules": "-",
97
+ "origin": "accidental-candidate",
98
+ "confidence": { "kind": "confirmed" },
99
+ "citations": [
100
+ { "kind": "ledger", "id": "route-get-api-users" },
101
+ { "kind": "src", "path": "Controllers/AuthController.cs", "lines": [27, 32] }
102
+ ],
103
+ "parity": null
104
+ },
105
+ {
106
+ "id": "UM-003",
107
+ "cap": "user-management",
108
+ "requirement": "User can request a password reset link by email without the response revealing whether the account exists",
109
+ "actors": "User",
110
+ "objects": "Reset token",
111
+ "rules": "Response is identical whether or not the account exists",
112
+ "origin": "intended",
113
+ "confidence": { "kind": "inferred" },
114
+ "citations": [
115
+ { "kind": "ledger", "id": "route-post-api-password-reset" },
116
+ { "kind": "src", "path": "Controllers/AuthController.cs", "lines": [34, 45] }
117
+ ],
118
+ "parity": null
119
+ }
120
+ ]
121
+ ```
122
+
123
+ (this is the batch file's `rows` array; the envelope carrying `batch` and
124
+ `phase` around it is elided here since import-cmd.ts's own shape is
125
+ already covered above.)
126
+
127
+ `migrate import reqs batch.json` accepts all three in one call and prints
128
+ `import reqs: 3 added, 0 updated, batch b-reqs-um-001`. UM-002's
129
+ confidence is `confirmed` because the missing auth check is not in doubt;
130
+ its origin is `accidental-candidate` because nothing in the source
131
+ suggests leaving every user's data open to any caller was ever a decision
132
+ anyone made. UM-003 is where the evidence trust order actually bites:
133
+ `code` confirms a link is sent and that a missing account does not change
134
+ the response, but nothing citable in this source shows what happens once
135
+ a token is submitted, so that piece is `inferred`, not `confirmed`, no
136
+ matter how confident the wording sounds. `parity` is `null` on all three
137
+ on purpose: assigning an oracle is phase 4's job, not this one's, and the
138
+ parity gate exempts nothing at `confirmed` or `inferred`, only at
139
+ `queued` (parity.md covers why).
140
+
141
+ ### Mandatory citations, and the citation gate
142
+
143
+ **At least one citation is required on every requirement; `migrate import
144
+ reqs` refuses one with none.** This was always the rule; what changed is
145
+ that a fabricated `src` citation now fails a gate instead of merely
146
+ breaking one. `citations` runs by default (`--no-citations` opts out) and
147
+ resolves every `src` citation against the read-only source checkout,
148
+ line ranges included.
149
+
150
+ A worked failure, run on a disposable copy of the store so this defective
151
+ row never enters the running example: a requirement citing a controller
152
+ that was never part of the checkout.
153
+
154
+ ```json
155
+ { "kind": "src", "path": "Controllers/SessionController.cs", "lines": [10, 20] }
156
+ ```
157
+
158
+ `migrate import reqs` accepts the row (the importer only checks shape, not
159
+ existence), but `migrate check` reports it under its own gate, by name:
160
+
161
+ ```
162
+ citations:
163
+ UM-004 cites Controllers/SessionController.cs, which does not exist in the source tree
164
+ ```
165
+
166
+ A citation with an inverted range (`start > end`) or a range past the
167
+ file's actual line count fails the same gate the same way, each with its
168
+ own message naming the requirement and the exact problem.
169
+
170
+ ### Writing the disposition back
171
+
172
+ **Past enumerate, the elements batch you import here is the only writer of
173
+ a *resolved* `disposition`.** Nothing about writing a requirement changes an
174
+ element's disposition by itself; every element the requirement accounts
175
+ for needs its own row in an elements batch, disposition set to `mapped`
176
+ with the requirement's id, or to `out-of-scope` with a queue id if it is
177
+ being carried forward unmapped on purpose. This is the write-back
178
+ `SKILL.md` calls out as load-bearing: a review found the walkthrough could
179
+ not clear its own coverage gate without this second import, because
180
+ nothing else moves an element toward a terminal disposition once enumerate
181
+ ends. (`migrate reset --phase extract` also writes this field, but only
182
+ back to `unaccounted`, undoing the phase rather than progressing it.)
183
+
184
+ A worked example, run against the same store, four elements mapped and one
185
+ carried out of scope:
186
+
187
+ ```json
188
+ [
189
+ { "id": "route-post-api-login", "surface": "routes", "element": "POST /api/login",
190
+ "found_by": ["code", "nav"], "disposition": { "kind": "mapped", "fr": "UM-001" },
191
+ "refs": [{ "kind": "src", "path": "Controllers/AuthController.cs", "lines": [9, 25] }],
192
+ "lens": "code", "notes": "" },
193
+ { "id": "route-get-legacy-admin-tool", "surface": "routes", "element": "GET /legacy-admin-tool",
194
+ "found_by": ["code"], "disposition": { "kind": "out-of-scope", "queue": "q-legacy-admin-tool" },
195
+ "refs": [], "lens": "code", "notes": "not linked from nav; found only by grepping controller attributes" }
196
+ ]
197
+ ```
198
+
199
+ (again, the `rows` array; the other three rows in the real batch are the
200
+ same shape and are omitted here for space: `route-get-api-users` mapped to
201
+ `UM-002`, `route-post-api-password-reset` mapped to `UM-003`, and
202
+ `table-users` mapped to `UM-001`, the table backing the credentials check
203
+ UM-001 describes.)
204
+
205
+ `migrate import elements batch.json` accepts the full batch and prints
206
+ `import elements: 0 added, 5 updated, batch b-elements-disposition-001`
207
+ (upsert by id, same mechanic as any other re-import). Coverage moves from
208
+ `0/5 mapped, 0 out-of-scope, 5 unaccounted` before this batch to `4/5
209
+ mapped, 1 out-of-scope, 0 unaccounted` after it.
210
+
211
+ **An `out-of-scope` disposition's queue id is checked for existence by the
212
+ refs gate, and file it before you rely on that in a check, not after.**
213
+ `migrate check` reads the current store every time it runs; a queue id an
214
+ element names but that has no file yet is a real, present violation the
215
+ moment anything checks, not a future one. File it in the same pass:
216
+
217
+ ```markdown
218
+ ---
219
+ id: q-legacy-admin-tool
220
+ severity: minor
221
+ status: open
222
+ ---
223
+
224
+ ## Evidence
225
+
226
+ `GET /legacy-admin-tool` was found by the `code` lens (grepping controller
227
+ attributes) but not by `nav` (it has no menu entry, no link anywhere in
228
+ Startup.cs's route registrations). No FR describes it.
229
+
230
+ ## Options
231
+
232
+ (a) Carry it forward as-is. (b) Mark it out of scope as dead code. (c) Ask
233
+ the operator whether anything still calls it.
234
+
235
+ ## Recommendation
236
+
237
+ Recommend (b); an endpoint with no nav entry and no citation anywhere else
238
+ in the source reads as abandoned, not hidden-but-load-bearing.
239
+ ```
240
+
241
+ `migrate queue add q-legacy-admin-tool.md` accepts this and prints `queue
242
+ add: q-legacy-admin-tool [minor]` (queue.md covers the grammar this file
243
+ must satisfy). Only after this does the coverage line above hold up against
244
+ a real `check`, not merely against the batch import that produced it.
245
+
246
+ ### The business-rule lens: rule-sweep
247
+
248
+ **A `rule-sweep` census records a deliberate search, per capability, for
249
+ code-enforced rules that no CRUD-shaped requirement already captured.**
250
+ Balance: `found == as_requirements + queued.length`. It has no `directions`
251
+ field and no bound: it is a sweep, not a two-direction enumeration, so
252
+ nothing here asks it to dedupe across independent counting methods the way
253
+ a lens or an attribute census does.
254
+
255
+ A worked example: `AuthController.cs:8` carries `// TODO: lock the account
256
+ after 5 failed attempts; not implemented yet` directly above `Login`. The
257
+ sweep found it; it cannot become a requirement describing current behavior
258
+ because it is explicitly not implemented, so it goes to the queue instead.
259
+
260
+ ```json
261
+ {
262
+ "kind": "rule-sweep",
263
+ "subject": "user-management",
264
+ "phase": "extract",
265
+ "probes": 3,
266
+ "found": 1,
267
+ "as_requirements": 0,
268
+ "queued": ["q-account-lockout-scope"],
269
+ "batch": "b-rules-um-001"
270
+ }
271
+ ```
272
+
273
+ `migrate census rule-sweep.json` accepts this and prints `census: recorded
274
+ rule-sweep:user-management`. An imbalanced record is rejected with the
275
+ specific numbers named: submitting `found: 2` against the same
276
+ `as_requirements`/`queued` reports `rule-sweep census for user-management
277
+ does not balance: found 2 but as_requirements 0 + queued 1 = 1`, run
278
+ against a real store.
279
+
280
+ File the queue item this record names, in the same pass, even though (the
281
+ attribute section below explains why) no gate will ever check that you
282
+ did:
283
+
284
+ ```markdown
285
+ ---
286
+ id: q-account-lockout-scope
287
+ severity: moderate
288
+ status: open
289
+ ---
290
+
291
+ ## Evidence
292
+
293
+ `Controllers/AuthController.cs:8` carries a `TODO: lock the account after 5
294
+ failed attempts; not implemented yet` comment directly above `Login`. The
295
+ rule-sweep found this as a probe hit; it is not implemented, so it cannot
296
+ become a requirement describing current behavior, and it duplicates the
297
+ same open question `q-users-islocked-semantics` (below) raises about the
298
+ `IsLocked` column.
299
+
300
+ ## Options
301
+
302
+ (a) Write it as a requirement for the target anyway, since it is documented
303
+ intent. (b) Leave it out of scope entirely; the legacy system never enforced
304
+ it. (c) Fold it into `q-users-islocked-semantics` and let the operator rule
305
+ on both at once.
306
+
307
+ ## Recommendation
308
+
309
+ Recommend (c); both items are one open question (does lockout exist or
310
+ not), not two.
311
+ ```
312
+
313
+ `migrate queue add q-account-lockout-scope.md` accepts this and prints
314
+ `queue add: q-account-lockout-scope [moderate]`.
315
+
316
+ **A rule-sweep with nothing to report still needs a record**, the same
317
+ zero-findings discipline enumerate.md states for a lens: `probes: N,
318
+ found: 0, as_requirements: 0, queued: []` is a real, closeable record, not
319
+ a reason to skip writing one.
320
+
321
+ ### The attribute lens
322
+
323
+ **The lens contract from enumerate applies one level down, to any surface
324
+ with sub-elements: table to columns, report to parameters, screen to
325
+ fields, endpoint to parameters.** An `attribute` census needs at least two
326
+ independent directions, each naming its evidence, exactly like a `lens`
327
+ census, and `total` is bounded the same way:
328
+ `max(directions) <= total <= sum(directions)`. Balance:
329
+ `explained + queued.length == behavioral`.
330
+
331
+ **The exemption list is judgment this manual holds, not code: identity
332
+ keys, audit stamps, and tenant discriminators are exempt from
333
+ `behavioral`.** A column that only exists to be a primary key, or to record
334
+ who created or last touched a row and when, or to say which tenant a row
335
+ belongs to, carries no behavior of its own to explain; counting it toward
336
+ `behavioral` would demand an "explanation" for something that has none.
337
+ Nothing enforces this list in code, the same way enumerate.md's naming
338
+ convention for skipped elements is a convention rather than a check: two
339
+ different agents can disagree about whether a given column is exempt, and
340
+ the tool has no opinion either way.
341
+
342
+ A worked example, run against a real store. The `Users` table has five
343
+ columns: `Id` (identity key, exempt), `Email`, `PasswordHash`, `CreatedAt`
344
+ (audit stamp, exempt), and `IsLocked`. The `ddl` direction counted all five
345
+ from the `CREATE TABLE` statement; `entity` counted four properties on the
346
+ EF entity class (one column has no mapped property). `Email` and
347
+ `PasswordHash` are both explained by UM-001 directly; `IsLocked` is neither
348
+ exempt nor explained by anything on record, and the account-lockout comment
349
+ that `q-account-lockout-scope` already raised makes its real semantics an
350
+ open question, not a settled one, so it is queued rather than guessed at.
351
+
352
+ ```json
353
+ {
354
+ "kind": "attribute",
355
+ "surface": "tables",
356
+ "subject": "table-users",
357
+ "phase": "extract",
358
+ "directions": {
359
+ "ddl": { "count": 5, "evidence": "column list from CREATE TABLE Users" },
360
+ "entity": { "count": 4, "evidence": "properties on the EF User entity class" }
361
+ },
362
+ "total": 5,
363
+ "behavioral": 3,
364
+ "explained": 2,
365
+ "queued": ["q-users-islocked-semantics"],
366
+ "batch": "b-attr-users-001"
367
+ }
368
+ ```
369
+
370
+ `migrate census attr-table-users.json` accepts this and prints `census:
371
+ recorded attribute:table-users`. The same shape failures the lens census
372
+ enforces apply here too, run against a real store: the old bare-count
373
+ shape (`"directions": {"ddl": 8, "entity": 7}`) is rejected by name on both
374
+ fields at once (`directions.ddl uses the old bare-count shape...`), a
375
+ single direction is rejected (`directions needs at least two independent
376
+ directions; the lens contract does not admit a single-direction
377
+ enumeration`), and an imbalanced record is rejected naming the exact
378
+ mismatch (`attribute census for table-users does not balance: behavioral 3
379
+ but explained 1 + queued 1 = 2`).
380
+
381
+ File the item this record names, the same as rule-sweep's above:
382
+
383
+ ```markdown
384
+ ---
385
+ id: q-users-islocked-semantics
386
+ severity: moderate
387
+ status: open
388
+ ---
389
+
390
+ ## Evidence
391
+
392
+ The `Users` table's `IsLocked` column is not an identity key, an audit
393
+ stamp, or a tenant discriminator, so it is not on the attribute lens's
394
+ exemption list, and it is not written or read anywhere in
395
+ `AuthController.cs`. The `Login` method's own comment says account lockout
396
+ "is not implemented yet."
397
+
398
+ ## Options
399
+
400
+ (a) Treat it as dead state and drop it from the target schema. (b) Treat it
401
+ as a real but unenforced rule and implement lockout in the target. (c) Ask
402
+ the operator which one matches actual production behavior.
403
+
404
+ ## Recommendation
405
+
406
+ Recommend (c); the column and the comment disagree about whether lockout
407
+ exists, and only the operator can say which one is true today.
408
+ ```
409
+
410
+ `migrate queue add q-users-islocked-semantics.md` accepts this and prints
411
+ `queue add: q-users-islocked-semantics [moderate]`.
412
+
413
+ **The honest limit: nothing checks that every attribute-bearing element
414
+ actually got an attribute census, or that every capability actually got a
415
+ rule-sweep.** Verified on a disposable copy of the store: stripping both
416
+ kinds of record out of `census.jsonl` there still passes `migrate check`
417
+ clean, because gate 2 only
418
+ tracks completeness for `lens` (against `[surfaces].types`) and `closer`
419
+ (against `[closers].set`); `attribute` and `rule-sweep` records are
420
+ balance-checked when present but never counted against any declared list.
421
+ Doing the sweep for every capability and the attribute pass for every
422
+ table, report, and screen with sub-elements is this manual's discipline,
423
+ not the tool's gate, the same way the closer set below is enforced by name
424
+ and these two kinds are not.
425
+
426
+ ### The declared closer set
427
+
428
+ **Every closer in `[closers].set` needs exactly one `closer` census
429
+ record; a declared closer with no record fails the gate by name.** Balance:
430
+ `findings == fixed + queued.length`. `checked` is informational and may be
431
+ zero: a closer that genuinely finds nothing to check in a small or
432
+ single-capability run still closes with a real record, not a skip.
433
+
434
+ The default three, and what each looks for:
435
+
436
+ - **cross-capability-workflow.** A user journey that spans more than one
437
+ capability (checkout touching a cart capability, a payment capability,
438
+ and an inventory capability) can get lost when capabilities are mined
439
+ independently, each agent seeing only its own slice. This closer checks
440
+ for exactly that seam.
441
+ - **scope-injection.** An FR that claims more than its citations actually
442
+ support: configurable multi-factor authentication written up as a
443
+ requirement when the source only ever shows a password check. This
444
+ closer checks every FR against what it cites, not against what sounds
445
+ plausible.
446
+ - **read-write-symmetry.** Every write path should have a matching read or
447
+ verification path, and vice versa. A write with no matching read is
448
+ worth naming even when it might turn out to be a false alarm (the
449
+ verification endpoint exists somewhere this pass did not look), because
450
+ the alternative is a defect nobody ever went looking for.
451
+
452
+ Three worked examples, run against the same real store, one per closer,
453
+ each closing on the shape its own finding actually took: no finding at
454
+ all, a finding fixed on the spot, and a finding that needed the queue.
455
+
456
+ **cross-capability-workflow finds nothing here, and that is a real,
457
+ closeable answer, not a skip.** This run has exactly one capability
458
+ (`user-management`), so no workflow inside it can possibly cross into a
459
+ second one; a closer whose whole job is catching a cross-capability seam
460
+ correctly reports zero findings on a single-capability run, checked or not.
461
+
462
+ ```json
463
+ {
464
+ "kind": "closer",
465
+ "closer": "cross-capability-workflow",
466
+ "phase": "extract",
467
+ "checked": 2,
468
+ "findings": 0,
469
+ "fixed": 0,
470
+ "queued": [],
471
+ "batch": "b-closer-cross-capability-001"
472
+ }
473
+ ```
474
+
475
+ `migrate census closer-cross-capability.json` accepts this and prints
476
+ `census: recorded closer:cross-capability-workflow`.
477
+
478
+ **scope-injection catches a finding and fixes it on the spot, no queue
479
+ needed.** A draft of UM-003 originally asserted that the reset token
480
+ expires after 15 minutes; nothing in `AuthController.cs` shows any
481
+ expiry logic at all, so the closer flagged the claim and the sentence was
482
+ trimmed out of the requirement before it was ever imported. `fixed` records
483
+ exactly this: a finding this pass could resolve itself, without an owner.
484
+
485
+ ```json
486
+ {
487
+ "kind": "closer",
488
+ "closer": "scope-injection",
489
+ "phase": "extract",
490
+ "checked": 3,
491
+ "findings": 1,
492
+ "fixed": 1,
493
+ "queued": [],
494
+ "batch": "b-closer-scope-injection-001"
495
+ }
496
+ ```
497
+
498
+ `migrate census closer-scope-injection.json` accepts this and prints
499
+ `census: recorded closer:scope-injection`.
500
+
501
+ **read-write-symmetry finds something it cannot resolve itself, so it goes
502
+ to the queue.** `ResetPassword` writes a reset token and emails it, but no
503
+ controller in the checkout reads or verifies a submitted token.
504
+
505
+ ```json
506
+ {
507
+ "kind": "closer",
508
+ "closer": "read-write-symmetry",
509
+ "phase": "extract",
510
+ "checked": 3,
511
+ "findings": 1,
512
+ "fixed": 0,
513
+ "queued": ["q-reset-token-verify-missing"],
514
+ "batch": "b-closer-read-write-symmetry-001"
515
+ }
516
+ ```
517
+
518
+ `migrate census closer-read-write-symmetry.json` accepts this and prints
519
+ `census: recorded closer:read-write-symmetry`. A closer record must
520
+ declare `"phase": "extract"`; declaring anything else is rejected at write
521
+ time, by name, the mirror image of the lens/`enumerate` constraint
522
+ enumerate.md documents: `closer census must declare phase "extract", the
523
+ only phase gate 10 (run-state) checks its batch against; found
524
+ "enumerate"`.
525
+
526
+ File the item this closer named:
527
+
528
+ ```markdown
529
+ ---
530
+ id: q-reset-token-verify-missing
531
+ severity: critical
532
+ status: open
533
+ ---
534
+
535
+ ## Evidence
536
+
537
+ `read-write-symmetry` checked every write path against a matching read
538
+ path. `ResetPassword` writes a reset token via `GenerateResetToken` and
539
+ emails it, but no controller in the source reads or verifies a submitted
540
+ token: there is no `POST /api/password-reset/confirm` or equivalent. Either
541
+ the verification endpoint exists somewhere this pass did not look, or reset
542
+ tokens are issued and never checked.
543
+
544
+ ## Options
545
+
546
+ (a) Widen the search (other controllers, an area folder, a separate
547
+ service) before concluding it is missing. (b) Treat it as a real gap and
548
+ flag it for the target to fix, not replicate. (c) Ask the operator directly
549
+ whether reset ever worked end-to-end in production.
550
+
551
+ ## Recommendation
552
+
553
+ Recommend (c); a write with no matching read is exactly what this closer
554
+ exists to catch, and only the operator can say whether it is a real defect
555
+ or evidence this pass has not looked far enough yet.
556
+ ```
557
+
558
+ `migrate queue add q-reset-token-verify-missing.md` accepts this and
559
+ prints `queue add: q-reset-token-verify-missing [critical]` (queue.md's
560
+ own worked grammar example is built on this exact file).
561
+
562
+ A declared closer with no record at all fails `census`, run on a
563
+ disposable copy with extract reset before any closer census was recorded
564
+ (never reset the running example itself just to see this message):
565
+
566
+ ```
567
+ census:
568
+ declared closer cross-capability-workflow has no census record
569
+ declared closer scope-injection has no census record
570
+ declared closer read-write-symmetry has no census record
571
+ ```
572
+
573
+ ### Workflow tracing
574
+
575
+ A requirement should describe a whole user journey, not stop at the first
576
+ method that handles part of it. `Login` validates credentials and issues a
577
+ token; a real workflow trace follows that token forward to whatever
578
+ subsequently checks it, not just the one method that mints it. On a larger
579
+ source, one journey routinely crosses more than one controller, or a
580
+ controller and a background job together; cross-capability-workflow above
581
+ is exactly the closer that catches a journey mined this way and split
582
+ across two agents' capabilities without anyone noticing the seam.
583
+
584
+ ## What closes it
585
+
586
+ Every capability mined, every element disposed, every declared closer
587
+ recorded, every queue item this phase's own findings named already filed
588
+ (above). `migrate check --phase extract` will not read as clean mid-run,
589
+ which is expected: the census gate reads the whole store regardless of
590
+ `--phase`, so it still names every surface this scratch run never
591
+ enumerated (`jobs`, `reports`, `screens`, `integrations`, `workflows`,
592
+ `settings`). `refs` does not appear below only because `q-legacy-admin-tool`
593
+ was already filed above; skip that step and it reappears here, naming
594
+ `route-get-legacy-admin-tool`, exactly the way enumerate.md's and seam.md's
595
+ own noisy-but-expected checks name what is genuinely still missing rather
596
+ than padding the count. Run `migrate check --phase extract` for real,
597
+ right before flipping the phase, against a store with exactly the rows
598
+ this manual's examples built:
599
+
600
+ ```
601
+ 4/5 mapped, 1 out-of-scope, 0 unaccounted
602
+
603
+ Violations (10):
604
+ census:
605
+ declared surface jobs has no lens census record; the lens did not run or did not close
606
+ declared surface reports has no lens census record; the lens did not run or did not close
607
+ declared surface screens has no lens census record; the lens did not run or did not close
608
+ declared surface integrations has no lens census record; the lens did not run or did not close
609
+ declared surface workflows has no lens census record; the lens did not run or did not close
610
+ declared surface settings has no lens census record; the lens did not run or did not close
611
+ parity:
612
+ UM-001 has no parity plan
613
+ UM-002 has no parity plan
614
+ UM-003 has no parity plan
615
+ run-state:
616
+ phase extract is running; every phase through extract must be done
617
+ ```
618
+
619
+ The `parity` lines are expected too, for the same reason enumerate.md and
620
+ seam.md give for their own noisy mid-run checks: assigning an oracle is
621
+ phase 4's job, and the `run-state` line clears the moment the phase is
622
+ flipped, not before. Once every capability is mined and every declared
623
+ closer has closed, flip it:
624
+
625
+ ```
626
+ migrate phase extract --status done
627
+ ```
628
+
629
+ ## Degradation
630
+
631
+ - **No recipe for the detected stack.** Contract-only, exactly as in
632
+ enumerate: derive your own attribute directions and rule-sweep probes;
633
+ the census still gates identically.
634
+ - **No runtime environment.** The evidence trust order loses its top tier;
635
+ confidence rests on `code`, `nav`, and `docs` alone, and a behavior only
636
+ a live run could confirm is `inferred` at best, never `confirmed`.
637
+ - **A rule-sweep or attribute census with nothing to report.** Record that
638
+ fact explicitly, the same zero-findings discipline as a lens: a real
639
+ record with zero counts, not a skipped one.
640
+ - **A declared closer that finds nothing to check.** `checked: 0, findings:
641
+ 0` is a valid, closeable record for a closer that genuinely does not
642
+ apply at this run's current scale, not a reason to omit it.
643
+
644
+ ## Commands
645
+
646
+ ```
647
+ migrate import reqs <batch.json>
648
+ migrate import elements <batch.json>
649
+ migrate census <record.json>
650
+ migrate queue add <item.md>
651
+ migrate phase extract --status done
652
+ ```