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
|
@@ -225,11 +225,22 @@ export function saveResolution(root, featureId, resolution) {
|
|
|
225
225
|
// its warning, so `unwaived`/`blocking` treat it exactly as if it had never been recorded. A
|
|
226
226
|
// waiver can be BOTH stale and expired at once (nothing prevents that combination); the two
|
|
227
227
|
// lists are independent, not mutually exclusive.
|
|
228
|
+
// D-waiver-renewal: the single shared expiry predicate. Previously inlined once here and NOT
|
|
229
|
+
// consulted by `cmdContractWaive`'s own "is this already waived" check (bin/bskel.mjs) -- that
|
|
230
|
+
// second, unsynchronized check meant an expired waiver was `waived` for evaluateResolution's
|
|
231
|
+
// purposes (correctly stops blocking) but simultaneously "already exists" for the CLI's own
|
|
232
|
+
// existingKeys set, so re-waiving it silently matched zero new entries and never updated the
|
|
233
|
+
// stored expires_at. Exporting one function closes that gap at the source instead of requiring
|
|
234
|
+
// two call sites to independently reimplement `Date.parse(w.expires_at) <= now` in sync forever.
|
|
235
|
+
export function isWaiverExpired(waiver, now = Date.now()) {
|
|
236
|
+
return typeof waiver.expires_at === 'string' && Date.parse(waiver.expires_at) <= now;
|
|
237
|
+
}
|
|
238
|
+
|
|
228
239
|
export function evaluateResolution(contract, resolution) {
|
|
229
240
|
const status = classifyContract(contract);
|
|
230
241
|
const waivers = resolution.waivers ?? [];
|
|
231
242
|
const now = Date.now();
|
|
232
|
-
const expiredWaivers = waivers.filter((w) =>
|
|
243
|
+
const expiredWaivers = waivers.filter((w) => isWaiverExpired(w, now));
|
|
233
244
|
const expiredKeys = new Set(expiredWaivers.map(warningKey));
|
|
234
245
|
const waivedKeys = new Set(waivers.filter((w) => !expiredKeys.has(warningKey(w))).map(warningKey));
|
|
235
246
|
|
package/contracts/openapi.mjs
CHANGED
|
@@ -54,6 +54,24 @@ const SCHEMA_REF_PREFIX = '#/components/schemas/';
|
|
|
54
54
|
// cyclic component, resolved into a top-level `$defs` map -- see inlineSchema()). Distinct from
|
|
55
55
|
// SCHEMA_REF_PREFIX, which is what this module RESOLVES on the way IN from a source document.
|
|
56
56
|
const DEFS_REF_PREFIX = '#/$defs/';
|
|
57
|
+
// D-openapi-request-response-refs: the two remaining Reference Object forms a real OpenAPI 3.1
|
|
58
|
+
// document can use besides a schema-level $ref -- a whole Request Body Object
|
|
59
|
+
// (`operation.requestBody: {$ref: "#/components/requestBodies/<Name>"}`) or a whole Response
|
|
60
|
+
// Object (`operation.responses.<status>: {$ref: "#/components/responses/<Name>"}`). Previously
|
|
61
|
+
// out of scope entirely (see the stale comments this change updates); see DECISIONS.md for the
|
|
62
|
+
// real-data finding that reopened it.
|
|
63
|
+
const REQUEST_BODY_REF_PREFIX = '#/components/requestBodies/';
|
|
64
|
+
const RESPONSE_REF_PREFIX = '#/components/responses/';
|
|
65
|
+
// D-openapi-request-response-refs: no dedicated real-corpus size measurement yet for these two
|
|
66
|
+
// specific component maps (unlike MAX_COMPONENT_SCHEMAS above, which has one) -- every real
|
|
67
|
+
// document measured so far (RealWorld/Conduit's own official spec; Team-IZ-Backend;
|
|
68
|
+
// polarsource/polar, neither of which uses this ref form at all) has far fewer requestBodies/
|
|
69
|
+
// responses components than schemas components. Reuses MAX_SECURITY_SCHEMES' own "small named
|
|
70
|
+
// component map, conservative round default" precedent rather than MAX_COMPONENT_SCHEMAS' much
|
|
71
|
+
// larger one. Revisit with real corpus data once a document large enough to test it exists, per
|
|
72
|
+
// this project's own data-first-numerics discipline.
|
|
73
|
+
const MAX_COMPONENT_REQUEST_BODIES = 512;
|
|
74
|
+
const MAX_COMPONENT_RESPONSES = 512;
|
|
57
75
|
|
|
58
76
|
// A3: response/error JSON Schema projection. Reuses every inlineSchema() defense above
|
|
59
77
|
// unchanged (keyword/format whitelist, MAX_SCHEMA_DEPTH/NODES/PATTERN_LENGTH) -- measured by
|
|
@@ -488,6 +506,38 @@ export function loadOpenApiDocument(filePath) {
|
|
|
488
506
|
// route is ambiguous even within the document itself). `$ref` path items are skipped, not
|
|
489
507
|
// resolved (out of scope for this vertical slice -- see DECISIONS.md).
|
|
490
508
|
//
|
|
509
|
+
// D-openapi-request-response-refs: resolves ONE level of "#/components/requestBodies/<Name>" or
|
|
510
|
+
// "#/components/responses/<Name>" indirection against the document's own componentRequestBodies/
|
|
511
|
+
// componentResponses map -- the two remaining Reference Object forms a real OpenAPI 3.1 document
|
|
512
|
+
// uses besides a schema-level $ref (RealWorld/Conduit's own official spec uses this pervasively,
|
|
513
|
+
// for every single operation; see DECISIONS.md). Not a schema-level resolution (inlineSchema()'s
|
|
514
|
+
// own job, unchanged) -- this operates one level up, on the Request Body Object / Response Object
|
|
515
|
+
// itself, and returns a plain object exactly as if the source document had inlined it directly.
|
|
516
|
+
// - `node` not an object, or an object with no `$ref` key: returned unchanged (the overwhelming
|
|
517
|
+
// common case -- most operations never use this indirection at all).
|
|
518
|
+
// - a `$ref` with any sibling key, an unsupported prefix, a name failing COMPONENT_SCHEMA_NAME_RE
|
|
519
|
+
// (same prototype-pollution-safe whitelist inlineSchema() already applies one layer down), or a
|
|
520
|
+
// name not present in `componentMap`: resolution fails -- returns `null`, the exact same "skip
|
|
521
|
+
// gracefully, never fabricate" outcome this module already used for a genuinely bodyless
|
|
522
|
+
// operation or an undocumented response status, so every existing caller's null-handling already
|
|
523
|
+
// covers it with no further change.
|
|
524
|
+
// Deliberately NOT recursive -- no real document measured (RealWorld's official spec, its Java
|
|
525
|
+
// and Node reference implementations, Team-IZ-Backend, polarsource/polar) ever chains a
|
|
526
|
+
// requestBodies/responses $ref to ANOTHER requestBodies/responses $ref; a document that did would
|
|
527
|
+
// fail closed here rather than this module guessing at a chain no real case has ever needed.
|
|
528
|
+
function resolveComponentObjectRef(node, refPrefix, componentMap) {
|
|
529
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return null;
|
|
530
|
+
if (!Object.hasOwn(node, '$ref')) return node;
|
|
531
|
+
const siblingKeys = Object.keys(node).filter((k) => k !== '$ref');
|
|
532
|
+
if (siblingKeys.length > 0) return null;
|
|
533
|
+
const ref = node['$ref'];
|
|
534
|
+
if (typeof ref !== 'string' || !ref.startsWith(refPrefix)) return null;
|
|
535
|
+
const name = ref.slice(refPrefix.length);
|
|
536
|
+
if (name.includes('~') || name.includes('%') || !COMPONENT_SCHEMA_NAME_RE.test(name)) return null;
|
|
537
|
+
const resolved = componentMap.get(name);
|
|
538
|
+
return resolved && typeof resolved === 'object' && !Array.isArray(resolved) ? resolved : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
491
541
|
// A2: also builds `componentSchemas` (Map<name, schemaNode>, from `doc.components.schemas`) and
|
|
492
542
|
// retains each operation's raw `requestBody` node on its `entry` -- both were previously
|
|
493
543
|
// discarded entirely (A1 only needed {verb, path, operationId}). Indexing stays O(top-level
|
|
@@ -503,10 +553,20 @@ export function indexOpenApiDocument(doc) {
|
|
|
503
553
|
// security scheme name becomes an object key downstream, in the contract's own root-level
|
|
504
554
|
// sourceSecuritySchemes).
|
|
505
555
|
const securitySchemes = new Map();
|
|
556
|
+
// D-openapi-request-response-refs: Map<name, Request Body Object | Response Object>, same
|
|
557
|
+
// "Map, never a plain object" + COMPONENT_SCHEMA_NAME_RE whitelist reasoning as
|
|
558
|
+
// componentSchemas/securitySchemes above -- resolveComponentObjectRef() below is the one
|
|
559
|
+
// consumer, used to dereference an operation's own requestBody/responses entries in place,
|
|
560
|
+
// once, right here at index time, so every downstream function keeps reading a plain
|
|
561
|
+
// (already-resolved) object exactly as it always has.
|
|
562
|
+
const componentRequestBodies = new Map();
|
|
563
|
+
const componentResponses = new Map();
|
|
506
564
|
const stats = {
|
|
507
565
|
path_count: 0, operation_count: 0, skipped_path_refs: 0, rejected_operation_ids: 0,
|
|
508
566
|
component_schema_count: 0, rejected_component_schemas: 0,
|
|
509
567
|
security_scheme_count: 0, rejected_security_schemes: 0,
|
|
568
|
+
component_request_body_count: 0, rejected_component_request_bodies: 0,
|
|
569
|
+
component_response_count: 0, rejected_component_responses: 0,
|
|
510
570
|
};
|
|
511
571
|
|
|
512
572
|
const openapiVersion = typeof doc.openapi === 'string' ? doc.openapi : null;
|
|
@@ -545,9 +605,39 @@ export function indexOpenApiDocument(doc) {
|
|
|
545
605
|
stats.security_scheme_count = securitySchemes.size;
|
|
546
606
|
}
|
|
547
607
|
|
|
608
|
+
const rawComponentRequestBodies = rawComponents ? rawComponents.requestBodies : null;
|
|
609
|
+
if (rawComponentRequestBodies && typeof rawComponentRequestBodies === 'object' && !Array.isArray(rawComponentRequestBodies)) {
|
|
610
|
+
const names = Object.keys(rawComponentRequestBodies);
|
|
611
|
+
if (names.length > MAX_COMPONENT_REQUEST_BODIES) {
|
|
612
|
+
return { ok: false, error: `OpenAPI document has ${names.length} component request bodies, exceeds the ${MAX_COMPONENT_REQUEST_BODIES}-request-body limit` };
|
|
613
|
+
}
|
|
614
|
+
for (const name of names) {
|
|
615
|
+
const value = rawComponentRequestBodies[name];
|
|
616
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) continue;
|
|
617
|
+
if (!COMPONENT_SCHEMA_NAME_RE.test(name)) { stats.rejected_component_request_bodies++; continue; }
|
|
618
|
+
componentRequestBodies.set(name, value);
|
|
619
|
+
}
|
|
620
|
+
stats.component_request_body_count = componentRequestBodies.size;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const rawComponentResponses = rawComponents ? rawComponents.responses : null;
|
|
624
|
+
if (rawComponentResponses && typeof rawComponentResponses === 'object' && !Array.isArray(rawComponentResponses)) {
|
|
625
|
+
const names = Object.keys(rawComponentResponses);
|
|
626
|
+
if (names.length > MAX_COMPONENT_RESPONSES) {
|
|
627
|
+
return { ok: false, error: `OpenAPI document has ${names.length} component responses, exceeds the ${MAX_COMPONENT_RESPONSES}-response limit` };
|
|
628
|
+
}
|
|
629
|
+
for (const name of names) {
|
|
630
|
+
const value = rawComponentResponses[name];
|
|
631
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) continue;
|
|
632
|
+
if (!COMPONENT_SCHEMA_NAME_RE.test(name)) { stats.rejected_component_responses++; continue; }
|
|
633
|
+
componentResponses.set(name, value);
|
|
634
|
+
}
|
|
635
|
+
stats.component_response_count = componentResponses.size;
|
|
636
|
+
}
|
|
637
|
+
|
|
548
638
|
const paths = doc.paths;
|
|
549
639
|
if (typeof paths !== 'object' || paths === null || Array.isArray(paths)) {
|
|
550
|
-
return { ok: true, byOperationId, byRoute, componentSchemas, securitySchemes, stats, servers: [], openapiVersion, schemaDialectSupported };
|
|
640
|
+
return { ok: true, byOperationId, byRoute, componentSchemas, securitySchemes, componentRequestBodies, componentResponses, stats, servers: [], openapiVersion, schemaDialectSupported };
|
|
551
641
|
}
|
|
552
642
|
|
|
553
643
|
const pathKeys = Object.keys(paths);
|
|
@@ -589,17 +679,32 @@ export function indexOpenApiDocument(doc) {
|
|
|
589
679
|
}
|
|
590
680
|
|
|
591
681
|
// A2: raw requestBody node retained verbatim (bounded by the document's own
|
|
592
|
-
// MAX_DOCUMENT_BYTES cap -- no new read, no new size limit needed).
|
|
593
|
-
// (`#/components/requestBodies/*`)
|
|
594
|
-
//
|
|
595
|
-
|
|
682
|
+
// MAX_DOCUMENT_BYTES cap -- no new read, no new size limit needed).
|
|
683
|
+
// D-openapi-request-response-refs: a `$ref` requestBody (`#/components/requestBodies/*`)
|
|
684
|
+
// is resolved HERE, once, at index time -- every downstream function (applyRequestBodySchema,
|
|
685
|
+
// applyRequestMediaTypes) keeps reading `entry.requestBody` as a plain Request Body Object
|
|
686
|
+
// exactly as before, unaware whether the source document inlined it or referenced it.
|
|
687
|
+
// resolveComponentObjectRef() falls back to null on any failure to resolve (missing/
|
|
688
|
+
// malformed $ref target, name not in componentRequestBodies, unsupported prefix, a sibling
|
|
689
|
+
// key alongside $ref) -- the same "skip gracefully, never fabricate" posture this module
|
|
690
|
+
// already takes for a genuinely bodyless operation.
|
|
691
|
+
const rawRequestBody = typeof operation.requestBody === 'object' && operation.requestBody !== null && !Array.isArray(operation.requestBody)
|
|
596
692
|
? operation.requestBody
|
|
597
693
|
: null;
|
|
694
|
+
const requestBody = resolveComponentObjectRef(rawRequestBody, REQUEST_BODY_REF_PREFIX, componentRequestBodies);
|
|
598
695
|
// A3: raw responses map retained verbatim, same "no new read, no new size cap" reasoning
|
|
599
696
|
// as requestBody above -- bounded by MAX_DOCUMENT_BYTES already.
|
|
600
|
-
|
|
697
|
+
// D-openapi-request-response-refs: each STATUS's own value is independently resolved the
|
|
698
|
+
// same way requestBody is above -- a Response Object `$ref` (`#/components/responses/*`)
|
|
699
|
+
// resolves to the real object; an unresolvable one becomes `null` at that status key,
|
|
700
|
+
// which every existing downstream consumer (projectResponseSchemas, applyPerStatusResponses)
|
|
701
|
+
// already treats as "nothing documented for this status", not a failure.
|
|
702
|
+
const rawResponses = typeof operation.responses === 'object' && operation.responses !== null && !Array.isArray(operation.responses)
|
|
601
703
|
? operation.responses
|
|
602
704
|
: null;
|
|
705
|
+
const responses = rawResponses
|
|
706
|
+
? Object.fromEntries(Object.keys(rawResponses).map((status) => [status, resolveComponentObjectRef(rawResponses[status], RESPONSE_REF_PREFIX, componentResponses)]))
|
|
707
|
+
: null;
|
|
603
708
|
// A7: raw parameters/security/summary/tags retained verbatim, same "no new read, no new
|
|
604
709
|
// size cap" reasoning as requestBody/responses above. `security` is deliberately
|
|
605
710
|
// Array.isArray-checked rather than truthy-checked -- a real, explicit `[]` (11/148 real
|
|
@@ -632,7 +737,7 @@ export function indexOpenApiDocument(doc) {
|
|
|
632
737
|
? doc.servers.filter((s) => s && typeof s.url === 'string').map((s) => s.url)
|
|
633
738
|
: [];
|
|
634
739
|
|
|
635
|
-
return { ok: true, byOperationId, byRoute, componentSchemas, securitySchemes, stats, servers, openapiVersion, schemaDialectSupported };
|
|
740
|
+
return { ok: true, byOperationId, byRoute, componentSchemas, securitySchemes, componentRequestBodies, componentResponses, stats, servers, openapiVersion, schemaDialectSupported };
|
|
636
741
|
}
|
|
637
742
|
|
|
638
743
|
// `S` (scan path) always starts with "/" (scanners/adapters/java-spring.mjs's joinPath guarantees
|
|
@@ -1015,9 +1120,12 @@ function walkSchemaNode(node, componentSchemas, depth, visiting, state, limits)
|
|
|
1015
1120
|
// let alone body shape. `docEntry` is the OpenAPI-side entry (from byOperationId or byRoute) whose
|
|
1016
1121
|
// `.requestBody` indexOpenApiDocument() retained. Never treats "nothing to project" as a failure --
|
|
1017
1122
|
// only an actual unresolvable schema increments schema_unresolved / sets schemaUnresolvedReason.
|
|
1123
|
+
// D-openapi-request-response-refs: `docEntry.requestBody` is never `{$ref: ...}` by the time it
|
|
1124
|
+
// reaches here -- indexOpenApiDocument() already resolved a `#/components/requestBodies/*` $ref
|
|
1125
|
+
// (or fell back to null if it couldn't) at index time, once, for every consumer.
|
|
1018
1126
|
function applyRequestBodySchema(result, docEntry, componentSchemas, stats, includeFieldDocs) {
|
|
1019
1127
|
const requestBody = docEntry.requestBody;
|
|
1020
|
-
if (!requestBody
|
|
1128
|
+
if (!requestBody) {
|
|
1021
1129
|
stats.schema_none++;
|
|
1022
1130
|
return;
|
|
1023
1131
|
}
|
|
@@ -1471,16 +1579,13 @@ function applyPerStatusResponses(result, docEntry, componentSchemas, stats, sche
|
|
|
1471
1579
|
for (const key of Object.keys(responses)) {
|
|
1472
1580
|
if (!RESPONSE_STATUS_KEY_RE.test(key)) continue; // not a legal status key -- dropped, not a failure
|
|
1473
1581
|
const resp = responses[key];
|
|
1582
|
+
// D-openapi-request-response-refs: `resp` is never `{$ref: ...}` by the time it reaches here
|
|
1583
|
+
// -- indexOpenApiDocument() already resolved a `#/components/responses/*` $ref (or replaced
|
|
1584
|
+
// it with null if it couldn't) at index time, once, for every consumer. A genuinely
|
|
1585
|
+
// unresolvable ref lands here as `null` and is skipped by the check below, same as before:
|
|
1586
|
+
// still no fabricated description-only entry carrying the synthetic
|
|
1587
|
+
// PER_STATUS_NO_DESCRIPTION_STANDIN for a status this document didn't really document.
|
|
1474
1588
|
if (typeof resp !== 'object' || resp === null || Array.isArray(resp)) continue;
|
|
1475
|
-
// A8 follow-up (Codex review): a Response Object `$ref` (`components.responses.<Name>`, legal
|
|
1476
|
-
// per the official 3.1 meta-schema's `response-or-reference`) is not resolved here -- 0 real
|
|
1477
|
-
// occurrences against the Team-IZ-Backend oracle (694 response objects, 0 $ref), named rather
|
|
1478
|
-
// than built, same "don't build for zero real cases" discipline as non-json-response-schemas/
|
|
1479
|
-
// response-headers below. Skipping is the fail-closed choice: falling through with
|
|
1480
|
-
// resp.description/resp.content both undefined would produce a description-only entry carrying
|
|
1481
|
-
// the synthetic PER_STATUS_NO_DESCRIPTION_STANDIN as if the source truly documented this status
|
|
1482
|
-
// with no description -- false. A referenced response is simply omitted for this status.
|
|
1483
|
-
if (typeof resp.$ref === 'string') continue;
|
|
1484
1589
|
|
|
1485
1590
|
const entry = {};
|
|
1486
1591
|
if (typeof resp.description === 'string' && resp.description.length > 0 && resp.description !== PER_STATUS_NO_DESCRIPTION_STANDIN) {
|
|
@@ -1528,6 +1633,8 @@ function applyPerStatusResponses(result, docEntry, componentSchemas, stats, sche
|
|
|
1528
1633
|
// schemas/feature-contract.schema.json too). Gated on schemaProjectionEnabled for the same reason
|
|
1529
1634
|
// as applyPerStatusResponses above -- a media-type schema resolves through the same inlineSchema()
|
|
1530
1635
|
// path.
|
|
1636
|
+
// D-openapi-request-response-refs: `docEntry.requestBody` is never `{$ref: ...}` by the time it
|
|
1637
|
+
// reaches here -- see applyRequestBodySchema's own identical note.
|
|
1531
1638
|
function applyRequestMediaTypes(result, docEntry, componentSchemas, stats, schemaProjectionEnabled, includeFieldDocs) {
|
|
1532
1639
|
if (!schemaProjectionEnabled) {
|
|
1533
1640
|
result.requestMediaTypesSkippedDialect = true;
|
|
@@ -1535,7 +1642,7 @@ function applyRequestMediaTypes(result, docEntry, componentSchemas, stats, schem
|
|
|
1535
1642
|
return;
|
|
1536
1643
|
}
|
|
1537
1644
|
const requestBody = docEntry.requestBody;
|
|
1538
|
-
if (!requestBody || typeof requestBody !== 'object' || Array.isArray(requestBody)
|
|
1645
|
+
if (!requestBody || typeof requestBody !== 'object' || Array.isArray(requestBody)) {
|
|
1539
1646
|
stats.request_media_types_none++;
|
|
1540
1647
|
return;
|
|
1541
1648
|
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// `bskel handles plan`, never a hard dependency of the base install, never invoked silently.
|
|
4
4
|
// See DECISIONS.md.
|
|
5
5
|
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
7
|
-
import { execFile, execFileSync } from 'node:child_process';
|
|
8
|
+
import { execFile, execFileSync, spawn } from 'node:child_process';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import { promisify } from 'node:util';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
10
12
|
|
|
11
13
|
const execFileAsync = promisify(execFile);
|
|
12
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -57,3 +59,85 @@ export async function runAstClassify(dtoFilePath, srcRoot) {
|
|
|
57
59
|
}
|
|
58
60
|
return JSON.parse(jsonLine);
|
|
59
61
|
}
|
|
62
|
+
|
|
63
|
+
// D-java-source-splice: writes `locators` (each {type_fqn, member_kind, member_name,
|
|
64
|
+
// erased_param_types}) to a scratch temp file in the plain, delimiter-free line format
|
|
65
|
+
// Main.java's readLocators() expects (deliberately not JSON -- the helper has no JSON parser
|
|
66
|
+
// dependency and none of these fields can ever contain a newline), invokes the helper's new
|
|
67
|
+
// "locate" mode, and returns its parsed `{topLevelTypes, results}`. The temp file is always
|
|
68
|
+
// removed, even on failure.
|
|
69
|
+
export async function runAstLocate(javaFilePath, srcRoot, locators) {
|
|
70
|
+
const detection = detectAstHelperAvailable();
|
|
71
|
+
if (!detection.available) {
|
|
72
|
+
throw new Error(detection.reason);
|
|
73
|
+
}
|
|
74
|
+
const lines = [String(locators.length)];
|
|
75
|
+
for (const loc of locators) {
|
|
76
|
+
lines.push(loc.type_fqn, loc.member_kind, loc.member_name, String((loc.erased_param_types ?? []).length));
|
|
77
|
+
for (const p of loc.erased_param_types ?? []) lines.push(p);
|
|
78
|
+
}
|
|
79
|
+
const tmpFile = path.join(os.tmpdir(), `bskel-ast-locators-${randomUUID()}.txt`);
|
|
80
|
+
fs.writeFileSync(tmpFile, `${lines.join('\n')}\n`);
|
|
81
|
+
try {
|
|
82
|
+
console.error('bskel: running the AST helper (locate mode)...');
|
|
83
|
+
let stdout;
|
|
84
|
+
try {
|
|
85
|
+
({ stdout } = await execFileAsync(
|
|
86
|
+
gradlewPath(),
|
|
87
|
+
['run', '--console=plain', '-q', `--args="locate" "${javaFilePath}" "${srcRoot}" "${tmpFile}"`],
|
|
88
|
+
{ cwd: HELPER_DIR, maxBuffer: 16 * 1024 * 1024 },
|
|
89
|
+
));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new Error(`AST helper (locate) invocation failed: ${err.stderr || err.message}`);
|
|
92
|
+
}
|
|
93
|
+
const jsonLine = stdout.split('\n').find((line) => line.trim().startsWith('{'));
|
|
94
|
+
if (!jsonLine) {
|
|
95
|
+
throw new Error(`AST helper (locate) produced no parseable JSON output:\n${stdout}`);
|
|
96
|
+
}
|
|
97
|
+
return JSON.parse(jsonLine);
|
|
98
|
+
} finally {
|
|
99
|
+
fs.rmSync(tmpFile, { force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// D-java-source-splice: a plain syntax gate for RENDERED content that has not been written to
|
|
104
|
+
// disk yet -- the helper's "parse -" mode reads `sourceText` from stdin (no temp file, no
|
|
105
|
+
// classpath/src-root needed at all for a syntax-only check) and returns {ok:true} or
|
|
106
|
+
// {ok:false, problems:[...]}. Uses `spawn` directly, NOT the promisified `execFile` used
|
|
107
|
+
// elsewhere in this file -- confirmed live that Node's async execFile has no `input` option at
|
|
108
|
+
// all (only execFileSync does); passing one is silently ignored and the child process hangs
|
|
109
|
+
// reading from this process's OWN inherited stdin instead of the string ever intended for it.
|
|
110
|
+
export async function runAstParse(sourceText) {
|
|
111
|
+
const detection = detectAstHelperAvailable();
|
|
112
|
+
if (!detection.available) {
|
|
113
|
+
throw new Error(detection.reason);
|
|
114
|
+
}
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const child = spawn(gradlewPath(), ['run', '--console=plain', '-q', '--args="parse" "-"'], {
|
|
117
|
+
cwd: HELPER_DIR,
|
|
118
|
+
});
|
|
119
|
+
let stdout = '';
|
|
120
|
+
let stderr = '';
|
|
121
|
+
child.stdout.on('data', (d) => { stdout += d; });
|
|
122
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
123
|
+
child.on('error', (err) => reject(new Error(`AST helper (parse) invocation failed: ${err.message}`)));
|
|
124
|
+
child.on('close', (code) => {
|
|
125
|
+
if (code !== 0) {
|
|
126
|
+
reject(new Error(`AST helper (parse) invocation failed (exit ${code}): ${stderr || stdout}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const jsonLine = stdout.split('\n').find((line) => line.trim().startsWith('{'));
|
|
130
|
+
if (!jsonLine) {
|
|
131
|
+
reject(new Error(`AST helper (parse) produced no parseable JSON output:\n${stdout}`));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
resolve(JSON.parse(jsonLine));
|
|
136
|
+
} catch (err) {
|
|
137
|
+
reject(new Error(`AST helper (parse) produced unparseable JSON: ${err.message}\n${stdout}`));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
child.stdin.write(sourceText);
|
|
141
|
+
child.stdin.end();
|
|
142
|
+
});
|
|
143
|
+
}
|