backend-skeleton 1.4.0 → 1.6.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 (42) hide show
  1. package/README.md +113 -8
  2. package/bin/bskel.mjs +331 -53
  3. package/contracts/emit.mjs +22 -6
  4. package/contracts/openapi.mjs +125 -18
  5. package/handles/providers/java-spring/ast-bridge.mjs +85 -1
  6. package/handles/providers/java-spring/ast-helper/src/main/java/com/backendskeleton/asthelper/Main.java +407 -0
  7. package/handles/providers/java-spring/emit.mjs +126 -6
  8. package/handles/providers/java-spring/plan.mjs +244 -77
  9. package/handles/providers/java-spring/source-splice.mjs +477 -0
  10. package/handles/providers/java-spring/templates/AuthorizationPolicyStub.java.tmpl +30 -0
  11. package/handles/providers/java-spring/templates/HandleController.java.tmpl +21 -3
  12. package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +26 -0
  13. package/handles/providers/java-spring/templates/ResourceResolverPolicyStub.java.tmpl +9 -0
  14. package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +3 -3
  15. package/handles/providers/typescript-express/plan.mjs +15 -2
  16. package/lib/attest.mjs +59 -1
  17. package/lib/cli.mjs +79 -7
  18. package/lib/doctor.mjs +23 -0
  19. package/lib/exit-codes.mjs +17 -0
  20. package/lib/gate-definitions.mjs +65 -2
  21. package/lib/gate-export.mjs +199 -0
  22. package/lib/impact-export-graphify.mjs +145 -0
  23. package/lib/impact-graph.mjs +194 -0
  24. package/lib/impact-surface.mjs +158 -0
  25. package/lib/impact.mjs +286 -0
  26. package/lib/patch-kinds.mjs +24 -0
  27. package/lib/repo.mjs +46 -0
  28. package/lib/workflow.mjs +16 -0
  29. package/package.json +1 -1
  30. package/scanners/adapters/_java-spring-analyzer.mjs +55 -0
  31. package/scanners/adapters/java-spring.mjs +154 -3
  32. package/scanners/index.mjs +10 -0
  33. package/schemas/feature-contract.schema.json +12 -1
  34. package/schemas/gate-attestation.schema.json +6 -1
  35. package/schemas/gate-export.schema.json +530 -22
  36. package/schemas/handles-plan.schema.json +32 -0
  37. package/schemas/impact-baseline.schema.json +59 -0
  38. package/schemas/impact-graph.schema.json +53 -0
  39. package/schemas/impact-report.schema.json +86 -0
  40. package/schemas/impact-resolution.schema.json +33 -0
  41. package/schemas/java-source-splice.schema.json +84 -0
  42. package/schemas/patch-transaction.schema.json +87 -2
