backend-skeleton 1.0.0-beta.9 → 1.1.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 (77) hide show
  1. package/README.md +185 -13
  2. package/bin/bskel.mjs +689 -33
  3. package/contracts/emit.mjs +5 -1
  4. package/contracts/export.mjs +65 -7
  5. package/contracts/openapi.mjs +321 -30
  6. package/contracts/validate.mjs +23 -4
  7. package/handles/_engine.mjs +123 -30
  8. package/handles/capability-codec.mjs +94 -0
  9. package/handles/codec.mjs +13 -3
  10. package/handles/providers/java-spring/emit.mjs +78 -33
  11. package/handles/providers/java-spring/observe.mjs +4 -3
  12. package/handles/providers/java-spring/plan.mjs +73 -16
  13. package/handles/providers/java-spring/templates/HandleCodec.java.tmpl +19 -1
  14. package/handles/providers/java-spring/templates/HandleController.java.tmpl +19 -9
  15. package/handles/providers/java-spring/templates/HandleService.java.tmpl +21 -2
  16. package/handles/providers/java-spring/templates/RecordHandleSnapshot.java.tmpl +1 -1
  17. package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +36 -9
  18. package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +8 -2
  19. package/handles/providers/java-spring.mjs +8 -0
  20. package/handles/providers/python-fastapi/emit.mjs +21 -26
  21. package/handles/providers/python-fastapi/observe.mjs +6 -5
  22. package/handles/providers/python-fastapi/templates/codec.py.tmpl +18 -3
  23. package/handles/providers/python-fastapi/templates/record_snapshot.py.tmpl +129 -39
  24. package/handles/providers/python-fastapi.mjs +3 -3
  25. package/handles/providers/typescript-express/emit.mjs +144 -55
  26. package/handles/providers/typescript-express/observe.mjs +102 -0
  27. package/handles/providers/typescript-express/templates/codec.ts.tmpl +13 -3
  28. package/handles/providers/typescript-express/templates/contractCheck.ts.tmpl +136 -0
  29. package/handles/providers/typescript-express/templates/handleEntities.ts.tmpl +89 -0
  30. package/handles/providers/typescript-express/templates/handleService.ts.tmpl +81 -0
  31. package/handles/providers/typescript-express/templates/migration.sql.tmpl +36 -0
  32. package/handles/providers/typescript-express/templates/observeContract.ts.tmpl +146 -0
  33. package/handles/providers/typescript-express/templates/observedSchema.ts.tmpl +116 -0
  34. package/handles/providers/typescript-express/templates/recordSnapshotWrapper.ts.tmpl +123 -0
  35. package/handles/providers/typescript-express/templates/registry.ts.tmpl +19 -10
  36. package/handles/providers/typescript-express/templates/resolver.ts.tmpl +13 -0
  37. package/handles/providers/typescript-express/templates/resolverPolicy.ts.tmpl +20 -0
  38. package/handles/providers/typescript-express/templates/router.ts.tmpl +113 -2
  39. package/handles/providers/typescript-express.mjs +7 -4
  40. package/lib/attest.mjs +40 -0
  41. package/lib/cli.mjs +136 -5
  42. package/lib/cross-feature-collisions.mjs +286 -0
  43. package/lib/diff.mjs +35 -0
  44. package/lib/exit-codes.mjs +21 -0
  45. package/lib/fsutil.mjs +7 -2
  46. package/lib/gate-definitions.mjs +85 -1
  47. package/lib/gates.mjs +5 -1
  48. package/lib/http-server.mjs +192 -6
  49. package/lib/lock.mjs +68 -15
  50. package/lib/patch-kinds.mjs +52 -0
  51. package/lib/patch-transactions.mjs +206 -0
  52. package/lib/serve-ui.html +211 -0
  53. package/lib/verify.mjs +23 -6
  54. package/lib/workflow.mjs +31 -3
  55. package/package.json +8 -2
  56. package/scanners/adapters/_java-spring-analyzer.mjs +9 -1
  57. package/scanners/adapters/java-spring.mjs +114 -10
  58. package/scanners/adapters/javascript-express.mjs +46 -13
  59. package/scanners/adapters/python-fastapi.mjs +9 -1
  60. package/scanners/adapters/typescript-express.mjs +19 -2
  61. package/scanners/db/ddl-apply.mjs +253 -0
  62. package/scanners/db/introspect.mjs +61 -32
  63. package/scanners/db/migrations.mjs +73 -18
  64. package/schemas/cross-feature-report.schema.json +66 -0
  65. package/schemas/cross-feature-resolution.schema.json +28 -0
  66. package/schemas/feature-contract.schema.json +3 -3
  67. package/schemas/gate-attestation.schema.json +22 -0
  68. package/schemas/gate-export.schema.json +58 -0
  69. package/schemas/handles-plan.schema.json +2 -0
  70. package/schemas/oracle-manifest.schema.json +58 -0
  71. package/schemas/patch-transaction.schema.json +182 -0
  72. package/schemas/scan-report.schema.json +6 -4
  73. package/schemas/stack-choice.schema.json +12 -1
  74. package/schemas/stack-record.schema.json +6 -1
  75. package/stack/apply.mjs +51 -7
  76. package/stack/catalog/ngrok.yml +8 -2
  77. package/stack/config-apply.mjs +168 -0
