backend-skeleton 1.0.0-beta.9 → 1.0.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 (45) hide show
  1. package/README.md +122 -12
  2. package/bin/bskel.mjs +564 -15
  3. package/contracts/emit.mjs +5 -1
  4. package/contracts/export.mjs +26 -3
  5. package/contracts/openapi.mjs +29 -3
  6. package/handles/_engine.mjs +79 -29
  7. package/handles/providers/java-spring/plan.mjs +22 -9
  8. package/handles/providers/java-spring/templates/HandleController.java.tmpl +7 -3
  9. package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +12 -6
  10. package/handles/providers/python-fastapi/templates/record_snapshot.py.tmpl +101 -39
  11. package/handles/providers/typescript-express/emit.mjs +23 -23
  12. package/handles/providers/typescript-express/observe.mjs +101 -0
  13. package/handles/providers/typescript-express/templates/contractCheck.ts.tmpl +136 -0
  14. package/handles/providers/typescript-express/templates/observeContract.ts.tmpl +146 -0
  15. package/handles/providers/typescript-express/templates/observedSchema.ts.tmpl +116 -0
  16. package/lib/attest.mjs +40 -0
  17. package/lib/cli.mjs +125 -3
  18. package/lib/cross-feature-collisions.mjs +286 -0
  19. package/lib/diff.mjs +35 -0
  20. package/lib/fsutil.mjs +7 -2
  21. package/lib/gate-definitions.mjs +85 -1
  22. package/lib/gates.mjs +5 -1
  23. package/lib/http-server.mjs +192 -6
  24. package/lib/lock.mjs +68 -15
  25. package/lib/patch-kinds.mjs +52 -0
  26. package/lib/patch-transactions.mjs +206 -0
  27. package/lib/serve-ui.html +211 -0
  28. package/lib/workflow.mjs +31 -3
  29. package/package.json +5 -2
  30. package/scanners/adapters/java-spring.mjs +6 -0
  31. package/scanners/adapters/python-fastapi.mjs +9 -1
  32. package/scanners/adapters/typescript-express.mjs +6 -0
  33. package/scanners/db/ddl-apply.mjs +253 -0
  34. package/scanners/db/introspect.mjs +61 -32
  35. package/scanners/db/migrations.mjs +73 -18
  36. package/schemas/cross-feature-report.schema.json +66 -0
  37. package/schemas/cross-feature-resolution.schema.json +28 -0
  38. package/schemas/gate-attestation.schema.json +22 -0
  39. package/schemas/gate-export.schema.json +58 -0
  40. package/schemas/patch-transaction.schema.json +182 -0
  41. package/schemas/scan-report.schema.json +6 -4
  42. package/schemas/stack-choice.schema.json +12 -1
  43. package/stack/apply.mjs +4 -1
  44. package/stack/catalog/ngrok.yml +8 -2
  45. package/stack/config-apply.mjs +168 -0
package/README.md CHANGED
@@ -23,10 +23,20 @@ of just another thing to double-check by hand.
23
23
  commits behind the real default branch and never noticed. Every gate in this tool is a regression
24
24
  check for a specific failure mode found the same way — see `DECISIONS.md` for the full record.
25
25
 
26
- ## Status: beta
26
+ ## Status: 1.0.0
27
27
 
28
- This is the first public release, and it's deliberately labeled a beta rather than `1.0.0`
29
- stable. Split by actual maturity, not by feature list:
28
+ As of `1.0.0`, this project makes an explicit API-stability promise: **`bskel`'s CLI surface --
29
+ command/flag names and their meaning, every `--json` output shape (all schemas under `schemas/`),
30
+ gate names and pass/fail semantics, and exit codes -- is stable.** A change that breaks any of
31
+ those requires a major version bump, never a patch or minor release; a new command, a new optional
32
+ flag, or a new additive field on an existing JSON shape is always minor-version-safe (every schema
33
+ under `schemas/` is `additionalProperties: false` specifically so a genuinely new field shows up as
34
+ a real, visible schema change rather than something an existing consumer could silently miss). See
35
+ `D-stable-api-contract` in `DECISIONS.md` for the full policy and what's explicitly excluded from
36
+ it.
37
+
38
+ That promise is about the *interface*, not a claim that every subsystem has been proven in
39
+ production -- which still splits the same way it always has:
30
40
 
31
41
  - **`scan`, `contract` (including `export`), and `new`** are the most exercised paths — real,
32
42
  measured verification against a real production Spring Boot repo (see `DECISIONS.md`), plus a
@@ -38,19 +48,17 @@ stable. Split by actual maturity, not by feature list:
38
48
  independently correct roles instead of silently sharing one) are both implemented -- see
39
49
  `DECISIONS.md`'s `D-handle-registry-enforcement`/`D-resolver-authorization-action-aware`. Real