package/README.md CHANGED
@@ -40,6 +40,7 @@ check for a specific failure mode found the same way — see `DECISIONS.md` for
40
40
  - [Database schema (optional)](#database-schema-optional)
41
41
  - [An ERD of your database schema (optional)](#an-erd-of-your-database-schema-optional)
42
42
  - [Applying DDL to a live database (optional)](#applying-ddl-to-a-live-database-optional)
43
+ - [Splicing real Java source (optional, java-spring only)](#splicing-real-java-source-optional-java-spring-only)
43
44
  - [Declaring field-to-field dependencies (optional)](#declaring-field-to-field-dependencies-optional)
44
45
  - [Patching a config file (optional)](#patching-a-config-file-optional)
45
46
  - [Signed gate attestations (optional)](#signed-gate-attestations-optional)
@@ -77,8 +78,14 @@ production -- which still splits the same way it always has:
77
78
  `DECISIONS.md`'s `D-handle-registry-enforcement`/`D-resolver-authorization-action-aware`. Real
78
79
  gaps remain and are explicitly still open, not closed: registry enforcement is opt-in, off by
79
80
  default; authorization inference now recognizes both `@PreAuthorize(hasRole(...))` and
80
- `hasAuthority(...)` (see `DECISIONS.md`'s `D-resolver-authorization-action-aware`), but
81
- `hasAnyRole`/`hasAnyAuthority` (list-shape), ownership, and tenant policy are still unaddressed.
81
+ `hasAuthority(...)` (see `DECISIONS.md`'s `D-resolver-authorization-action-aware`) via an explicit
82
+ per-action policy contract (`D-resolver-policy-contract`) -- a companion annotation this scanner
83
+ cannot safely evaluate (`@PostAuthorize`, `@Secured`, `@RolesAllowed`, ownership/tenant checks,
84
+ `hasAnyRole`/`hasAnyAuthority`) now refuses auto-materialization and generates an
85
+ `AuthorizationPolicy` interface that blocks the target app from starting until a human implements
86
+ it -- ships default-on, `handles emit` exits non-zero (23) until acknowledged with `--force
87
+ --reason` or the policy is implemented. `hasAnyRole`/`hasAnyAuthority` list-shapes remain
88
+ unaddressed (deliberately, see `D-resolver-policy-contract`'s own EXIT).
82
89
  All three providers (Java/Python/TypeScript) now generate a real `sbf_handle`/
83
90
  `sbf_handle_snapshot` schema and support `--enforce-registry`/`recover()` -- TypeScript's own
84
91
  registration mechanism is a higher-order wrapper function, not a decorator (no Java-AOP or
@@ -376,6 +383,49 @@ the DDL surface at all unless a signing key was also given. See `D-ddl-apply` in
376
383
  the full design and every explicitly-deferred boundary (non-Postgres databases, connection
377
384
  pooling, production safety rails).
378
385
 
386
+ ### Splicing real Java source (optional, java-spring only)
387
+
388
+ `bskel patch propose --kind java-source-splice` extends the same lifecycle to a third kind: a
389
+ closed, four-operation vocabulary for editing REAL, hand-written `.java` source --
390
+ `replace-method-body`, `insert-method-body-prologue`, `replace-field-initializer`, `add-import`.
391
+ A member is located by its language-guaranteed identity (a type's fully-qualified name plus, for a
392
+ method, its erased parameter types — the same rule javac itself uses to forbid two overloads
393
+ sharing one erased signature), resolved via a real JavaParser+Symbol Solver pass and cross-checked
394
+ by an independent, non-AST falsifier before anything is written. The postcondition is a real
395
+ `./gradlew compileJava`; a failure automatically restores the original file, never leaves a broken
396
+ one in place:
397
+
398
+ ```bash
399
+ cat > splice.json <<'EOF'
400
+ {
401
+ "schema": "sbf.java-source-splice/1",
402
+ "file": "src/main/java/com/example/demo/domain/widget/application/WidgetServiceImpl.java",
403
+ "edits": [{
404
+ "op": "replace-method-body",
405
+ "locator": {
406
+ "type_fqn": "com.example.demo.domain.widget.application.WidgetServiceImpl",
407
+ "member_kind": "method",
408
+ "member_name": "updateWidget",
409
+ "erased_param_types": ["java.util.UUID", "com.example.demo.domain.widget.presentation.dto.UpdateWidgetRequest"]
410
+ },
411
+ "replacement": "{\n\t\treturn widgetRepository.save(findWidget(widgetId));\n\t}"
412
+ }]
413
+ }
414
+ EOF
415
+ bskel patch propose --feature 001-widget-management --kind java-source-splice --splice-file splice.json
416
+ bskel patch approve --feature 001-widget-management --transaction <id> --reason "..."
417
+ bskel patch apply --feature 001-widget-management --transaction <id> --confirm WidgetServiceImpl#updateWidget
418
+ ```
419
+
420
+ Every edit outside the four ops refuses outright (no member add/remove/rename, no signature/
421
+ annotation edits, no nested/inner/anonymous/local types, no multi-top-level-type files, no
422
+ `apply-diff`) — there is no general-purpose patch/diff applier here, only grammar-delimited,
423
+ independently-verified regions. Requires the bundled AST helper (`bskel doctor` reports readiness
424
+ under "java-source-splice prerequisites"). Python/TypeScript source splicing is not supported —
425
+ neither has an equivalent AST helper or whole-project compile check in this repo. See
426
+ `D-java-source-splice` in `DECISIONS.md` for the full three-mechanism node-identity design and
427
+ every explicitly-deferred boundary.
428
+
379
429
  ### Declaring field-to-field dependencies (optional)
380
430
 
381
431
  When one feature's data actually depends on another feature's (e.g. a `WidgetDto.name` that's
@@ -393,6 +443,44 @@ bskel dependency remove --feature 001-widget-management --resource WidgetDto --f
393
443
  --reason "no longer coupled"
394
444
  ```
395
445
 
446
+ ### Cross-feature impact graph: field-level change detection with a mandatory disposition (optional)
447
+
448
+ `bskel dependency declare` above records that an edge EXISTS; it does not, by itself, tell the
449
+ *source* feature when its own contract/resource shape actually changes in a way that breaks the
450
+ downstream side. `bskel impact check`/`bskel impact accept` close that gap -- an `impact` gate,
451
+ complementing `dependencies`, that blocks the feature that CHANGED (not just the one depending on
452
+ it) until every downstream impact has an explicit `compatible`/`migrate`/`waive` disposition:
453
+
454
+ ```bash
455
+ bskel impact accept --feature 002-organization-management # capture the first baseline
456
+ # ... later, OrganizationDto.taxRate's backing file changes ...
457
+ bskel impact check --feature 002-organization-management --json # names the exact change_key + downstream feature
458
+ bskel impact disposition --feature 002-organization-management \
459
+ --change <change_key> --downstream 001-widget-management \
460
+ --mode compatible --reason "purely additive" # or --mode migrate --tracked-by "ISSUE-42"
461
+ # or --mode waive --expires-days 14
462
+ bskel impact accept --feature 002-organization-management # unblocked; advances the baseline
463
+ bskel impact check --all --json # the recommended CI invocation -- sweeps every feature
464
+ ```
465
+
466
+ A `migrate` disposition creates a real two-sided handshake: it records an INBOUND obligation on the
467
+ downstream feature (`001-widget-management` here), whose own `impact` gate stays blocked until it
468
+ runs `bskel impact ack --feature 001-widget-management --from 002-organization-management --change
469
+ <change_key> --reason "..."`. Every disposition key embeds a hash of the NEW change, so a
470
+ disposition recorded for one shape never silently covers a later, different change to the same
471
+ field -- there is no wildcard. Only `proven` (exact-identity) impacts block; `heuristic` ones
472
+ (a guessed table name, a lower-confidence collision) are always reported, never blocking on their
473
+ own. Not a `handles emit` prerequisite (gated only at `bskel verify`) -- this repo's own
474
+ `cross_feature`-gate CI incident is why. See `D-cross-feature-impact-graph` in `DECISIONS.md`.
475
+
476
+ `bskel impact export --format graphify|json|mermaid [--out <path>] [--focus <node-id>] [--rings N]`
477
+ is the one LLM-free seam to an exploration layer: `--format graphify` writes the locally-installed
478
+ `graphify` skill's own native extraction file shape directly (no install/detect/extract steps, no
479
+ LLM call, no network) -- a consumer runs `graphify.build.build_from_json()` + `cluster()` +
480
+ `to_obsidian()` on it to get a browsable Obsidian vault of the whole cross-feature graph.
481
+ `--focus`/`--rings` write a Focus+Context data projection (`ring`/`detail`, inspired by Lamping/
482
+ Rao/Pirolli's 1995 hyperbolic-tree technique) into the export only -- never read back by any gate.
483
+
396
484
  `bskel serve [--port N] [--host <addr>]` starts a small local HTTP server (loopback-only by
397
485
  default, matching this project's "safe default, explicit override" convention) that serves a
398
486
  read-only browser UI at `/` for the whole repo's dependency graph, backed by `GET /api/graph`. The
@@ -429,10 +517,13 @@ bskel patch apply --feature 001-organization-management --transaction <id>
429
517
 
430
518
  ### Signed gate attestations (optional)
431
519
 
432
- `bskel gate export` already produces a CI-independent report of every gate's current status. Add
433
- `--sign --key <privateKeyPath>` to detached-sign it (Ed25519, via Node's own `crypto` module — no
434
- new dependency), then verify it offline, on any machine, without network access or trusting
435
- whatever produced it:
520
+ `bskel gate export` already produces a CI-independent report of every gate's current status,
521
+ including a `live` verdict RECOMPUTED at export time (so a gate whose stored record still says
522
+ `pass` but whose inputs have since changed is honestly reported as `stale`, never silently signed
523
+ as passing), the tool's own version, git tree identity, unconditional artifact hashes, and a
524
+ forced/revoked/waiver roll-up. Add `--sign --key <privateKeyPath>` to detached-sign it (Ed25519,
525
+ via Node's own `crypto` module — no new dependency), then verify it offline, on any machine,
526
+ without network access or trusting whatever produced it:
436
527
 
437
528
  ```bash
438
529
  bskel attest keygen --out ~/.bskel-keys # writes attest-private.pem (0600) + attest-public.pem
@@ -441,8 +532,16 @@ bskel gate export --feature 001-organization-management \
441
532
  bskel attest verify --file attestation.json --pubkey ~/.bskel-keys/attest-public.pem
442
533
  ```
443
534
 
444
- `attest verify`'s exit code reflects signature validity only — whether the gates inside actually
445
- passed is a separate, printed summary. See `D-gate-attestation-signing` in `DECISIONS.md`.
535
+ `--sign` refuses a dirty working tree (same `--allow-dirty` convention `bskel preflight` already
536
+ uses) — the acknowledgement, if you pass it, is recorded inside the signed payload itself, not just
537
+ a flag you happened to type. `attest verify`'s exit code reflects signature validity only — whether
538
+ the gates inside actually passed is a separate, printed summary. Three opt-in, default-off checks
539
+ narrow what "valid" is allowed to mean for your use case without touching that exit-code contract:
540
+ `--expect-head <sha>` (refuse an attestation about the wrong commit), `--max-age-minutes N`
541
+ (refuse a stale one), `--reject-dirty` (refuse one signed over an acknowledged-dirty tree) — each
542
+ failing exits `22`, distinct from `1` (signature invalid), so "authentic but not what you asked
543
+ for" is never confused with "not authentic". See `D-gate-attestation-signing` and
544
+ `D-attestation-payload-completeness` in `DECISIONS.md`.
446
545
 
447
546
  ### Signed observe receipts (optional)
448
547
 
@@ -552,6 +651,12 @@ checking" pass). Highlights (full record in `DECISIONS.md`'s "Security hardening
552
651
  - **Authority derivation is per-method, not per-file** — a controller's first `@PreAuthorize` match
553
652
  no longer silently applies to every resolver generated from that file; an unsupported annotation
554
653
  shape (`hasAnyRole`, SpEL) fails closed to a `TODO_ROLE` placeholder rather than guessing.
654
+ - **A companion authorization annotation (`@PostAuthorize`, `@Secured`, `@RolesAllowed`) next to an
655
+ otherwise-safe `@PreAuthorize` refuses auto-materialization entirely** (`D-resolver-policy-contract`)
656
+ — a real, closed IDOR: `@PreAuthorize("hasRole('USER')")` sitting beside
657
+ `@PostAuthorize("returnObject.ownerId == authentication.name")` used to auto-materialize `ROLE_USER`
658
+ and silently ignore the ownership check. `handles emit` now generates an `AuthorizationPolicy`
659
+ interface that blocks the target app from starting (not the build) until a human implements it.
555
660
  - **Handle recovery cross-checks type/kind/pointer against the registry row**, not just the raw
556
661
  UUID — the most severe finding: an attacker who controls the handle's `type` field could
557
662
  otherwise request a different, more sensitive resource's snapshot history that happens to share