package/README.md CHANGED
@@ -23,10 +23,43 @@ 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
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:
26
+ ![bskel run against a real fixture repo: preflight, a brownfield collision scan, and a feature status gate table](https://raw.githubusercontent.com/popixoxipop-collab/backend-skeleton/main/docs/demo.gif)
27
+
28
+ *A real terminal, a real `bskel` binary, a real fixture repo — not a scripted transcript. Source:
29
+ [`docs/demo.tape`](docs/demo.tape), regenerated with [`docs/record-demo.sh`](docs/record-demo.sh).*
30
+
31
+ ## Contents
32
+
33
+ - [Status: 1.0.0](#status-100)
34
+ - [Quickstart](#quickstart)
35
+ - [Starting from nothing (greenfield)](#starting-from-nothing-greenfield)
36
+ - [Publishing a feature's contract as OpenAPI (optional)](#publishing-a-features-contract-as-openapi-optional)
37
+ - [Database schema (optional)](#database-schema-optional)
38
+ - [Applying DDL to a live database (optional)](#applying-ddl-to-a-live-database-optional)
39
+ - [Declaring field-to-field dependencies (optional)](#declaring-field-to-field-dependencies-optional)
40
+ - [Patching a config file (optional)](#patching-a-config-file-optional)
41
+ - [Signed gate attestations (optional)](#signed-gate-attestations-optional)
42
+ - [Compatibility](#compatibility)
43
+ - [Generated-file policy](#generated-file-policy)
44
+ - [Security model](#security-model)
45
+ - [Troubleshooting](#troubleshooting)
46
+ - [What ships in the package](#what-ships-in-the-package)
47
+ - [License](#license)
48
+
49
+ ## Status: 1.0.0
50
+
51
+ As of `1.0.0`, this project makes an explicit API-stability promise: **`bskel`'s CLI surface --
52
+ command/flag names and their meaning, every `--json` output shape (all schemas under `schemas/`),
53
+ gate names and pass/fail semantics, and exit codes -- is stable.** A change that breaks any of
54
+ those requires a major version bump, never a patch or minor release; a new command, a new optional
55
+ flag, or a new additive field on an existing JSON shape is always minor-version-safe (every schema
56
+ under `schemas/` is `additionalProperties: false` specifically so a genuinely new field shows up as
57
+ a real, visible schema change rather than something an existing consumer could silently miss). See
58
+ `D-stable-api-contract` in `DECISIONS.md` for the full policy and what's explicitly excluded from
59
+ it.
60
+
61
+ That promise is about the *interface*, not a claim that every subsystem has been proven in
62
+ production -- which still splits the same way it always has:
30
63
 
31
64
  - **`scan`, `contract` (including `export`), and `new`** are the most exercised paths — real,
32
65
  measured verification against a real production Spring Boot repo (see `DECISIONS.md`), plus a
@@ -38,19 +71,20 @@ stable. Split by actual maturity, not by feature list:
38
71
  independently correct roles instead of silently sharing one) are both implemented -- see
39
72
  `DECISIONS.md`'s `D-handle-registry-enforcement`/`D-resolver-authorization-action-aware`. Real
40
73
  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.
74
+ default; authorization inference now recognizes both `@PreAuthorize(hasRole(...))` and
75
+ `hasAuthority(...)` (see `DECISIONS.md`'s `D-resolver-authorization-action-aware`), but
76
+ `hasAnyRole`/`hasAnyAuthority` (list-shape), ownership, and tenant policy are still unaddressed.
77
+ All three providers (Java/Python/TypeScript) now generate a real `sbf_handle`/
78
+ `sbf_handle_snapshot` schema and support `--enforce-registry`/`recover()` -- TypeScript's own
79
+ registration mechanism is a higher-order wrapper function, not a decorator (no Java-AOP or
80
+ Python-decorator equivalent exists in this ecosystem the templates could safely rely on; see
81
+ `D-typescript-express-registry-parity` in `DECISIONS.md`). Treat `handles emit`'s output as a
82
+ scaffold to finish by hand, not a production-ready subsystem, until a real deployment happens.
49
83
 
50
84
  ## Quickstart
51
85
 
52
86
  ```bash
53
- npm install -g backend-skeleton@beta # or: npx backend-skeleton@beta <command>
87
+ npm install -g backend-skeleton # or: npx backend-skeleton <command>
54
88
  cd <target-repo> # must be a git repository
55
89
 
56
90
  bskel doctor # what's on PATH, which scanner adapter detects this repo, and why
@@ -66,6 +100,9 @@ bskel scan disposition --feature 001-organization-management --mode reuse --note
66
100
 
67
101
  bskel contract emit --feature 001-organization-management
68
102
  # feature_id-scoped JSON Schema contract, from real source annotations
103
+ bskel scan cross-feature-check --feature 001-organization-management
104
+ # refuses to proceed if this feature's resourceType/table/operationId
105
+ # collides with another feature -- required before handles emit
69
106
  bskel handles plan --feature 001-organization-management
70
107
  bskel handles emit --feature 001-organization-management
71
108
  # UUID-addressable field handles + generated resolver code
@@ -75,6 +112,36 @@ bskel verify --feature 001-organization-management --build
75
112
  # target repo's own build wrapper (gradlew/mvnw/npm), if present
76
113
  ```
77
114
 
115
+ `bskel status`/`bskel next` are what you actually run over and over — real output, captured against
116
+ a fixture repo partway through the flow above, not written by hand:
117
+
118
+ ```text
119
+ $ bskel status --feature 001-organization-management
120
+ # Status: 001-organization-management
121
+
122
+ ## Gates
123
+ - [PASS] preflight
124
+ - [PASS] scan
125
+ - [(not_run)] cross_feature (required-when-present, feature-scoped)
126
+ - [BLOCKING] contract
127
+ - [(not_run)] dependencies (required-when-present, feature-scoped)
128
+ - [(not_run)] handles (required-when-present, feature-scoped)
129
+ - [(not_run)] stack (required-when-present, repo-scoped)
130
+ - [(not_run)] patch_transactions (required-when-present, feature-scoped)
131
+ - [(not_run)] conformance (required-when-present, feature-scoped)
132
+
133
+ ## Artifacts
134
+ - [OK] contract: specs/001-organization-management/contracts/001-organization-management.schema.json
135
+
136
+ ## Next
137
+ - bskel contract waive --feature 001-organization-management --code <CODE> (--subject "..."|--all) --reason "..." # or: bskel gate force contract --feature 001-organization-management --reason "..." if intentional # contract gate is awaiting disposition
138
+
139
+ ## Optional, not yet run: handles, stack
140
+ ```
141
+
142
+ Every gate line is a real, disk-verified check — the `Next` line is always the exact command to
143
+ unblock whatever's currently `BLOCKING`, so there's no separate doc to cross-reference mid-workflow.
144
+
78
145
  ### Starting from nothing (greenfield)
79
146
 
80
147
  Every command above assumes an existing Spring Boot or FastAPI repo. If you don't have one yet:
@@ -172,6 +239,111 @@ read from `.env`) for live, read-only Postgres introspection (`information_schem
172
239
  inside a `BEGIN TRANSACTION READ ONLY`) and a source-vs-live drift report. Both are informational
173
240
  additions to the scan report — neither blocks any gate. See `D-db-schema-plane` in `DECISIONS.md`.
174
241
 
242
+ The same `--db`/`--database-url-env` flags, passed to `bskel scan cross-feature-check`, add a 4th
243
+ collision signal: a real live (or migration-file-derived) Postgres foreign-key edge whose two
244
+ tables are declared by two *different* features surfaces as a `db_foreign_key` finding, direction-
245
+ and confidence-scored the same way the existing NAME-identity signals are. Every `fk_check` in the
246
+ report also carries `generated_at` — when the underlying data was actually captured, so a
247
+ `persisted`/`migrations`-mode correlation (reused from an earlier scan, not a fresh connection) can
248
+ be judged for staleness rather than trusted blindly. See `D-cross-feature-fk-inference` in
249
+ `DECISIONS.md`.
250
+
251
+ ### Applying DDL to a live database (optional)
252
+
253
+ `bskel patch propose --kind ddl-apply` extends the same propose/approve/apply/rollback lifecycle
254
+ `config_apply` uses to a second kind: hand-authored `CREATE`/`ALTER`/`DROP TABLE`/`INDEX`/`SCHEMA`
255
+ statements, run inside a real Postgres transaction and only `COMMIT`ted once the introspected
256
+ schema actually matches the declared postcondition — anything else `ROLLBACK`s automatically, never
257
+ partially applies:
258
+
259
+ ```bash
260
+ bskel patch propose --feature 001-organization-management --kind ddl-apply \
261
+ --database-url-env BSKEL_DB_URL --sql-file migrations/add_tax_rate.sql --schema public
262
+ bskel patch approve --feature 001-organization-management --transaction <id> --reason "..."
263
+ bskel patch apply --feature 001-organization-management --transaction <id>
264
+ # a transaction that DROPs one or more tables requires retyping the sorted, comma-joined table
265
+ # name(s) as --confirm instead of the transaction id -- the same "type the resource name to
266
+ # delete" pattern GitHub uses for its own irreversible actions
267
+ ```
268
+
269
+ Rollback of an *applied* `ddl-apply` transaction is refused outright by design — the only path back
270
+ is a new, forward transaction with hand-written reverse DDL, never an automated revert. The
271
+ allowlist structurally excludes anything that can't run inside a transaction block (e.g. `CREATE
272
+ INDEX CONCURRENTLY`) and anything outside `TABLE`/`INDEX`/`SCHEMA` DDL. `bskel serve
273
+ --database-url-env <NAME> [--sign-key <path>] [--require-sign-key]` exposes the same lifecycle
274
+ through the browser UI's own propose/approve/apply routes; `--require-sign-key` refuses to start
275
+ the DDL surface at all unless a signing key was also given. See `D-ddl-apply` in `DECISIONS.md` for
276
+ the full design and every explicitly-deferred boundary (non-Postgres databases, connection
277
+ pooling, production safety rails).
278
+
279
+ ### Declaring field-to-field dependencies (optional)
280
+
281
+ When one feature's data actually depends on another feature's (e.g. a `WidgetDto.name` that's
282
+ populated from `OrganizationDto.taxRate`), `bskel dependency declare` records that link and passes
283
+ the `dependencies` gate for it. Declaring a dependency also warns the *source* feature next time
284
+ someone re-runs `bskel status`/`bskel next` there, so a downstream consumer doesn't silently break:
285
+
286
+ ```bash
287
+ bskel dependency declare --feature 001-widget-management --resource WidgetDto --field name \
288
+ --source-feature 002-organization-management --source-resource OrganizationDto --source-field taxRate \
289
+ --reason "widget display name mirrors the owning org's rate tier" [--memo "..."]
290
+ bskel dependency list --feature 001-widget-management --json
291
+ bskel dependency remove --feature 001-widget-management --resource WidgetDto --field name \
292
+ --source-feature 002-organization-management --source-resource OrganizationDto --source-field taxRate \
293
+ --reason "no longer coupled"
294
+ ```
295
+
296
+ `bskel serve [--port N] [--host <addr>]` starts a small local HTTP server (loopback-only by
297
+ default, matching this project's "safe default, explicit override" convention) that serves a
298
+ read-only browser UI at `/` for the whole repo's dependency graph, backed by `GET /api/graph`. The
299
+ UI's own POST/DELETE calls to `/api/features/:id/dependencies` go through the exact same
300
+ `declareDependency`/`removeDependency` functions the CLI above uses — nothing is duplicated
301
+ between the two — and, like every other mutating command in this project, both take a JSON body,
302
+ not query parameters. `GET`/`HEAD` responses carry `Access-Control-Allow-Origin: *`; the mutating
303
+ routes never do, so only same-origin requests (the bundled UI itself) can write.
304
+
305
+ ![bskel serve's dependency-graph UI, showing a real declared field-to-field dependency resolved as synced](https://raw.githubusercontent.com/popixoxipop-collab/backend-skeleton/main/docs/serve-ui.png)
306
+
307
+ *The page's own header describes it honestly: "Not a redesign of the original Fieldwire mockup --
308
+ this exists to prove the API actually works, nothing more." It's a minimal read-only check page, not
309
+ a polished dashboard — every table on it comes straight from `GET /api/graph`.*
310
+
311
+ ### Patching a config file (optional)
312
+
313
+ `bskel stack apply`'s `config_check` sometimes reports `needs-manual-patch` — a target file exists
314
+ but isn't wired up the way the chosen stack (e.g. `ngrok`) needs. `bskel patch propose/approve/
315
+ apply/rollback` closes that gap for the catalog entries that declare a machine-applicable fix,
316
+ using a comment-preserving edit with a content-addressed preimage check (refuses to apply if the
317
+ target changed since you approved it) and a real rollback:
318
+
319
+ ```bash
320
+ bskel patch propose --feature 001-organization-management --choice ngrok \
321
+ --target src/main/resources/application.yaml
322
+ bskel patch approve --feature 001-organization-management --transaction <id> --reason "..."
323
+ bskel patch apply --feature 001-organization-management --transaction <id>
324
+ # ...or, to undo: bskel patch rollback --feature 001-organization-management --transaction <id> --reason "..."
325
+ ```
326
+
327
+ `bskel patch list --feature <id>` shows every transaction and its status. See
328
+ `D-patch-transactions` in `DECISIONS.md`.
329
+
330
+ ### Signed gate attestations (optional)
331
+
332
+ `bskel gate export` already produces a CI-independent report of every gate's current status. Add
333
+ `--sign --key <privateKeyPath>` to detached-sign it (Ed25519, via Node's own `crypto` module — no
334
+ new dependency), then verify it offline, on any machine, without network access or trusting
335
+ whatever produced it:
336
+
337
+ ```bash
338
+ bskel attest keygen --out ~/.bskel-keys # writes attest-private.pem (0600) + attest-public.pem
339
+ bskel gate export --feature 001-organization-management \
340
+ --sign --key ~/.bskel-keys/attest-private.pem --out attestation.json
341
+ bskel attest verify --file attestation.json --pubkey ~/.bskel-keys/attest-public.pem
342
+ ```
343
+
344
+ `attest verify`'s exit code reflects signature validity only — whether the gates inside actually
345
+ passed is a separate, printed summary. See `D-gate-attestation-signing` in `DECISIONS.md`.
346
+
175
347
  Every command is read-only until you explicitly run one of the mutating steps above — `bskel
176
348
  status`/`bskel next` (no arguments needed) tell you which gate is next and print the exact
177
349
  copy-pasteable command for it, without touching anything.