40
50
  gaps remain and are explicitly still open, not closed: registry enforcement is opt-in, off by
41
- default; authorization inference still only recognizes a single `@PreAuthorize(hasRole(...))`
42
- shape (`hasAuthority`, role lists, ownership/tenant policy are unaddressed); and Java/Python are
43
- the only providers either applies to -- TypeScript Express has no persistent handle table at
44
- all. Treat `handles emit`'s output as a scaffold to finish by hand, not a production-ready
45
- subsystem, until a real deployment happens.
46
-
47
- Version numbers, install instructions, and a real feedback path will firm up as this gets used
48
- against more real repos.
51
+ default; authorization inference now recognizes both `@PreAuthorize(hasRole(...))` and
52
+ `hasAuthority(...)` (see `DECISIONS.md`'s `D-resolver-authorization-action-aware`), but
53
+ `hasAnyRole`/`hasAnyAuthority` (list-shape), ownership, and tenant policy are still unaddressed;
54
+ and Java/Python are the only providers either applies to -- TypeScript Express has no persistent
55
+ handle table at all. Treat `handles emit`'s output as a scaffold to finish by hand, not a
56
+ production-ready subsystem, until a real deployment happens.
49
57
 
50
58
  ## Quickstart
51
59
 
52
60
  ```bash
53
- npm install -g backend-skeleton@beta # or: npx backend-skeleton@beta <command>
61
+ npm install -g backend-skeleton # or: npx backend-skeleton <command>
54
62
  cd <target-repo> # must be a git repository
55
63
 
56
64
  bskel doctor # what's on PATH, which scanner adapter detects this repo, and why
@@ -66,6 +74,9 @@ bskel scan disposition --feature 001-organization-management --mode reuse --note
66
74
 
67
75
  bskel contract emit --feature 001-organization-management
68
76
  # feature_id-scoped JSON Schema contract, from real source annotations
77
+ bskel scan cross-feature-check --feature 001-organization-management
78
+ # refuses to proceed if this feature's resourceType/table/operationId
79
+ # collides with another feature -- required before handles emit
69
80
  bskel handles plan --feature 001-organization-management
70
81
  bskel handles emit --feature 001-organization-management
71
82
  # UUID-addressable field handles + generated resolver code
@@ -172,6 +183,105 @@ read from `.env`) for live, read-only Postgres introspection (`information_schem
172
183
  inside a `BEGIN TRANSACTION READ ONLY`) and a source-vs-live drift report. Both are informational
173
184
  additions to the scan report — neither blocks any gate. See `D-db-schema-plane` in `DECISIONS.md`.
174
185
 
