backend-skeleton 1.2.0 → 1.3.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 +100 -1
- package/bin/bskel.mjs +379 -6
- package/contracts/csv.mjs +100 -0
- package/lib/cli.mjs +62 -1
- package/lib/doctor.mjs +11 -10
- package/lib/workflow.mjs +6 -0
- package/new/index.mjs +20 -0
- package/package.json +5 -2
- package/patterns/schema.sql +18 -0
- package/patterns/store.mjs +122 -0
- package/scanners/adapters/_express-shared.mjs +7 -9
- package/scanners/adapters/java-spring.mjs +7 -9
- package/scanners/adapters/python-fastapi.mjs +7 -9
- package/scanners/db/erd.mjs +0 -0
- package/scanners/index.mjs +70 -17
- package/scanners/render.mjs +12 -3
- package/scanners/text-util.mjs +22 -0
- package/schemas/pattern-record.schema.json +19 -0
- package/schemas/scan-report.schema.json +6 -2
package/scanners/index.mjs
CHANGED
|
@@ -182,7 +182,12 @@ export function computeDbDrift(liveTables, relatedModules) {
|
|
|
182
182
|
// every other env-var-driven input in this codebase is resolved at the bin/bskel.mjs layer, never
|
|
183
183
|
// inside a "pure" lib/scanner function), and keeps this function synchronous (Plane C's real I/O
|
|
184
184
|
// is `await`ed by the caller before ever calling this).
|
|
185
|
-
|
|
185
|
+
//
|
|
186
|
+
// D-zero-config-scan: `rgAvailable` is the SAME kind of CLI-boundary-resolved input as `dbSchema`
|
|
187
|
+
// -- bin/bskel.mjs computes it once via lib/doctor.mjs's binaryAvailable('rg') and hands it in as
|
|
188
|
+
// plain data. Defaults to `true` so every existing call site (this whole test suite included)
|
|
189
|
+
// stays byte-for-byte unchanged.
|
|
190
|
+
export function runScan({ repoRoot, terms, includeDb = false, dbSchema = null, adapters = ADAPTERS, rgAvailable = true }) {
|
|
186
191
|
const detections = adapters
|
|
187
192
|
.map((a) => ({ a, d: a.detect(repoRoot) }))
|
|
188
193
|
.filter(({ d }) => d != null)
|
|
@@ -221,26 +226,73 @@ export function runScan({ repoRoot, terms, includeDb = false, dbSchema = null, a
|
|
|
221
226
|
// than crashing -- the gate token still falls back to hashing the report itself.
|
|
222
227
|
const filesRead = result.filesRead ?? [];
|
|
223
228
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
// D-zero-config-scan: an empty `terms` array is how bin/bskel.mjs's cmdScan signals its new
|
|
230
|
+
// zero-flag "inventory" mode (neither --terms nor --feature was given) -- every detected module
|
|
231
|
+
// is listed exactly as the adapter found it, with NO term-matching/scoring/collision-detection
|
|
232
|
+
// performed. Faking `score: 0` here would be actively misleading -- 0 already means "scored,
|
|
233
|
+
// found nothing" in the existing contract -- so each module is built as an explicit allowlist
|
|
234
|
+
// (never `{...m}`), which guarantees `score`/`evidence`/`capped_signals` are genuinely ABSENT
|
|
235
|
+
// from the object, not merely `undefined`, and stops any future adapter-internal field from
|
|
236
|
+
// leaking into this report shape by accident.
|
|
237
|
+
const isInventory = terms.length === 0;
|
|
238
|
+
let relatedModules;
|
|
239
|
+
let collisions;
|
|
240
|
+
let verdict;
|
|
241
|
+
if (isInventory) {
|
|
242
|
+
relatedModules = modules
|
|
243
|
+
.map((m) => ({ module: m.module, controllers: m.controllers, entities: m.entities, enums: m.enums, dtos: m.dtos }))
|
|
244
|
+
.sort((a, b) => a.module.localeCompare(b.module)); // deterministic -- no score to sort by
|
|
245
|
+
collisions = []; // not "nothing collides" -- nothing was CHECKED. See the disclaimer below.
|
|
246
|
+
verdict = 'inventory';
|
|
247
|
+
} else {
|
|
248
|
+
// O6: score alone isn't a deterministic sort key -- two modules tying on score fall back to
|
|
249
|
+
// whatever order they were already in, which traces back to non-deterministic rg discovery
|
|
250
|
+
// order in the adapters above (mitigated there too, but a second determinism layer here is
|
|
251
|
+
// cheap and doesn't depend on every adapter getting it right). Module name is a stable,
|
|
252
|
+
// meaningful secondary key.
|
|
253
|
+
const scored = modules
|
|
254
|
+
.map((m) => {
|
|
255
|
+
const { score, evidence, cappedSignals } = scoreModule(m, terms);
|
|
256
|
+
return { ...m, score, evidence, capped_signals: cappedSignals };
|
|
257
|
+
})
|
|
258
|
+
.sort((a, b) => b.score - a.score || a.module.localeCompare(b.module));
|
|
235
259
|
|
|
236
|
-
|
|
237
|
-
|
|
260
|
+
relatedModules = scored.filter((m) => m.score > 0);
|
|
261
|
+
collisions = relatedModules.filter((m) => m.score >= COLLISION_THRESHOLD);
|
|
238
262
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
263
|
+
verdict = 'greenfield';
|
|
264
|
+
if (collisions.length > 0) verdict = 'collision';
|
|
265
|
+
else if (relatedModules.length > 0) verdict = 'adjacent';
|
|
266
|
+
}
|
|
242
267
|
|
|
243
268
|
const unknowns = [];
|
|
269
|
+
// D-zero-config-scan: the FIRST unknowns entry in inventory mode -- visible in --json too, not
|
|
270
|
+
// just prose a human might skim past -- so nothing downstream (human or agent) backfills a
|
|
271
|
+
// relevance/collision judgment this report never made. See D-greenfield-parameters's safe/
|
|
272
|
+
// unsafe line: every module/controller/entity listed below is a real, observed fact about this
|
|
273
|
+
// repo; this disclaimer is what keeps "here's what exists" from being read as "here's what's
|
|
274
|
+
// safe to build".
|
|
275
|
+
if (isInventory) {
|
|
276
|
+
unknowns.push(
|
|
277
|
+
'this is an unscored inventory of every module this adapter found -- no term-matching, ' +
|
|
278
|
+
'relevance scoring, or collision/greenfield classification was performed against any ' +
|
|
279
|
+
'specific feature idea. Re-run with --terms <keywords> or --feature <id> to check a ' +
|
|
280
|
+
'specific idea against this repo before treating anything here as "safe" or "colliding".',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
// D-zero-config-scan: without `rg`, every real adapter's detect() silently degrades (a blanket
|
|
284
|
+
// try/catch around the rg shell-out returns [] on failure -- see scanners/text-util.mjs's
|
|
285
|
+
// listRgFiles()), so a real Spring/FastAPI/Express repo can look indistinguishable from one
|
|
286
|
+
// this tool genuinely doesn't recognize. `rg_available` (always present on the returned report,
|
|
287
|
+
// both modes) is the machine-checkable signal; this is the human-readable one.
|
|
288
|
+
if (!rgAvailable) {
|
|
289
|
+
unknowns.push(
|
|
290
|
+
'ripgrep (`rg`) was not found on PATH -- every real scanner adapter depends on it for file ' +
|
|
291
|
+
'discovery, so a low-confidence or apparently-empty result here may simply mean `rg` is ' +
|
|
292
|
+
'missing, not that this repo lacks recognizable backend structure. Install it (`brew ' +
|
|
293
|
+
'install ripgrep`) and re-run, or run `bskel doctor` for full diagnostics.',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
244
296
|
if (!includeDb) {
|
|
245
297
|
unknowns.push('DB not scanned (Plane C is opt-in via --db --database-url-env <NAME>) -- pass --db to scan migration files, add --database-url-env for live introspection too. See A4 in CATALOG.md.');
|
|
246
298
|
} else if (!dbSchema?.live) {
|
|
@@ -271,6 +323,7 @@ export function runScan({ repoRoot, terms, includeDb = false, dbSchema = null, a
|
|
|
271
323
|
confidence,
|
|
272
324
|
api_surface_source: apiSurfaceSource,
|
|
273
325
|
verdict,
|
|
326
|
+
rg_available: rgAvailable,
|
|
274
327
|
path_prefix_signals: pathPrefixSignals,
|
|
275
328
|
related_modules: relatedModules,
|
|
276
329
|
collisions,
|
package/scanners/render.mjs
CHANGED
|
@@ -50,13 +50,22 @@ export function renderScanMarkdown(report) {
|
|
|
50
50
|
lines.push(`**Verdict**: \`${report.verdict}\``);
|
|
51
51
|
lines.push('');
|
|
52
52
|
|
|
53
|
+
// D-zero-config-scan: inventory mode has no term/collision semantics -- "greenfield for these
|
|
54
|
+
// terms" would be a lie when there were no terms to begin with, and a scored module's heading
|
|
55
|
+
// would dangle a `bskel scan explain` pointer that mode can never actually serve (it requires
|
|
56
|
+
// --feature, structurally unreachable here).
|
|
57
|
+
const isInventory = report.verdict === 'inventory';
|
|
53
58
|
if (report.related_modules.length === 0) {
|
|
54
|
-
lines.push(
|
|
59
|
+
lines.push(isInventory
|
|
60
|
+
? `No modules detected by the \`${report.adapter}\` adapter in this repo.`
|
|
61
|
+
: 'No related modules found -- greenfield for these terms.');
|
|
55
62
|
} else {
|
|
56
|
-
lines.push('## Related modules');
|
|
63
|
+
lines.push(isInventory ? '## Modules found' : '## Related modules');
|
|
57
64
|
lines.push('');
|
|
58
65
|
for (const mod of report.related_modules) {
|
|
59
|
-
lines.push(
|
|
66
|
+
lines.push(isInventory
|
|
67
|
+
? `### \`${mod.module}\``
|
|
68
|
+
: `### \`${mod.module}\` (score: ${mod.score} -- run \`bskel scan explain ${mod.module}\` for the evidence breakdown)`);
|
|
60
69
|
for (const c of mod.controllers) {
|
|
61
70
|
lines.push(`- Controller \`${c.className}\` (base path \`${c.basePath}\`), ${c.endpoints.length} endpoint(s):`);
|
|
62
71
|
for (const ep of c.endpoints) {
|
package/scanners/text-util.mjs
CHANGED
|
@@ -31,3 +31,25 @@ export function byShallowestThenName(a, b) {
|
|
|
31
31
|
const depthB = b.split(path.sep).length;
|
|
32
32
|
return depthA !== depthB ? depthA - depthB : a.localeCompare(b);
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// D-zero-config-scan: the pure "is this binary on PATH" check, deliberately living HERE (a
|
|
36
|
+
// leaf module with zero project-internal imports) rather than in lib/doctor.mjs, where it was
|
|
37
|
+
// first written. lib/doctor.mjs imports lib/verify.mjs, which imports scanners/registry.mjs --
|
|
38
|
+
// and every adapter file is dynamically import()ed BY registry.mjs's own top-level await. An
|
|
39
|
+
// adapter importing lib/doctor.mjs would close that cycle (adapter -> lib/doctor.mjs ->
|
|
40
|
+
// lib/verify.mjs -> scanners/registry.mjs -> [import()s the adapter]) and registry.mjs's OWN
|
|
41
|
+
// header comment already flags exactly this risk ("no adapter imports anything from this
|
|
42
|
+
// module" -- true of registry.mjs itself, but lib/doctor.mjs transitively reaches it). Found
|
|
43
|
+
// live: the first version of this change put binaryAvailable() in lib/doctor.mjs and every
|
|
44
|
+
// adapter import of it hung the whole CLI (a real "Detected unsettled top-level await" Node
|
|
45
|
+
// warning, reproduced against a real fixture repo, not a hypothetical). lib/doctor.mjs now
|
|
46
|
+
// re-exports this for its own existing callers (bin/bskel.mjs, its own binaryCheck()) --
|
|
47
|
+
// nothing outside this file needs to know it moved.
|
|
48
|
+
export function binaryAvailable(name, execFn = execFileSync) {
|
|
49
|
+
try {
|
|
50
|
+
execFn(name, ['--version'], { stdio: 'pipe' });
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:sbf:pattern-record:1",
|
|
4
|
+
"title": "backend-skeleton pattern-accrual record (sbf_pattern.params, one row)",
|
|
5
|
+
"description": "D-pattern-accrual: one real `bskel new --record-pattern` invocation, recorded into a database the user owns and named via --pattern-database-url-env -- never bskel's own repo state. `params` holds ONLY the flag values new/index.mjs's reusableParamsFor(stack) names for that stack (the user-typed CLI value, not a resolved/defaulted one) -- keyed by the exact flag name (e.g. `java-version`, not `javaVersion`), so `bskel pattern suggest`'s output is always a literal, paste-ready `--flag value` pair. Validated on both write (patterns/store.mjs's recordPattern) and read (listPatterns/getPattern) -- the patch-approvals.mjs doctrine.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema", "pattern_id", "stack", "params", "recorded_at"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema": { "const": "sbf.pattern/1" },
|
|
11
|
+
"pattern_id": { "type": "string", "format": "uuid" },
|
|
12
|
+
"stack": { "enum": ["spring", "fastapi"] },
|
|
13
|
+
"params": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": { "type": "string" }
|
|
16
|
+
},
|
|
17
|
+
"recorded_at": { "type": "string", "format": "date-time" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
"adapter": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
13
13
|
"confidence": { "enum": ["high", "low"] },
|
|
14
14
|
"api_surface_source": { "type": "string" },
|
|
15
|
-
"verdict": { "enum": ["greenfield", "adjacent", "collision"] },
|
|
15
|
+
"verdict": { "enum": ["greenfield", "adjacent", "collision", "inventory"] },
|
|
16
|
+
"rg_available": {
|
|
17
|
+
"description": "D-zero-config-scan: whether `rg` (ripgrep) was found on PATH when this scan ran -- every real adapter's detect() silently degrades without it (see scanners/text-util.mjs's listRgFiles()), so this is the machine-checkable signal that distinguishes 'this repo genuinely has no recognizable backend structure' from 'rg is just missing'. Optional (not in the top-level `required` array below) purely because older/hand-written report objects predate this field -- every report `runScan()` itself produces always sets it.",
|
|
18
|
+
"type": "boolean"
|
|
19
|
+
},
|
|
16
20
|
"path_prefix_signals": { "type": "array" },
|
|
17
21
|
"related_modules": {
|
|
18
22
|
"type": "array",
|
|
19
23
|
"items": {
|
|
20
24
|
"type": "object",
|
|
21
|
-
"required": ["module"
|
|
25
|
+
"required": ["module"],
|
|
22
26
|
"properties": {
|
|
23
27
|
"module": { "type": "string" },
|
|
24
28
|
"score": { "type": "number" },
|