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
@@ -31,8 +31,12 @@ export const CONTRACT_SCHEMA_VERSION = '8';
31
31
  // item. `pathParamsHeuristic` names every segment that still fell back, so a downstream consumer
32
32
  // (contracts/export.mjs's collectOmissions()) can tell, per operation, whether ANY segment is still
33
33
  // a guess -- `null` (never `[]`) when every segment was source-resolved or the route has none.
34
+ // Update (D-openapi-path-params, closing the O8 typescript-express port's own Finding 2): the
35
+ // regex also recognizes Express's own `:name`/`:name(...)` segment syntax now, not just OpenAPI/
36
+ // Spring/FastAPI-style `{name}` -- additive-only for java-spring/python-fastapi (their own route
37
+ // strings never contain a colon in this position).
34
38
  function pathParamsSchema(routePath, sourcePathParamSchemas = null) {
35
- const params = [...routePath.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
39
+ const params = [...routePath.matchAll(/\{(\w+)\}|:(\w+)(?:\([^)]*\))?/g)].map((m) => m[1] ?? m[2]);
36
40
  const properties = {};
37
41
  const heuristicNames = [];
38
42
  for (const p of params) {
@@ -82,8 +82,18 @@ const ERROR_RESPONSE_DESCRIPTION = 'Error. The source contract records the union
82
82
  // meaning moves from "never built" to "content-AND-flag-conditional"), while `title`/`examples`
83
83
  // (plural)/`externalDocs`/`xml`/`deprecated` move to a new, narrower structural entry
84
84
  // (`field-metadata`) -- measured 0 real occurrences each against the Team-IZ-Backend oracle, so
85
- // they stay permanently unbuilt on the same "don't build for zero real cases" grounds as the two
86
- // A8 entries below, not this item's scope. What else stays structural: `vendor-extensions` (x-*
85
+ // they stayed unbuilt on the same "don't build for zero real cases" grounds as the two A8 entries
86
+ // below, not this item's scope.
87
+ // A14 (D-openapi-field-metadata-passthrough): D-oracle-corpus-openapi-remeasurement (ROADMAP
88
+ // Phase 5c) found `title`/plural `examples`/`deprecated` DO occur for real against a much larger
89
+ // second corpus (polarsource/polar, 1046 component schemas vs 308) -- those 3 of the original 5
90
+ // `field-metadata` keywords are now conditionally copied (contracts/openapi.mjs's
91
+ // DOCUMENTATION_KEYWORDS), same "gated on --descriptions" doctrine as A11's own description/
92
+ // example. `field-metadata` migrates from a STRUCTURAL (always-present) omission to an ANY-based
93
+ // one below -- the exact same migration A10 made for `operation-descriptions` when it left the
94
+ // original single `descriptions` structural entry. `externalDocs`/`xml` remain 0 real occurrences
95
+ // even at Polar's scale, so THOSE two keep the narrower structural entry
96
+ // `external-docs-and-xml-metadata`. What else stays structural: `vendor-extensions` (x-*
87
97
  // keys on an operation are never copied -- excluded in principle, not by cap or failure, since
88
98
  // their semantics are tool-specific), and two A8 additions: `non-json-response-schemas` (a non-JSON
89
99
  // response media type's NAME is copied via a per-status entry's `mediaTypes`, but its SHAPE is never
@@ -92,7 +102,7 @@ const ERROR_RESPONSE_DESCRIPTION = 'Error. The source contract records the union
92
102
  // -- 0/694 real occurrences, a genuinely visible gap only now that per-status responses look
93
103
  // complete).
94
104
  const STRUCTURAL_OMISSIONS = Object.freeze([
95
- 'field-metadata',
105
+ 'external-docs-and-xml-metadata',
96
106
  'non-json-response-schemas',
97
107
  'response-headers',
98
108
  'vendor-extensions',
@@ -102,7 +112,8 @@ const OMISSION_PROSE = Object.freeze({
102
112
  'cookie-parameters': 'cookie parameters, for at least one operation that does not carry a fully-copied set (never emitted at all when --openapi-file was not given, or the source document declared none)',
103
113
  'error-schemas': 'a JSON error-body schema for at least one operation',
104
114
  'field-descriptions': 'a schema field\'s own `description`/`example` (a property\'s own annotation, distinct from the operation-level `description` field -- see `operation-descriptions` below), for at least one field in the request-body/response/error schema of at least one operation -- copied only when `contract emit --descriptions` was used (the same flag as operation-level description) AND the source declared one for that exact field AND it did not exceed the length/size cap; otherwise the field carries no `description`/`example` key, never synthesized. Not tracked separately for per-status responses, non-JSON request media types, or path-parameter schemas -- those may carry field docs when the flag is on, but their presence is not reflected in this specific omission entry',
105
- 'field-metadata': 'a schema field\'s `title`, plural `examples`, `externalDocs`, `xml`, or `deprecated` keyword -- dropped unconditionally while inlining a schema (contracts/openapi.mjs\'s DROPPED_KEYWORDS), regardless of `--descriptions`. Permanently unbuilt: 0 real occurrences of any of these five measured against the Team-IZ-Backend oracle',
115
+ 'field-metadata': 'a schema field\'s `title`, plural `examples`, or `deprecated` keyword, for at least one field in the request-body/response/error schema of at least one operation -- copied only when `--descriptions` is passed (contracts/openapi.mjs\'s DOCUMENTATION_KEYWORDS, A14/D-openapi-field-metadata-passthrough), same doctrine as `field-descriptions`. Present whenever the flag was not used at all, none of this operation\'s projected schemas carry any of the three, or the value exceeded its cap (MAX_TITLE_LENGTH/MAX_EXAMPLES_ARRAY_LENGTH/MAX_EXAMPLE_LENGTH)',
116
+ 'external-docs-and-xml-metadata': 'a schema field\'s `externalDocs` or `xml` keyword -- dropped unconditionally while inlining a schema (contracts/openapi.mjs\'s DROPPED_KEYWORDS), regardless of `--descriptions`. Permanently unbuilt: 0 real occurrences of either keyword measured against either real corpus (the original Team-IZ-Backend oracle or the larger polarsource/polar re-measurement) -- see D-oracle-corpus-openapi-remeasurement in DECISIONS.md',
106
117
  'header-parameters': 'header parameters, for at least one operation that does not carry a fully-copied set (never emitted at all when --openapi-file was not given, or the source document declared none)',
107
118
  'non-json-request-media-types': 'the media type of the request body, for at least one operation that takes one -- a non-application/json request media type is emitted only when a real source document declared one for that exact operation, copied byte-for-byte; otherwise this document shows a JSON media-type entry because that is all the contract knows, never because the real body is known to be JSON',
108
119
  'non-json-response-schemas': 'a JSON Schema for any response body in a media type other than application/json -- the media type is named where a source document declared one for that status, but its shape is never projected',
@@ -145,6 +156,26 @@ function schemaHasFieldDocs(node, seen = new Set()) {
145
156
  return false;
146
157
  }
147
158
 
159
+ // A14 (D-openapi-field-metadata-passthrough): the exact same recursive shape as schemaHasFieldDocs
160
+ // above, checking the OTHER three DOCUMENTATION_KEYWORDS (title/examples/deprecated) instead of
161
+ // description/example -- kept as a separate function rather than merged into schemaHasFieldDocs
162
+ // so the two disclosure keys (`field-descriptions` vs `field-metadata`) stay independently
163
+ // derived from what's ACTUALLY in the projected schema, not conflated into one flag a caller
164
+ // can't tell apart.
165
+ function schemaHasFieldMetadata(node, seen = new Set()) {
166
+ if (node === null || typeof node !== 'object' || Array.isArray(node) || seen.has(node)) return false;
167
+ seen.add(node);
168
+ if (typeof node.title === 'string' || Object.hasOwn(node, 'examples') || Object.hasOwn(node, 'deprecated')) return true;
169
+ if (node.properties && typeof node.properties === 'object' && !Array.isArray(node.properties)) {
170
+ for (const propSchema of Object.values(node.properties)) {
171
+ if (schemaHasFieldMetadata(propSchema, seen)) return true;
172
+ }
173
+ }
174
+ if (node.items && typeof node.items === 'object' && schemaHasFieldMetadata(node.items, seen)) return true;
175
+ if (node.additionalProperties && typeof node.additionalProperties === 'object' && schemaHasFieldMetadata(node.additionalProperties, seen)) return true;
176
+ return false;
177
+ }
178
+
148
179
  // Derived from the contract's ACTUAL content, not hardcoded -- an operation that takes a body but
149
180
  // has no projected schema, or has no response/error schema, each add their own entry, so the list
150
181
  // says what is missing from THIS document rather than reciting a fixed disclaimer.
@@ -196,6 +227,10 @@ export function collectOmissions(contract) {
196
227
  // not gated on whether a schema exists first.
197
228
  const fieldSchemas = [op.requestBodySchema, op.responseSchema, op.errorSchema].filter(Boolean);
198
229
  if (!fieldSchemas.some((s) => schemaHasFieldDocs(s))) omissions.add('field-descriptions');
230
+ // A14: same ANY-based doctrine as field-descriptions immediately above -- added whenever
231
+ // NONE of this operation's projected schemas carry a field-level title/examples/deprecated,
232
+ // including the case where the operation has no projected schema at all.
233
+ if (!fieldSchemas.some((s) => schemaHasFieldMetadata(s))) omissions.add('field-metadata');
199
234
  }
200
235
  return [...omissions].sort();
201
236
  }
@@ -239,20 +274,37 @@ function renderDescription(contract, omissions, statusCodes) {
239
274
  return lines.join('\n');
240
275
  }
241
276
 
277
+ // Update (D-openapi-export, closing the gap named in D-openapi-reconciliation's own Update note):
278
+ // rewrites Express's own `:name`/`:name(...)` segments into OpenAPI's `{name}` templating -- an
279
+ // OpenAPI Paths Object key must use `{name}` (verified against the real 3.1 meta-schema), but
280
+ // `op.path` for an unmatched/document-less typescript-express operation is still the scan's own
281
+ // colon syntax. A true no-op for java-spring/python-fastapi (their own paths never contain a colon
282
+ // in this position, confirmed live -- no behavior change).
283
+ function colonPathToBraceSyntax(routePath) {
284
+ return routePath.replace(/:(\w+)(?:\([^)]*\))?/g, '{$1}');
285
+ }
286
+
242
287
  // Every `{name}` in the path template, in order, deduplicated (OpenAPI forbids two parameters
243
288
  // sharing name+location). The schema comes from the contract's own `pathParams.properties`; the
244
289
  // `{}` fallback for a name the contract has no property for is "unconstrained", which is both
245
290
  // honest and the minimum the 3.1 meta-schema accepts (`$defs.parameter`'s
246
291
  // `oneOf: [{required:["schema"]}, {required:["content"]}]` means a parameter MUST carry one or the
247
292
  // other -- confirmed by executing the real schema).
293
+ //
294
+ // Update (D-openapi-export, closing the gap named in D-openapi-reconciliation's own Update note):
295
+ // also recognizes Express's own `:name`/`:name(...)` syntax -- `op.path` for a typescript-express
296
+ // operation that never matched/adopted against an OpenAPI document (drift/missing/unresolved/
297
+ // ambiguous, or a document-less scan-only contract) is still the scan's own colon syntax, and this
298
+ // function would otherwise silently list zero path parameters for it. The existing `{name}`
299
+ // branch's own character class (`[^{}/]+`, more permissive than `\w+`) stays untouched.
248
300
  function buildPathParameters(op) {
249
301
  const props = op.pathParams && typeof op.pathParams === 'object' && !Array.isArray(op.pathParams)
250
302
  ? (op.pathParams.properties ?? {})
251
303
  : {};
252
304
  const seen = new Set();
253
305
  const params = [];
254
- for (const match of String(op.path).matchAll(/\{([^{}/]+)\}/g)) {
255
- const name = match[1];
306
+ for (const match of String(op.path).matchAll(/\{([^{}/]+)\}|:(\w+)(?:\([^)]*\))?/g)) {
307
+ const name = match[1] ?? match[2];
256
308
  if (seen.has(name)) continue;
257
309
  seen.add(name);
258
310
  params.push({
@@ -500,7 +552,13 @@ export function buildOpenApiDocument({ contract, snapshot = null, options = {} }
500
552
 
501
553
  for (const operationId of operationIds) {
502
554
  const op = contract.operations[operationId];
503
- const route = String(op.path);
555
+ // Update (D-openapi-export): op.path can still be Express's own colon syntax (a
556
+ // typescript-express operation that never matched/adopted against an OpenAPI document) --
557
+ // an OpenAPI Paths Object key must use {name} templating (verified against the real 3.1
558
+ // meta-schema), so this is rewritten unconditionally here, once, before it becomes both the
559
+ // document's own path key AND every downstream use of `route` in this loop. A no-op for
560
+ // java-spring/python-fastapi (their own paths never contain a colon in this position).
561
+ const route = colonPathToBraceSyntax(String(op.path));
504
562
  if (!route.startsWith('/')) {
505
563
  return { ok: false, error: `operation "${operationId}" has path "${route}", which does not start with "/" -- an OpenAPI Paths Object key must (verified against the official 3.1 meta-schema)` };
506
564
  }