186
+ The same `--db`/`--database-url-env` flags, passed to `bskel scan cross-feature-check`, add a 4th
187
+ collision signal: a real live (or migration-file-derived) Postgres foreign-key edge whose two
188
+ tables are declared by two *different* features surfaces as a `db_foreign_key` finding, direction-
189
+ and confidence-scored the same way the existing NAME-identity signals are. Every `fk_check` in the
190
+ report also carries `generated_at` — when the underlying data was actually captured, so a
191
+ `persisted`/`migrations`-mode correlation (reused from an earlier scan, not a fresh connection) can
192
+ be judged for staleness rather than trusted blindly. See `D-cross-feature-fk-inference` in
193
+ `DECISIONS.md`.
194
+
195
+ ### Applying DDL to a live database (optional)
196
+
197
+ `bskel patch propose --kind ddl-apply` extends the same propose/approve/apply/rollback lifecycle
198
+ `config_apply` uses to a second kind: hand-authored `CREATE`/`ALTER`/`DROP TABLE`/`INDEX`/`SCHEMA`
199
+ statements, run inside a real Postgres transaction and only `COMMIT`ted once the introspected
200
+ schema actually matches the declared postcondition — anything else `ROLLBACK`s automatically, never
201
+ partially applies:
202
+
203
+ ```bash
204
+ bskel patch propose --feature 001-organization-management --kind ddl-apply \
205
+ --database-url-env BSKEL_DB_URL --sql-file migrations/add_tax_rate.sql --schema public
206
+ bskel patch approve --feature 001-organization-management --transaction <id> --reason "..."
207
+ bskel patch apply --feature 001-organization-management --transaction <id>
208
+ # a transaction that DROPs one or more tables requires retyping the sorted, comma-joined table
209
+ # name(s) as --confirm instead of the transaction id -- the same "type the resource name to
210
+ # delete" pattern GitHub uses for its own irreversible actions
211
+ ```
212
+
213
+ Rollback of an *applied* `ddl-apply` transaction is refused outright by design — the only path back
214
+ is a new, forward transaction with hand-written reverse DDL, never an automated revert. The
215
+ allowlist structurally excludes anything that can't run inside a transaction block (e.g. `CREATE
216
+ INDEX CONCURRENTLY`) and anything outside `TABLE`/`INDEX`/`SCHEMA` DDL. `bskel serve
217
+ --database-url-env <NAME> [--sign-key <path>] [--require-sign-key]` exposes the same lifecycle
218
+ through the browser UI's own propose/approve/apply routes; `--require-sign-key` refuses to start
219
+ the DDL surface at all unless a signing key was also given. See `D-ddl-apply` in `DECISIONS.md` for
220
+ the full design and every explicitly-deferred boundary (non-Postgres databases, connection
221
+ pooling, production safety rails).
222
+
223
+ ### Declaring field-to-field dependencies (optional)
224
+
225
+ When one feature's data actually depends on another feature's (e.g. a `WidgetDto.name` that's
226
+ populated from `OrganizationDto.taxRate`), `bskel dependency declare` records that link and passes
227
+ the `dependencies` gate for it. Declaring a dependency also warns the *source* feature next time
228
+ someone re-runs `bskel status`/`bskel next` there, so a downstream consumer doesn't silently break:
229
+
230
+ ```bash
231
+ bskel dependency declare --feature 001-widget-management --resource WidgetDto --field name \
232
+ --source-feature 002-organization-management --source-resource OrganizationDto --source-field taxRate \
233
+ --reason "widget display name mirrors the owning org's rate tier" [--memo "..."]
234
+ bskel dependency list --feature 001-widget-management --json
235
+ bskel dependency remove --feature 001-widget-management --resource WidgetDto --field name \
236
+ --source-feature 002-organization-management --source-resource OrganizationDto --source-field taxRate \
237
+ --reason "no longer coupled"
238
+ ```
239
+
240
+ `bskel serve [--port N] [--host <addr>]` starts a small local HTTP server (loopback-only by
241
+ default, matching this project's "safe default, explicit override" convention) that serves a
242
+ read-only browser UI at `/` for the whole repo's dependency graph, backed by `GET /api/graph`. The
243
+ UI's own POST/DELETE calls to `/api/features/:id/dependencies` go through the exact same
244
+ `declareDependency`/`removeDependency` functions the CLI above uses — nothing is duplicated
245
+ between the two — and, like every other mutating command in this project, both take a JSON body,
246
+ not query parameters. `GET`/`HEAD` responses carry `Access-Control-Allow-Origin: *`; the mutating
247
+ routes never do, so only same-origin requests (the bundled UI itself) can write.
248
+
249
+ ### Patching a config file (optional)
250
+
251
+ `bskel stack apply`'s `config_check` sometimes reports `needs-manual-patch` — a target file exists
252
+ but isn't wired up the way the chosen stack (e.g. `ngrok`) needs. `bskel patch propose/approve/
253
+ apply/rollback` closes that gap for the catalog entries that declare a machine-applicable fix,
254
+ using a comment-preserving edit with a content-addressed preimage check (refuses to apply if the
255
+ target changed since you approved it) and a real rollback:
256
+
257
+ ```bash
258
+ bskel patch propose --feature 001-organization-management --choice ngrok \
259
+ --target src/main/resources/application.yaml
260
+ bskel patch approve --feature 001-organization-management --transaction <id> --reason "..."
261
+ bskel patch apply --feature 001-organization-management --transaction <id>
262
+ # ...or, to undo: bskel patch rollback --feature 001-organization-management --transaction <id> --reason "..."
263
+ ```
264
+
265
+ `bskel patch list --feature <id>` shows every transaction and its status. See
266
+ `D-patch-transactions` in `DECISIONS.md`.
267
+
268
+ ### Signed gate attestations (optional)
269
+
270
+ `bskel gate export` already produces a CI-independent report of every gate's current status. Add
271
+ `--sign --key <privateKeyPath>` to detached-sign it (Ed25519, via Node's own `crypto` module — no
272
+ new dependency), then verify it offline, on any machine, without network access or trusting
273
+ whatever produced it:
274
+
275
+ ```bash
276
+ bskel attest keygen --out ~/.bskel-keys # writes attest-private.pem (0600) + attest-public.pem
277
+ bskel gate export --feature 001-organization-management \
278
+ --sign --key ~/.bskel-keys/attest-private.pem --out attestation.json
279
+ bskel attest verify --file attestation.json --pubkey ~/.bskel-keys/attest-public.pem
280
+ ```
281
+
282
+ `attest verify`'s exit code reflects signature validity only — whether the gates inside actually
283
+ passed is a separate, printed summary. See `D-gate-attestation-signing` in `DECISIONS.md`.
284
+
175
285
  Every command is read-only until you explicitly run one of the mutating steps above — `bskel
176
286
  status`/`bskel next` (no arguments needed) tell you which gate is next and print the exact
177
287
  copy-pasteable command for it, without touching anything.