@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.
- package/README.md +18 -2
- package/dist/cli/index.js +105 -28
- package/package.json +1 -1
- package/skills/index.json +14 -2
- package/skills/magpie/README.md +2 -1
- package/skills/magpie/SKILL.md +138 -45
- package/skills/magpie/bin/magpie.ts +43 -0
- package/skills/magpie/fixtures/example-pr/brief.json +18 -0
- package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
- package/skills/magpie/fixtures/fake-gh.sh +14 -0
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/references/peer-review.md +7 -2
- package/skills/magpie/references/scout.md +90 -0
- package/skills/magpie/references/specialists.md +117 -6
- package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
- package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
- package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
- package/skills/magpie/scripts/__tests__/gh.test.ts +90 -0
- package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
- package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
- package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +173 -1
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +151 -1
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
- package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +207 -7
- package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
- package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
- package/skills/magpie/scripts/diff-chunks.ts +28 -0
- package/skills/magpie/scripts/findings-files.ts +32 -0
- package/skills/magpie/scripts/gh.ts +68 -14
- package/skills/magpie/scripts/git-diff.ts +111 -0
- package/skills/magpie/scripts/path-filter.ts +9 -5
- package/skills/magpie/scripts/preview-cmd.ts +11 -1
- package/skills/magpie/scripts/refresh.ts +32 -3
- package/skills/magpie/scripts/render-cmd.ts +35 -11
- package/skills/magpie/scripts/render-findings.ts +78 -2
- package/skills/magpie/scripts/render-progress.ts +6 -1
- package/skills/magpie/scripts/setup-cmd.ts +38 -1
- package/skills/magpie/scripts/shard.ts +171 -0
- package/skills/magpie/scripts/status-cmd.ts +7 -4
- package/skills/magpie/scripts/types.ts +50 -0
- package/skills/magpie/skill.json +2 -2
- package/skills/magpie/templates/styles.css +66 -0
- package/skills/migrate/README.md +194 -0
- package/skills/migrate/SKILL.md +197 -0
- package/skills/migrate/bin/migrate +15 -0
- package/skills/migrate/bin/migrate.ts +309 -0
- package/skills/migrate/biome.json +35 -0
- package/skills/migrate/bun.lock +24 -0
- package/skills/migrate/docs/architecture.md +294 -0
- package/skills/migrate/docs/reference.md +590 -0
- package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
- package/skills/migrate/fixtures/tiny-express/app.js +29 -0
- package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
- package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
- package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
- package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
- package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
- package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
- package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
- package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
- package/skills/migrate/install.sh +68 -0
- package/skills/migrate/package.json +17 -0
- package/skills/migrate/references/phases/enumerate.md +291 -0
- package/skills/migrate/references/phases/extract.md +652 -0
- package/skills/migrate/references/phases/parity.md +275 -0
- package/skills/migrate/references/phases/probe.md +135 -0
- package/skills/migrate/references/phases/queue.md +242 -0
- package/skills/migrate/references/phases/seam.md +416 -0
- package/skills/migrate/references/recipes/README.md +116 -0
- package/skills/migrate/references/recipes/aspnet.md +287 -0
- package/skills/migrate/references/run-ops.md +280 -0
- package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
- package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
- package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
- package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
- package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
- package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
- package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
- package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
- package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
- package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
- package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
- package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
- package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
- package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
- package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
- package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
- package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
- package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
- package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
- package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
- package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
- package/skills/migrate/scripts/census-cmd.ts +109 -0
- package/skills/migrate/scripts/census.ts +342 -0
- package/skills/migrate/scripts/check-cmd.ts +24 -0
- package/skills/migrate/scripts/check.ts +376 -0
- package/skills/migrate/scripts/citations.ts +92 -0
- package/skills/migrate/scripts/config.ts +237 -0
- package/skills/migrate/scripts/ids.ts +31 -0
- package/skills/migrate/scripts/import-cmd.ts +141 -0
- package/skills/migrate/scripts/init-cmd.ts +118 -0
- package/skills/migrate/scripts/leaks.ts +184 -0
- package/skills/migrate/scripts/lock.ts +188 -0
- package/skills/migrate/scripts/paths.ts +103 -0
- package/skills/migrate/scripts/phase-cmd.ts +63 -0
- package/skills/migrate/scripts/phases.ts +113 -0
- package/skills/migrate/scripts/queue-cmd.ts +98 -0
- package/skills/migrate/scripts/queue.ts +258 -0
- package/skills/migrate/scripts/report-cmd.ts +47 -0
- package/skills/migrate/scripts/report.ts +131 -0
- package/skills/migrate/scripts/reset-cmd.ts +120 -0
- package/skills/migrate/scripts/status-cmd.ts +52 -0
- package/skills/migrate/scripts/store.ts +159 -0
- package/skills/migrate/scripts/types.ts +137 -0
- package/skills/migrate/scripts/validate.ts +221 -0
- package/skills/migrate/skill.json +33 -0
- package/skills/migrate/templates/config.toml +27 -0
- package/skills/migrate/templates/queue-item.md +17 -0
- package/skills/migrate/tsconfig.json +18 -0
- package/skills/migrate/uninstall.sh +31 -0
- package/skills/sluice/SKILL.md +82 -0
- package/skills/sluice/references/deep-channel.md +94 -0
- package/skills/sluice/references/finish.md +35 -0
- package/skills/sluice/references/intent.md +29 -0
- package/skills/sluice/references/review.md +42 -0
- package/skills/sluice/references/root-cause.md +38 -0
- package/skills/sluice/references/show-or-say.md +36 -0
- package/skills/sluice/references/test-first.md +35 -0
- package/skills/sluice/references/verify.md +26 -0
- 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
|