backend-skeleton 1.5.0 → 1.7.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 +113 -8
- package/bin/bskel.mjs +549 -63
- package/contracts/completeness.mjs +12 -1
- package/contracts/openapi.mjs +125 -18
- package/handles/providers/java-spring/ast-bridge.mjs +85 -1
- package/handles/providers/java-spring/ast-helper/src/main/java/com/backendskeleton/asthelper/Main.java +407 -0
- package/handles/providers/java-spring/emit.mjs +126 -6
- package/handles/providers/java-spring/plan.mjs +220 -74
- package/handles/providers/java-spring/source-splice.mjs +477 -0
- package/handles/providers/java-spring/templates/AuthorizationPolicyStub.java.tmpl +30 -0
- package/handles/providers/java-spring/templates/HandleController.java.tmpl +21 -3
- package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +26 -0
- package/handles/providers/java-spring/templates/ResourceResolverPolicyStub.java.tmpl +9 -0
- package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +3 -3
- package/lib/attest.mjs +59 -1
- package/lib/cli.mjs +125 -7
- package/lib/decision-log.mjs +58 -0
- package/lib/doctor.mjs +23 -0
- package/lib/exit-codes.mjs +17 -0
- package/lib/gate-definitions.mjs +65 -2
- package/lib/gate-export.mjs +250 -0
- package/lib/impact-export-graphify.mjs +145 -0
- package/lib/impact-graph.mjs +194 -0
- package/lib/impact-surface.mjs +158 -0
- package/lib/impact.mjs +334 -0
- package/lib/patch-kinds.mjs +24 -0
- package/lib/repo.mjs +46 -0
- package/lib/workflow.mjs +16 -0
- package/package.json +1 -1
- package/scanners/adapters/_java-spring-analyzer.mjs +6 -0
- package/schemas/decision-event.schema.json +46 -0
- package/schemas/gate-attestation.schema.json +6 -1
- package/schemas/gate-export.schema.json +606 -22
- package/schemas/handles-plan.schema.json +32 -0
- package/schemas/impact-baseline.schema.json +59 -0
- package/schemas/impact-graph.schema.json +53 -0
- package/schemas/impact-report.schema.json +86 -0
- package/schemas/impact-resolution.schema.json +33 -0
- package/schemas/java-source-splice.schema.json +84 -0
- 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`)
|
|
81
|
-
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
`
|
|
445
|
-
|
|
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
|