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.
- package/README.md +185 -13
- package/bin/bskel.mjs +689 -33
- package/contracts/emit.mjs +5 -1
- package/contracts/export.mjs +65 -7
- package/contracts/openapi.mjs +321 -30
- package/contracts/validate.mjs +23 -4
- package/handles/_engine.mjs +123 -30
- package/handles/capability-codec.mjs +94 -0
- package/handles/codec.mjs +13 -3
- package/handles/providers/java-spring/emit.mjs +78 -33
- package/handles/providers/java-spring/observe.mjs +4 -3
- package/handles/providers/java-spring/plan.mjs +73 -16
- package/handles/providers/java-spring/templates/HandleCodec.java.tmpl +19 -1
- package/handles/providers/java-spring/templates/HandleController.java.tmpl +19 -9
- package/handles/providers/java-spring/templates/HandleService.java.tmpl +21 -2
- package/handles/providers/java-spring/templates/RecordHandleSnapshot.java.tmpl +1 -1
- package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +36 -9
- package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +8 -2
- package/handles/providers/java-spring.mjs +8 -0
- package/handles/providers/python-fastapi/emit.mjs +21 -26
- package/handles/providers/python-fastapi/observe.mjs +6 -5
- package/handles/providers/python-fastapi/templates/codec.py.tmpl +18 -3
- package/handles/providers/python-fastapi/templates/record_snapshot.py.tmpl +129 -39
- package/handles/providers/python-fastapi.mjs +3 -3
- package/handles/providers/typescript-express/emit.mjs +144 -55
- package/handles/providers/typescript-express/observe.mjs +102 -0
- package/handles/providers/typescript-express/templates/codec.ts.tmpl +13 -3
- package/handles/providers/typescript-express/templates/contractCheck.ts.tmpl +136 -0
- package/handles/providers/typescript-express/templates/handleEntities.ts.tmpl +89 -0
- package/handles/providers/typescript-express/templates/handleService.ts.tmpl +81 -0
- package/handles/providers/typescript-express/templates/migration.sql.tmpl +36 -0
- package/handles/providers/typescript-express/templates/observeContract.ts.tmpl +146 -0
- package/handles/providers/typescript-express/templates/observedSchema.ts.tmpl +116 -0
- package/handles/providers/typescript-express/templates/recordSnapshotWrapper.ts.tmpl +123 -0
- package/handles/providers/typescript-express/templates/registry.ts.tmpl +19 -10
- package/handles/providers/typescript-express/templates/resolver.ts.tmpl +13 -0
- package/handles/providers/typescript-express/templates/resolverPolicy.ts.tmpl +20 -0
- package/handles/providers/typescript-express/templates/router.ts.tmpl +113 -2
- package/handles/providers/typescript-express.mjs +7 -4
- package/lib/attest.mjs +40 -0
- package/lib/cli.mjs +136 -5
- package/lib/cross-feature-collisions.mjs +286 -0
- package/lib/diff.mjs +35 -0
- package/lib/exit-codes.mjs +21 -0
- package/lib/fsutil.mjs +7 -2
- package/lib/gate-definitions.mjs +85 -1
- package/lib/gates.mjs +5 -1
- package/lib/http-server.mjs +192 -6
- package/lib/lock.mjs +68 -15
- package/lib/patch-kinds.mjs +52 -0
- package/lib/patch-transactions.mjs +206 -0
- package/lib/serve-ui.html +211 -0
- package/lib/verify.mjs +23 -6
- package/lib/workflow.mjs +31 -3
- package/package.json +8 -2
- package/scanners/adapters/_java-spring-analyzer.mjs +9 -1
- package/scanners/adapters/java-spring.mjs +114 -10
- package/scanners/adapters/javascript-express.mjs +46 -13
- package/scanners/adapters/python-fastapi.mjs +9 -1
- package/scanners/adapters/typescript-express.mjs +19 -2
- package/scanners/db/ddl-apply.mjs +253 -0
- package/scanners/db/introspect.mjs +61 -32
- package/scanners/db/migrations.mjs +73 -18
- package/schemas/cross-feature-report.schema.json +66 -0
- package/schemas/cross-feature-resolution.schema.json +28 -0
- package/schemas/feature-contract.schema.json +3 -3
- package/schemas/gate-attestation.schema.json +22 -0
- package/schemas/gate-export.schema.json +58 -0
- package/schemas/handles-plan.schema.json +2 -0
- package/schemas/oracle-manifest.schema.json +58 -0
- package/schemas/patch-transaction.schema.json +182 -0
- package/schemas/scan-report.schema.json +6 -4
- package/schemas/stack-choice.schema.json +12 -1
- package/schemas/stack-record.schema.json +6 -1
- package/stack/apply.mjs +51 -7
- package/stack/catalog/ngrok.yml +8 -2
- package/stack/config-apply.mjs +168 -0
package/contracts/emit.mjs
CHANGED
|
@@ -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+)\}
|
|
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) {
|
package/contracts/export.mjs
CHANGED
|
@@ -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
|
|
86
|
-
//
|
|
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
|
-
'
|
|
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`,
|
|
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(/\{([^{}/]+)\}
|
|
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
|
-
|
|
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
|
}
|