backend-skeleton 1.1.1 → 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 +127 -1
- package/bin/bskel.mjs +526 -20
- package/contracts/csv.mjs +100 -0
- package/handles/providers/java-spring/observe.mjs +15 -1
- package/handles/providers/java-spring/templates/ContractObservationAspect.java.tmpl +38 -0
- package/handles/providers/java-spring/templates/ObserveSchemaLoader.java.tmpl +13 -7
- package/handles/providers/java-spring/templates/ReceiptSigner.java.tmpl +174 -0
- package/handles/providers/python-fastapi/observe.mjs +5 -0
- package/handles/providers/python-fastapi/templates/observe_contract.py.tmpl +19 -0
- package/handles/providers/python-fastapi/templates/receipt_sign.py.tmpl +68 -0
- package/handles/providers/typescript-express/observe.mjs +5 -0
- package/handles/providers/typescript-express/templates/observeContract.ts.tmpl +19 -0
- package/handles/providers/typescript-express/templates/receiptSign.ts.tmpl +65 -0
- package/lib/cli.mjs +78 -2
- package/lib/doctor.mjs +11 -10
- package/lib/field-dependencies.mjs +2 -1
- package/lib/gate-definitions.mjs +8 -4
- package/lib/scan-report-paths.mjs +47 -0
- 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 +68 -11
- package/scanners/db/erd.mjs +0 -0
- package/scanners/index.mjs +71 -18
- package/scanners/render.mjs +12 -3
- package/scanners/text-util.mjs +22 -0
- package/schemas/conformance-report.schema.json +12 -1
- package/schemas/observe-receipt.schema.json +10 -1
- package/schemas/pattern-record.schema.json +19 -0
- package/schemas/scan-report.schema.json +7 -3
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
|
-
import { lineNumberAt, listRgFiles as sharedListRgFiles, byShallowestThenName } from '../text-util.mjs';
|
|
15
|
+
import { lineNumberAt, listRgFiles as sharedListRgFiles, byShallowestThenName, binaryAvailable } from '../text-util.mjs';
|
|
16
16
|
|
|
17
17
|
const PROJECT_FILE_GLOBS = ['pyproject.toml', 'requirements*.txt'];
|
|
18
18
|
const EXCLUDE_GLOBS = ['!**/.venv/**', '!**/site-packages/**', '!**/node_modules/**', '!**/__pycache__/**'];
|
|
@@ -25,6 +25,12 @@ const FASTAPI_DEP_RE = /(?:^|[\s"'[])fastapi(?:\[[^\]]*\])?(?:[\s"',\]=<>~!;]|$)
|
|
|
25
25
|
|
|
26
26
|
const VERB_DECORATOR_RE = /@(\w+)\.(get|post|put|patch|delete)\s*\(/gi;
|
|
27
27
|
const ROUTER_DECL_RE = /(\w+)\s*=\s*APIRouter\s*\(/g;
|
|
28
|
+
|
|
29
|
+
// D-fastapi-generic-module-name: real evidence only (polarsource/polar), not a speculative list --
|
|
30
|
+
// same "validated against a real oracle" precedent as KNOWN_DTO_SUFFIXES below. 57 real files
|
|
31
|
+
// literally named `endpoints.py`, 1 named `router.py`; no other generic stem (`routes`, `views`,
|
|
32
|
+
// `api`) has ever been observed.
|
|
33
|
+
const GENERIC_ROUTER_STEMS = new Set(['endpoints', 'router']);
|
|
28
34
|
const CLASS_RE = /^class\s+(\w+)\s*\(([^)]*)\)\s*:/gm;
|
|
29
35
|
const INCLUDE_ROUTER_RE = /include_router\s*\(/g;
|
|
30
36
|
|
|
@@ -287,8 +293,59 @@ const API_SURFACE_SOURCE = 'router-local paths only (this scan does not resolve
|
|
|
287
293
|
'document via `bskel contract emit --openapi-file <path> --path-prefix <prefix>` for trustworthy ' +
|
|
288
294
|
'operation identity and schemas.';
|
|
289
295
|
|
|
296
|
+
// D-fastapi-generic-module-name: real polarsource/polar dogfooding found `path.basename(file,
|
|
297
|
+
// '.py')` (the module-name rule below) collapses to the literal string "endpoints"/"router" for
|
|
298
|
+
// every file using that repo's own real, common convention -- name every router file generically,
|
|
299
|
+
// carry the real domain identity in the PARENT DIRECTORY instead (`organization/endpoints.py`,
|
|
300
|
+
// `member/endpoints.py`). Two CLOSED passes, not interleaved -- a generic file's directory-derived
|
|
301
|
+
// candidate must never be adopted before every non-generic file's own literal name is known, or a
|
|
302
|
+
// later-discovered collision could arrive too late (the wrong merge would already be written).
|
|
303
|
+
// Pass 1: every non-generic-stem file's own literal name is `reserved`, unchanged from today.
|
|
304
|
+
// Pass 2: a generic-stem file's candidate (its own parent directory's basename) is adopted only if
|
|
305
|
+
// it collides with neither `reserved` nor another generic file's already-`claimed` candidate --
|
|
306
|
+
// real evidence found 9 such real collisions in polar alone (e.g. `subscription/endpoints.py`'s
|
|
307
|
+
// candidate "subscription" collides with the real, unrelated, non-generic
|
|
308
|
+
// `customer_portal/endpoints/subscription.py`). On any collision, the file keeps its old literal
|
|
309
|
+
// generic name -- never guess into a silent wrong merge. `files` is already sorted by full path
|
|
310
|
+
// (`listRgFiles`), so processing order (and therefore first-claim-wins) is deterministic.
|
|
311
|
+
function resolveGenericModuleNames(files, readFile, projectRoot) {
|
|
312
|
+
const routerFiles = [];
|
|
313
|
+
const reserved = new Set();
|
|
314
|
+
for (const file of files) {
|
|
315
|
+
if (!/APIRouter\s*\(/.test(readFile(file))) continue;
|
|
316
|
+
const stem = path.basename(file, '.py');
|
|
317
|
+
routerFiles.push(file);
|
|
318
|
+
if (!GENERIC_ROUTER_STEMS.has(stem)) reserved.add(stem);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const moduleNameByFile = new Map();
|
|
322
|
+
const claimed = new Set();
|
|
323
|
+
for (const file of routerFiles) {
|
|
324
|
+
const stem = path.basename(file, '.py');
|
|
325
|
+
if (!GENERIC_ROUTER_STEMS.has(stem)) {
|
|
326
|
+
moduleNameByFile.set(file, stem);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const dir = path.dirname(file);
|
|
330
|
+
const candidate = dir !== projectRoot ? path.basename(dir) : null;
|
|
331
|
+
if (candidate && !reserved.has(candidate) && !claimed.has(candidate)) {
|
|
332
|
+
claimed.add(candidate);
|
|
333
|
+
moduleNameByFile.set(file, candidate);
|
|
334
|
+
} else {
|
|
335
|
+
moduleNameByFile.set(file, stem); // collision or no meaningful parent -- keep the old literal name
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return moduleNameByFile;
|
|
339
|
+
}
|
|
340
|
+
|
|
290
341
|
export function scanPythonFastApi(repoRoot, projectRoot) {
|
|
291
342
|
const files = listPythonFiles(projectRoot);
|
|
343
|
+
const fileTextCache = new Map();
|
|
344
|
+
const readFile = (f) => {
|
|
345
|
+
if (!fileTextCache.has(f)) fileTextCache.set(f, fs.readFileSync(f, 'utf8'));
|
|
346
|
+
return fileTextCache.get(f);
|
|
347
|
+
};
|
|
348
|
+
const moduleNameByFile = resolveGenericModuleNames(files, readFile, projectRoot);
|
|
292
349
|
const modules = new Map();
|
|
293
350
|
const moduleEntry = (name) => {
|
|
294
351
|
if (!modules.has(name)) modules.set(name, { module: name, controllers: [], entities: [], enums: [], dtos: [] });
|
|
@@ -298,13 +355,15 @@ export function scanPythonFastApi(repoRoot, projectRoot) {
|
|
|
298
355
|
const allEntities = [];
|
|
299
356
|
const allDtos = [];
|
|
300
357
|
for (const file of files) {
|
|
301
|
-
const text =
|
|
358
|
+
const text = readFile(file);
|
|
302
359
|
|
|
303
360
|
if (/APIRouter\s*\(/.test(text)) {
|
|
304
361
|
// module = filename stem, NOT the router's own prefix -- verified against the real oracle's
|
|
305
362
|
// login.py, which declares `APIRouter(tags=["login"])` with no prefix at all, so a
|
|
306
363
|
// prefix-derived name fails on a real file while the filename stem works for every one.
|
|
307
|
-
|
|
364
|
+
// D-fastapi-generic-module-name: for a GENERIC stem (`endpoints`/`router`), this is the
|
|
365
|
+
// collision-safe, directory-derived name instead -- see resolveGenericModuleNames() above.
|
|
366
|
+
const moduleName = moduleNameByFile.get(file);
|
|
308
367
|
const routerPrefixes = extractRouterPrefixes(text);
|
|
309
368
|
const rawEndpoints = extractEndpoints(text);
|
|
310
369
|
|
|
@@ -430,14 +489,12 @@ export const adapter = {
|
|
|
430
489
|
})) {
|
|
431
490
|
messages.push({ level: 'info', code: 'fastapi-not-a-dependency', message: `found ${depFiles.length} Python project file(s), but none declare a fastapi dependency` });
|
|
432
491
|
}
|
|
433
|
-
|
|
434
|
-
try
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (!rgOk) {
|
|
440
|
-
messages.push({ level: 'warn', code: 'rg-missing', message: 'ripgrep (rg) is not on PATH -- this adapter shells out to it and will throw, not degrade, if it is missing' });
|
|
492
|
+
// D-zero-config-scan: traced live -- this does NOT throw at scan time as the message used to
|
|
493
|
+
// claim. detectPythonFastApiRoot()'s own rg shell-out is wrapped in a blanket try/catch that
|
|
494
|
+
// returns [] on failure, so a missing `rg` makes this adapter silently detect nothing
|
|
495
|
+
// (degrades to generic-grep) rather than crashing.
|
|
496
|
+
if (!binaryAvailable('rg')) {
|
|
497
|
+
messages.push({ level: 'warn', code: 'rg-missing', message: 'ripgrep (rg) is not on PATH -- this adapter shells out to it for file discovery and silently detects nothing without it (degrades to the generic-grep fallback), it does not throw' });
|
|
441
498
|
}
|
|
442
499
|
// D-openapi-extraction-hint: unlike java-spring, this adapter's own capabilities already
|
|
443
500
|
// declare `api.operations: false` (FastAPI assigns operationIds at runtime) -- --openapi-file
|
|
Binary file
|
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) {
|
|
@@ -265,12 +317,13 @@ export function runScan({ repoRoot, terms, includeDb = false, dbSchema = null, a
|
|
|
265
317
|
}
|
|
266
318
|
|
|
267
319
|
return {
|
|
268
|
-
schema: 'sbf.scan-report/
|
|
320
|
+
schema: 'sbf.scan-report/2',
|
|
269
321
|
terms,
|
|
270
322
|
adapter,
|
|
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
|
+
}
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"matched": { "type": "integer", "minimum": 0, "description": "receipts whose contract_ref equals the CURRENT contract hash -- these count as evidence for the current contract." },
|
|
31
31
|
"stale_contract_ref": { "type": "integer", "minimum": 0, "description": "receipts recorded against a contract_ref that no longer matches -- kept on record for audit/trend purposes, not counted as current evidence." },
|
|
32
32
|
"violations": { "type": "integer", "minimum": 0 },
|
|
33
|
-
"unsupported": { "type": "integer", "minimum": 0 }
|
|
33
|
+
"unsupported": { "type": "integer", "minimum": 0 },
|
|
34
|
+
"unsigned": { "type": "integer", "minimum": 0, "description": "receipts with no signature field -- computed regardless of --pubkey, for forward visibility." },
|
|
35
|
+
"signature_invalid": { "type": "integer", "minimum": 0, "description": "receipts whose signature field failed verification -- only ever non-zero when --pubkey was given; meaningless (always 0) otherwise. Excluded from matched/violations, same as stale_contract_ref." }
|
|
34
36
|
}
|
|
35
37
|
},
|
|
36
38
|
"by_operation": {
|
|
@@ -44,6 +46,15 @@
|
|
|
44
46
|
"violations": { "type": "integer", "minimum": 0 }
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
},
|
|
50
|
+
"verification": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"description": "Whether signature verification was even attempted at generation time -- without this, signature_invalid: 0 is ambiguous between 'verified, zero invalid' and 'never checked'. See D-runtime-conformance-receipts.",
|
|
54
|
+
"properties": {
|
|
55
|
+
"pubkey_given": { "type": "boolean" },
|
|
56
|
+
"require_signature": { "type": "boolean" }
|
|
57
|
+
}
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "urn:sbf:observe-receipt:1",
|
|
4
4
|
"title": "backend-skeleton runtime contract-conformance receipt",
|
|
5
|
-
"description": "One JSONL line ContractObservationAspect logs per @ObserveContract-annotated call. Verdict-only, by design -- never carries an observed request/response VALUE, only pointers + constraint kinds. See DECISIONS.md D-runtime-conformance-receipts. `bskel observe import --receipts <path>` reads a stream of these.",
|
|
5
|
+
"description": "One JSONL line ContractObservationAspect logs per @ObserveContract-annotated call. Verdict-only, by design -- never carries an observed request/response VALUE, only pointers + constraint kinds. See DECISIONS.md D-runtime-conformance-receipts. `bskel observe import --receipts <path>` reads a stream of these. `signature` is optional -- unsigned receipts stay valid; `bskel observe import --pubkey <path>` opts into verifying it where present. See the D-runtime-conformance-receipts 'cryptographic receipt attestation' Update note.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["feature_id", "feature_uid", "operation_id", "contract_ref", "verb", "recorded_at", "violations"],
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
"message": { "type": "string", "maxLength": 300 }
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
},
|
|
31
|
+
"signature": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["algorithm", "value"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"algorithm": { "const": "ed25519" },
|
|
37
|
+
"value": { "type": "string", "description": "base64-encoded raw Ed25519 signature bytes over the canonicalized receipt (this field excluded)." }
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
}
|
|
@@ -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
|
+
}
|
|
@@ -6,19 +6,23 @@
|
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"required": ["schema", "terms", "adapter", "verdict", "related_modules", "collisions", "unknowns", "files_read"],
|
|
8
8
|
"properties": {
|
|
9
|
-
"schema": { "const": "sbf.scan-report/
|
|
9
|
+
"schema": { "const": "sbf.scan-report/2" },
|
|
10
10
|
"feature_id": { "type": "string" },
|
|
11
11
|
"terms": { "type": "array", "items": { "type": "string" } },
|
|
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" },
|