backend-skeleton 1.0.0-beta.6 → 1.0.0-beta.7
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/lib/gate-definitions.mjs +4 -3
- package/package.json +1 -1
- package/scanners/adapters/java-spring.mjs +1 -1
- package/scanners/adapters/python-fastapi.mjs +50 -0
- package/scanners/adapters/typescript-express.mjs +26 -0
- package/scanners/render.mjs +1 -1
- package/schemas/scan-report.schema.json +1 -1
package/lib/gate-definitions.mjs
CHANGED
|
@@ -196,9 +196,10 @@ export const GATE_DEFINITIONS = Object.freeze({
|
|
|
196
196
|
};
|
|
197
197
|
const moduleName = report?.disposition?.module ?? report?.related_modules?.[0]?.module;
|
|
198
198
|
const mod = report?.related_modules?.find((m) => m.module === moduleName);
|
|
199
|
-
// DTOs
|
|
200
|
-
//
|
|
201
|
-
|
|
199
|
+
// DTOs now included -- every adapter pushes {className, file} objects for dtos, the
|
|
200
|
+
// same shape controllers/entities/enums already use. See D-gate-precision "Continued
|
|
201
|
+
// (part 3)" in DECISIONS.md.
|
|
202
|
+
for (const item of [...(mod?.controllers ?? []), ...(mod?.entities ?? []), ...(mod?.enums ?? []), ...(mod?.dtos ?? [])]) {
|
|
202
203
|
if (!item.file) continue;
|
|
203
204
|
// related_modules[].{controllers,entities,enums}[].file are stored ABSOLUTE
|
|
204
205
|
// (unlike Part 1's own repo-relative files_read) -- confirmed live against a real
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backend-skeleton",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic gate layer for AI-assisted backend changes -- blocks brownfield collisions and contract/handle drift via disk-hash checks before code ships. Scaffolding codegen included (Java/Spring, Python/FastAPI, TypeScript/Express).",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -257,7 +257,7 @@ export function scanJavaSpring(repoRoot) {
|
|
|
257
257
|
if (en) moduleEntry(mod).enums.push(en);
|
|
258
258
|
}
|
|
259
259
|
if (mod && file.includes(`${path.sep}presentation${path.sep}dto${path.sep}`)) {
|
|
260
|
-
moduleEntry(mod).dtos.push(path.basename(file, '.java'));
|
|
260
|
+
moduleEntry(mod).dtos.push({ className: path.basename(file, '.java'), file });
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -160,6 +160,42 @@ function extractTableEntities(text, file) {
|
|
|
160
160
|
return entities;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// D-gate-precision (Continued, part 3): the DTO/request-response-shape counterpart to
|
|
164
|
+
// extractTableEntities() -- a SQLModel-family class WITHOUT `table=True` (`ItemBase(SQLModel)`,
|
|
165
|
+
// `ItemPublic(ItemBase)`, `ItemCreate(ItemBase)`), the real convention confirmed against this
|
|
166
|
+
// project's own committed fixture and the real oracle it mirrors (fastapi/full-stack-fastapi-
|
|
167
|
+
// template). Deliberately a separate function, not merged into extractTableEntities() -- zero
|
|
168
|
+
// regression risk to existing entity extraction. One narrow, named exclusion beyond "not
|
|
169
|
+
// table=True": `class Settings(BaseSettings):` (pydantic-settings' own well-known config-class
|
|
170
|
+
// base) is not API request/response surface -- excluding it is a concrete, common false-positive
|
|
171
|
+
// fix, not a hypothetical one. No broader positive allowlist (e.g. requiring `SQLModel`/`BaseModel`
|
|
172
|
+
// literally in the base list) is layered on top: the real oracle's own `ItemPublic(ItemBase)` shape
|
|
173
|
+
// (a DTO extending ANOTHER DTO, not SQLModel/BaseModel directly) would fail such an allowlist,
|
|
174
|
+
// trading a bounded, low-cost false positive for a worse false negative (a missed real DTO change)
|
|
175
|
+
// -- the same "prefer false positive over false negative" call this exact drift-detection mechanism
|
|
176
|
+
// already makes for controllers/entities/enums.
|
|
177
|
+
function extractDtoClasses(text, file) {
|
|
178
|
+
const dtos = [];
|
|
179
|
+
for (const m of text.matchAll(CLASS_RE)) {
|
|
180
|
+
if (/table\s*=\s*True/.test(m[2])) continue; // has its own entity extractor above
|
|
181
|
+
if (/\bBaseSettings\b/.test(m[2])) continue; // pydantic-settings config, not API surface
|
|
182
|
+
dtos.push({ className: m[1], file, line: lineNumberAt(text, m.index) });
|
|
183
|
+
}
|
|
184
|
+
return dtos;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Bounded, non-exhaustive allowlist of DTO class-name suffixes -- validated against this project's
|
|
188
|
+
// own committed fixture (ItemBase/ItemCreate/ItemPublic/ItemsPublic, all correctly stripping to
|
|
189
|
+
// "item"/"items", the real "items" module). A DTO whose class name uses a different convention
|
|
190
|
+
// (Schema, Payload, Out) lands in the `_dtos` bucket below instead -- named, accepted, untracked,
|
|
191
|
+
// same shape as an unmatched entity's `_models` fallback. Purely additive to extend: a future
|
|
192
|
+
// suffix added here can only ever gain coverage, never lose it.
|
|
193
|
+
const KNOWN_DTO_SUFFIXES = ['Base', 'Create', 'Update', 'Public', 'Read', 'Response', 'Request', 'In', 'Out'];
|
|
194
|
+
function stripKnownDtoSuffix(className) {
|
|
195
|
+
const suffix = KNOWN_DTO_SUFFIXES.find((s) => className.length > s.length && className.endsWith(s));
|
|
196
|
+
return suffix ? className.slice(0, -suffix.length) : className;
|
|
197
|
+
}
|
|
198
|
+
|
|
163
199
|
// A1 §7 equivalent for FastAPI: `include_router(router, prefix=X)` applies a prefix the
|
|
164
200
|
// per-router-file scan above cannot see (each file is read independently, with no idea another
|
|
165
201
|
// file mounts it under a further prefix). Two-step resolution when X is a variable/attribute
|
|
@@ -224,6 +260,7 @@ export function scanPythonFastApi(repoRoot, projectRoot) {
|
|
|
224
260
|
};
|
|
225
261
|
|
|
226
262
|
const allEntities = [];
|
|
263
|
+
const allDtos = [];
|
|
227
264
|
for (const file of files) {
|
|
228
265
|
const text = fs.readFileSync(file, 'utf8');
|
|
229
266
|
|
|
@@ -240,6 +277,7 @@ export function scanPythonFastApi(repoRoot, projectRoot) {
|
|
|
240
277
|
}
|
|
241
278
|
|
|
242
279
|
allEntities.push(...extractTableEntities(text, file));
|
|
280
|
+
allDtos.push(...extractDtoClasses(text, file));
|
|
243
281
|
}
|
|
244
282
|
|
|
245
283
|
// Entity -> module assignment: this repo's real layout has no domain/<module>/ folder (all
|
|
@@ -255,6 +293,18 @@ export function scanPythonFastApi(repoRoot, projectRoot) {
|
|
|
255
293
|
moduleEntry(targetModule ?? '_models').entities.push(entity);
|
|
256
294
|
}
|
|
257
295
|
|
|
296
|
+
// DTO -> module assignment: same narrow name-match as entities, suffix-stripped first via the
|
|
297
|
+
// bounded allowlist above (ItemPublic -> "item" -> matches "items"). Unmatched (unlisted suffix,
|
|
298
|
+
// or no correlation at all) lands in a separate `_dtos` bucket -- not merged into entities' own
|
|
299
|
+
// `_models` -- and is therefore NOT tracked by any feature's contract gate; a real, named,
|
|
300
|
+
// accepted limitation (see D-gate-precision "Continued (part 3)" in DECISIONS.md).
|
|
301
|
+
for (const dto of allDtos) {
|
|
302
|
+
const lower = stripKnownDtoSuffix(dto.className).toLowerCase();
|
|
303
|
+
const candidates = new Set([lower, `${lower}s`]);
|
|
304
|
+
const targetModule = [...modules.keys()].find((name) => candidates.has(name));
|
|
305
|
+
moduleEntry(targetModule ?? '_dtos').dtos.push(dto);
|
|
306
|
+
}
|
|
307
|
+
|
|
258
308
|
return {
|
|
259
309
|
modules: [...modules.values()],
|
|
260
310
|
pathPrefixSignals: extractIncludeRouterPrefixSignals(repoRoot, files),
|
|
@@ -32,6 +32,15 @@ const VERB_CALL_RE = new RegExp(`\\brouter\\.(${VERBS.join('|')})\\s*\\(`, 'gi')
|
|
|
32
32
|
const ROUTER_USE_RE = /\brouter\.use\s*\(/g;
|
|
33
33
|
const ENTITY_CLASS_RE = /@Entity\s*\(\s*(?:["'`]([^"'`]*)["'`])?\s*\)\s*\n?\s*export\s+class\s+(\w+)/g;
|
|
34
34
|
|
|
35
|
+
// D-gate-precision (Continued, part 3): a pure PATH-CONVENTION heuristic, mirroring java-spring's
|
|
36
|
+
// own identical solution to the identical problem (`.../presentation/dto/`) rather than inventing
|
|
37
|
+
// syntactic DTO detection this ecosystem doesn't have a reliable single marker for -- plain
|
|
38
|
+
// `interface`, `type` aliases, class-validator classes, Zod schemas, and undecorated classes are
|
|
39
|
+
// all real conventions, and this adapter's own `api.request-shape: false` capability already names
|
|
40
|
+
// why no such regex is attempted here. One entry per FILE under a `dto/` directory, not per
|
|
41
|
+
// exported symbol -- same file-level granularity java's own DTO tracking already settled for.
|
|
42
|
+
const DTO_DIR_SEGMENT = `${path.sep}dto${path.sep}`;
|
|
43
|
+
|
|
35
44
|
// Two independent signals required, mirroring java-spring's "build file AND src layout" /
|
|
36
45
|
// python-fastapi's "dependency declared AND source-confirmed" combined bar: (a) package.json
|
|
37
46
|
// declares express, (b) at least one .ts file actually imports Router from 'express' and calls
|
|
@@ -220,6 +229,7 @@ export function scanTypeScriptExpress(repoRoot, projectRoot) {
|
|
|
220
229
|
};
|
|
221
230
|
|
|
222
231
|
const allEntities = [];
|
|
232
|
+
const allDtos = [];
|
|
223
233
|
for (const file of files) {
|
|
224
234
|
const text = fileTexts.get(file);
|
|
225
235
|
// G6: `\bRouter\s*\(` -- see detectTypeScriptExpressRoot above. Same widening for the same
|
|
@@ -235,6 +245,9 @@ export function scanTypeScriptExpress(repoRoot, projectRoot) {
|
|
|
235
245
|
moduleEntry(moduleName).controllers.push({ className, basePath: prefix, operationIds: [], endpoints, file });
|
|
236
246
|
}
|
|
237
247
|
}
|
|
248
|
+
if (file.includes(DTO_DIR_SEGMENT)) {
|
|
249
|
+
allDtos.push({ className: path.basename(file, '.ts'), file }); // no `line` -- path-based, no content parsed
|
|
250
|
+
}
|
|
238
251
|
allEntities.push(...extractTableEntities(text, file));
|
|
239
252
|
}
|
|
240
253
|
|
|
@@ -248,6 +261,19 @@ export function scanTypeScriptExpress(repoRoot, projectRoot) {
|
|
|
248
261
|
moduleEntry(targetModule ?? '_models').entities.push(entity);
|
|
249
262
|
}
|
|
250
263
|
|
|
264
|
+
// DTO -> module assignment: same narrow name-match as entities, with a trailing literal
|
|
265
|
+
// `Dto`/`DTO` stripped first -- the one near-definitional, cross-project-safe TS DTO marker (a
|
|
266
|
+
// DTO's own name almost universally contains it). An action-prefixed name (`CreateUserDto.ts`,
|
|
267
|
+
// a common real-world shape) still will NOT exact-match after stripping ("createuser" !=
|
|
268
|
+
// "user"/"users") and lands in `_dtos` -- honestly uncovered rather than guessed at (see
|
|
269
|
+
// D-gate-precision "Continued (part 3)" in DECISIONS.md).
|
|
270
|
+
for (const dto of allDtos) {
|
|
271
|
+
const lower = dto.className.replace(/Dto$/i, '').toLowerCase();
|
|
272
|
+
const candidates = new Set([lower, `${lower}s`]);
|
|
273
|
+
const targetModule = [...modules.keys()].find((name) => candidates.has(name));
|
|
274
|
+
moduleEntry(targetModule ?? '_dtos').dtos.push(dto);
|
|
275
|
+
}
|
|
276
|
+
|
|
251
277
|
return {
|
|
252
278
|
modules: [...modules.values()],
|
|
253
279
|
pathPrefixSignals: [],
|
package/scanners/render.mjs
CHANGED
|
@@ -70,7 +70,7 @@ export function renderScanMarkdown(report) {
|
|
|
70
70
|
lines.push(`- Enum \`${en.name}\`: ${en.constants.join(', ')}`);
|
|
71
71
|
}
|
|
72
72
|
if (mod.dtos.length > 0) {
|
|
73
|
-
lines.push(`- DTOs: ${mod.dtos.join(', ')}`);
|
|
73
|
+
lines.push(`- DTOs: ${mod.dtos.map((d) => d.className).join(', ')}`);
|
|
74
74
|
}
|
|
75
75
|
lines.push('');
|
|
76
76
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"controllers": { "type": "array" },
|
|
26
26
|
"entities": { "type": "array" },
|
|
27
27
|
"enums": { "type": "array" },
|
|
28
|
-
"dtos": { "type": "array"
|
|
28
|
+
"dtos": { "type": "array" },
|
|
29
29
|
"evidence": {
|
|
30
30
|
"description": "D-scanner-evidence: every {signal, term, value, weight, file, line} match that contributed to `score` -- capped per signal type AT COLLECTION TIME (not filtered afterward), so this array's size stays bounded regardless of repo size. See `capped_signals` for which signal types actually hit that cap.",
|
|
31
31
|
"type": "array",
|