@onlineapps/conn-orch-validator 9.0.0 → 10.0.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/CHANGELOG.md +373 -0
- package/README.md +83 -9
- package/docs/DESIGN.md +21 -7
- package/manifests/biz-service.manifest.json +28 -5
- package/package.json +2 -2
- package/src/CookbookTestRunner.js +84 -16
- package/src/ValidationOrchestrator.js +73 -20
- package/src/cli/biz-ci-gate.js +28 -14
- package/src/cli/oa-sync-template.js +23 -8
- package/src/cli/oa-validate.js +7 -1
- package/src/index.js +21 -13
- package/src/lint/scripts/lintScripts.js +65 -18
- package/src/manifest/checks/composeRunnerBlock.js +37 -20
- package/src/manifest/checks/discoveryOrphan.js +2 -1
- package/src/manifest/checks/docsLintBridge.js +79 -21
- package/src/manifest/checks/gitTracked.js +12 -1
- package/src/manifest/checks/libraryPackage.js +3 -1
- package/src/manifest/checks/libraryWorkspace.js +18 -3
- package/src/manifest/checks/readmeRegion.js +9 -1
- package/src/manifest/checks/serviceConfig.js +29 -12
- package/src/manifest/checks/serviceFiles.js +34 -7
- package/src/manifest/checks/serviceIdentityRows.js +3 -1
- package/src/manifest/checks/serviceRuntime.js +3 -1
- package/src/manifest/discovery.js +25 -7
- package/src/manifest/runManifest.js +58 -7
- package/src/manifest/workspaceRoot.js +91 -5
- package/src/sync/serviceTemplate.js +76 -7
- package/src/sync/sharedEnv.js +11 -4
- package/src/sync/uniformFiles.js +91 -21
- package/src/utils/bizCiGateContract.js +25 -1
- package/src/utils/installContract.js +46 -5
- package/src/utils/libCompat.js +39 -19
- package/src/utils/preValidation.js +56 -11
- package/src/utils/stepFailure.js +106 -19
- package/src/utils/testCoverageContract.js +60 -2
- package/src/utils/throwawaySchema.js +92 -7
- package/src/validatorIdentity.js +31 -0
- package/src/validators/ServiceStructureValidator.js +41 -15
- package/src/validators/ValidationProofGenerator.js +73 -34
- package/templates/business-service/.dockerignore +9 -1
- package/templates/business-service/.gitlab-ci.yml +91 -25
- package/templates/business-service/README.md +14 -5
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
- package/templates/business-service/config/env-templates/shared.env +7 -1
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +1 -1
- package/templates/business-service/docs/80-setup/VALIDATION.md +1 -1
- package/templates/business-service/jest.config.js +9 -1
- package/templates/business-service/package.json.template +1 -1
- package/src/mocks/MockStorage.js +0 -188
package/src/sync/uniformFiles.js
CHANGED
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
* points the check at. A second list here would be a second owner of the shape,
|
|
12
12
|
* and the two would diverge exactly the way the nine copies of `init.sh` did.
|
|
13
13
|
*
|
|
14
|
-
* The corollary is that a row the manifest does not make renderable is
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* (
|
|
19
|
-
*
|
|
14
|
+
* The corollary is that a row the manifest does not make renderable is not a
|
|
15
|
+
* row of this run at all. `G-PROD-IMAGE` and `G-SETUP` name no reference — the
|
|
16
|
+
* first is a requirement about a pin inside a file, the second about a directory
|
|
17
|
+
* existing — so the MANIFEST run answers them and the sync does not plan them
|
|
18
|
+
* (`syncRows` below). Until d.507 the sync planned them and printed NOT RUN: a
|
|
19
|
+
* line every `--check` in every repository ended on, naming a state its reader
|
|
20
|
+
* could do nothing about, which is the false guarantee
|
|
21
|
+
* `.claude/rules/automation-gates.md` §5 calls a defect and the `Fix`-less
|
|
22
|
+
* message its §1 requirement 4 forbids.
|
|
23
|
+
*
|
|
24
|
+
* NOT RUN stays for the other case, which is the one it was made for: a row the
|
|
25
|
+
* sync MUST render and cannot reach the reference of — named, with the reason
|
|
26
|
+
* and the command that makes it reachable, never skipped in silence.
|
|
20
27
|
*
|
|
21
28
|
* WHERE A ROW NEEDS MORE THAN A SPLICE, THE MODULE THAT OWNS THE RULE RENDERS
|
|
22
29
|
* IT. Two rows are not "the reference, verbatim": the runner block carries three
|
|
@@ -39,10 +46,11 @@ const {
|
|
|
39
46
|
withoutBlock, IGNORE_BLOCK, DOCKERIGNORE_BLOCK
|
|
40
47
|
} = require('../manifest/checks/serviceFiles');
|
|
41
48
|
const {
|
|
42
|
-
spliceBlock, insertBlockUnder, topLevelKeys, replaceTopLevelKeys
|
|
49
|
+
spliceBlock, insertBlockUnder, insertBlockAfterFunction, topLevelKeys, replaceTopLevelKeys
|
|
43
50
|
} = require('./serviceTemplate');
|
|
44
51
|
const { requireIdentity } = require('../manifest/serviceIdentity');
|
|
45
52
|
const { rowNeedsWorkspace } = require('../manifest/manifestShape');
|
|
53
|
+
const { describeWorkspaceFix } = require('../manifest/workspaceRoot');
|
|
46
54
|
const { KINDS, applyUniformRegion } = require('./readmePointer');
|
|
47
55
|
const { serviceRegion } = require('./readmeLocation');
|
|
48
56
|
const { CHECK_REGISTRY } = require('../manifest/checks');
|
|
@@ -291,7 +299,12 @@ function renderDockerignoreEntries({ row, current, workspaceRoot }) {
|
|
|
291
299
|
`# --- ${DOCKERIGNORE_BLOCK}`,
|
|
292
300
|
`# Derived from ${referenceOwner(row.from)}, the declaration .gitignore reads too —`,
|
|
293
301
|
'# what a LOCAL production build must not copy into the image (being ignored by git',
|
|
294
|
-
|
|
302
|
+
'# excludes nothing from COPY . .), PLUS the test tree: tests stay in the repository',
|
|
303
|
+
'# and out of the artefact that runs, the same decision the library uniform makes as',
|
|
304
|
+
'# L-PACK-TESTS. The one exception is tests/cookbooks, which is not a test but a',
|
|
305
|
+
'# declaration the runtime reads — Tier-1 of the boot runs those cookbooks at phase',
|
|
306
|
+
'# 0.2, and an image without them refuses its own validation proof as NO_TESTS.',
|
|
307
|
+
"# Everything above this block is this repository's own.",
|
|
295
308
|
...missing,
|
|
296
309
|
`# --- end ${DOCKERIGNORE_BLOCK}`,
|
|
297
310
|
''
|
|
@@ -322,7 +335,11 @@ const RENDERERS = Object.freeze({
|
|
|
322
335
|
const SYNCABLE_CLASSES = Object.freeze(['identical', 'generated', 'contains']);
|
|
323
336
|
|
|
324
337
|
/**
|
|
325
|
-
* The manifest rows
|
|
338
|
+
* The manifest rows of the classes the generator owns, in manifest order.
|
|
339
|
+
*
|
|
340
|
+
* Not every one of them is a row this run renders — see `syncRows`. This is the
|
|
341
|
+
* full declaration, and the CLI needs it to tell a path the uniform declares as
|
|
342
|
+
* a REQUIREMENT from a path it has never heard of.
|
|
326
343
|
*
|
|
327
344
|
* @param {object} manifest
|
|
328
345
|
* @returns {object[]}
|
|
@@ -332,6 +349,34 @@ function uniformRows(manifest) {
|
|
|
332
349
|
return SYNCABLE_CLASSES.flatMap((className) => files[className] || []);
|
|
333
350
|
}
|
|
334
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Whether the sync is defined for this row: does it name what its content comes
|
|
354
|
+
* from?
|
|
355
|
+
*
|
|
356
|
+
* The predicate is the manifest's own distinction, not a list of ids: a row with
|
|
357
|
+
* a `from:` reference is a file rendered from that reference, a row without one
|
|
358
|
+
* is a requirement about the repository, and the reference is exactly what
|
|
359
|
+
* `desiredContent` would read. Judging the reference rather than the id is why a
|
|
360
|
+
* new requirement row needs no change here.
|
|
361
|
+
*
|
|
362
|
+
* @param {object} row
|
|
363
|
+
* @returns {boolean}
|
|
364
|
+
*/
|
|
365
|
+
function isSyncRow(row) {
|
|
366
|
+
if (!row || !row.from) return false;
|
|
367
|
+
return typeof row.from.path === 'string' || isPackageReference(row.from);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The manifest rows this run is defined by, in manifest order.
|
|
372
|
+
*
|
|
373
|
+
* @param {object} manifest
|
|
374
|
+
* @returns {object[]}
|
|
375
|
+
*/
|
|
376
|
+
function syncRows(manifest) {
|
|
377
|
+
return uniformRows(manifest).filter(isSyncRow);
|
|
378
|
+
}
|
|
379
|
+
|
|
335
380
|
/**
|
|
336
381
|
* Where two texts first differ, what the file says there, and what the run would
|
|
337
382
|
* put there instead.
|
|
@@ -373,17 +418,14 @@ function readServiceFile(serviceRoot, relative) {
|
|
|
373
418
|
}
|
|
374
419
|
|
|
375
420
|
/**
|
|
376
|
-
* What a row's file should contain
|
|
421
|
+
* What a row's file should contain.
|
|
377
422
|
*
|
|
378
|
-
*
|
|
423
|
+
* Only ever called for a row `isSyncRow` accepts, so the reference is there to
|
|
424
|
+
* be read; `planRow` is where that is checked, once, before anything reads.
|
|
425
|
+
*
|
|
426
|
+
* @returns {{desired: string}}
|
|
379
427
|
*/
|
|
380
428
|
function desiredContent({ row, current, serviceRoot, workspaceRoot }) {
|
|
381
|
-
if (!row.from || (typeof row.from.path !== 'string' && !isPackageReference(row.from))) {
|
|
382
|
-
return {
|
|
383
|
-
reason: 'the row declares no "from" reference, so nothing says what this file\'s content is rendered from'
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
429
|
const render = RENDERERS[row.check];
|
|
388
430
|
if (render !== undefined) return { desired: render({ row, current, serviceRoot, workspaceRoot }) };
|
|
389
431
|
|
|
@@ -401,6 +443,18 @@ function desiredContent({ row, current, serviceRoot, workspaceRoot }) {
|
|
|
401
443
|
throw new Error(`[UniformSync] Reference block "${row.block}" not found in ${referenceOwner(row.from)} - the row `
|
|
402
444
|
+ 'names a block the template does not carry. Fix: restore the block in the template, or correct the row.');
|
|
403
445
|
}
|
|
446
|
+
|
|
447
|
+
// The block is missing from a file that exists. That is "not synced", not
|
|
448
|
+
// "paste it in by hand": the content is generated and the run holds it, so the
|
|
449
|
+
// only open question is WHERE — and a row answers that by naming an anchor the
|
|
450
|
+
// file itself declares (`insert_after_function`). Without one the refusal
|
|
451
|
+
// stands, because the position would then be a decision about this service
|
|
452
|
+
// rather than a fact about the file (the same line `insertBlockUnder` draws
|
|
453
|
+
// for a compose mapping). A message that tells a human to copy generated bytes
|
|
454
|
+
// is the defect `.claude/rules/automation-gates.md` §1.4 names.
|
|
455
|
+
if (blockLines(current, row.block).length === 0 && typeof row.insert_after_function === 'string') {
|
|
456
|
+
return { desired: insertBlockAfterFunction({ text: current, fn: row.insert_after_function, replacement }) };
|
|
457
|
+
}
|
|
404
458
|
return { desired: spliceBlock({ text: current, block: row.block, replacement }) };
|
|
405
459
|
}
|
|
406
460
|
|
|
@@ -412,6 +466,15 @@ function desiredContent({ row, current, serviceRoot, workspaceRoot }) {
|
|
|
412
466
|
* current: string|null, desired?: string, detail?: string, reason?: string}}
|
|
413
467
|
*/
|
|
414
468
|
function planRow({ row, serviceRoot, workspaceRoot }) {
|
|
469
|
+
// Fail-fast rather than a NOT RUN line: `planSync` hands over only the rows
|
|
470
|
+
// `syncRows` selected, so a requirement row arriving here is a caller's
|
|
471
|
+
// mistake, and the caller is told which run does answer that row.
|
|
472
|
+
if (!isSyncRow(row)) {
|
|
473
|
+
throw new Error(`[UniformSync] ${row.id} declares no "from" reference - nothing says what ${row.path} `
|
|
474
|
+
+ 'would be rendered from, so this row is a requirement about the repository rather than a file this '
|
|
475
|
+
+ 'run writes. Fix: check it with npx oa-validate <serviceRoot>, which is the run that answers it.');
|
|
476
|
+
}
|
|
477
|
+
|
|
415
478
|
const current = readServiceFile(serviceRoot, row.path);
|
|
416
479
|
const base = { id: row.id, path: row.path, current };
|
|
417
480
|
|
|
@@ -425,9 +488,17 @@ function planRow({ row, serviceRoot, workspaceRoot }) {
|
|
|
425
488
|
return {
|
|
426
489
|
...base,
|
|
427
490
|
outcome: 'not-run',
|
|
491
|
+
// The check's own sentence, remedy included — not that sentence plus one
|
|
492
|
+
// of this module's. `workspaceRoot.js` § describeWorkspaceFix is the ONE
|
|
493
|
+
// owner of "point --workspace at a checkout that carries X" (d.516), and
|
|
494
|
+
// since d.523 every workspace-dependent check ends its NOT RUN sentence
|
|
495
|
+
// with it. Appending a second `Fix:` here produced two remedies for one
|
|
496
|
+
// absence, separated by a stray full stop — the second rail
|
|
497
|
+
// `.claude/rules/change-discipline.md` § One rail per concern names, in
|
|
498
|
+
// the one line a reader acts on.
|
|
428
499
|
reason: check.describeNotRun
|
|
429
500
|
? check.describeNotRun({ row, block: null })
|
|
430
|
-
:
|
|
501
|
+
: `the workspace root is not reachable. ${describeWorkspaceFix('api/ and api_biz/')}`
|
|
431
502
|
};
|
|
432
503
|
}
|
|
433
504
|
|
|
@@ -438,7 +509,6 @@ function planRow({ row, serviceRoot, workspaceRoot }) {
|
|
|
438
509
|
return { ...base, outcome: 'blocked', reason: error.message };
|
|
439
510
|
}
|
|
440
511
|
|
|
441
|
-
if (outcome.reason !== undefined) return { ...base, outcome: 'not-run', reason: outcome.reason };
|
|
442
512
|
if (outcome.desired === current) return { ...base, outcome: 'unchanged', desired: outcome.desired };
|
|
443
513
|
|
|
444
514
|
return {
|
|
@@ -456,7 +526,7 @@ function planRow({ row, serviceRoot, workspaceRoot }) {
|
|
|
456
526
|
* @returns {object[]} one entry per row, in manifest order
|
|
457
527
|
*/
|
|
458
528
|
function planSync({ manifest, serviceRoot, workspaceRoot, paths = [] }) {
|
|
459
|
-
const rows =
|
|
529
|
+
const rows = syncRows(manifest);
|
|
460
530
|
|
|
461
531
|
const wanted = paths.length === 0 ? rows : paths.map((wantedPath) => {
|
|
462
532
|
const row = rows.find((candidate) => candidate.path === wantedPath);
|
|
@@ -471,4 +541,4 @@ function planSync({ manifest, serviceRoot, workspaceRoot, paths = [] }) {
|
|
|
471
541
|
return wanted.map((row) => planRow({ row, serviceRoot, workspaceRoot }));
|
|
472
542
|
}
|
|
473
543
|
|
|
474
|
-
module.exports = { SYNCABLE_CLASSES, uniformRows, planRow, planSync, firstDifference };
|
|
544
|
+
module.exports = { SYNCABLE_CLASSES, uniformRows, isSyncRow, syncRows, planRow, planSync, firstDifference };
|
|
@@ -82,7 +82,31 @@ function assertRepoRelativePath(value, fieldName) {
|
|
|
82
82
|
function normalizeDatabaseDeclaration(database, connectors) {
|
|
83
83
|
// Absent means "this service has no database". Null rather than {} so callers
|
|
84
84
|
// distinguish that from an empty declaration and skip the steps explicitly.
|
|
85
|
-
|
|
85
|
+
//
|
|
86
|
+
// Absent is only allowed together with its connector. `db: true` with no
|
|
87
|
+
// block used to pass — a transition allowance while the six repositories
|
|
88
|
+
// still carried their own ci-setup-db.js ("F4 tightens this"). F4 landed:
|
|
89
|
+
// utils/setupDatabase.js replaced all six, none of the eight biz
|
|
90
|
+
// repositories carries one, and every repository declaring `db: true`
|
|
91
|
+
// declares a block (measured 2026-09-16), so the gate lands with compliance
|
|
92
|
+
// already in place (`automation-gates.md` §3) and the allowance goes with it
|
|
93
|
+
// (`architecture-principles.md` §11, no transition shims).
|
|
94
|
+
//
|
|
95
|
+
// What it cost while it stood: the combination makes `setup-db` report NOT
|
|
96
|
+
// APPLICABLE — correctly, there is no schema to build — while
|
|
97
|
+
// `wait-connectors` waits for a database and `run-prevalidation` then
|
|
98
|
+
// dispatches the service's own handlers against a schema nobody built. The
|
|
99
|
+
// DB steps must run exactly where a database is declared, and the two keys
|
|
100
|
+
// are one fact stated twice.
|
|
101
|
+
if (database === undefined || database === null) {
|
|
102
|
+
if (connectors.db) {
|
|
103
|
+
throw new Error('[BizCiGate] Contradictory contract - requiredConnectors.db is true but the contract '
|
|
104
|
+
+ 'carries no "database" block, so setup-db has no schema to build while wait-connectors waits for '
|
|
105
|
+
+ 'one and the cookbooks run against whatever is there. '
|
|
106
|
+
+ 'Fix: declare the database block (engine, schema, migrations), or set requiredConnectors.db to false.');
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
86
110
|
|
|
87
111
|
if (typeof database !== 'object' || Array.isArray(database)) {
|
|
88
112
|
throw new Error('[BizCiGate] Invalid database - Expected an object with engine, schema and migrations. '
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* applies — in both directions (§7)
|
|
21
21
|
* SQL_HEADERS every live SQL file declares its dataset class and safety, with
|
|
22
22
|
* VALUES from the contract's closed vocabularies and not merely the
|
|
23
|
-
* four keys (§4)
|
|
23
|
+
* four keys (§4). "Live" is what the installer applies: the
|
|
24
|
+
* migrations tree outside history, AND the seeds the integration
|
|
25
|
+
* contract declares, wherever they sit
|
|
26
|
+
* (`installation-sql-contract-scope` 003)
|
|
24
27
|
*
|
|
25
28
|
* Rules: api/docs/standards/repository-installation-sql-contract.md §2-§5, §7
|
|
26
29
|
* @see api/docs/governance/confirmations/installation-sql-contract-scope.md
|
|
@@ -127,8 +130,22 @@ function listSqlFiles(absoluteDir) {
|
|
|
127
130
|
* package directories alone: the root `migrations/*.sql` set IS the live
|
|
128
131
|
* migration set of four services, and until this loop reached it those files
|
|
129
132
|
* carried no header requirement at all while the gate reported PASS.
|
|
133
|
+
*
|
|
134
|
+
* `migrations/**` is not the whole scope either. What the contract covers is
|
|
135
|
+
* what the INSTALLER APPLIES, whatever directory it sits in
|
|
136
|
+
* (`installation-sql-contract-scope` 003: "Ano, vše co instalátor aplikuje"),
|
|
137
|
+
* and what the installer applies beyond the migration set is exactly the
|
|
138
|
+
* `database.seeds` list of the integration contract — read here from that one
|
|
139
|
+
* declaration rather than from a second list of directories, which would rot
|
|
140
|
+
* the moment a service put a seed somewhere new (`doc-code-binding.md` §1).
|
|
141
|
+
* Measured 2026-09-16: converter declares two seeds under `scripts/seed/` and
|
|
142
|
+
* ingest one, and the header gate had never looked at any of them.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} serviceRoot repository root to walk
|
|
145
|
+
* @param {string[]} seeds repo-relative seed paths the contract declares, which
|
|
146
|
+
* exist; one already inside `migrations/**` is not listed twice
|
|
130
147
|
*/
|
|
131
|
-
function listLiveSqlFiles(serviceRoot) {
|
|
148
|
+
function listLiveSqlFiles(serviceRoot, seeds = []) {
|
|
132
149
|
const found = [];
|
|
133
150
|
|
|
134
151
|
const walk = (relativeDir) => {
|
|
@@ -149,6 +166,11 @@ function listLiveSqlFiles(serviceRoot) {
|
|
|
149
166
|
};
|
|
150
167
|
|
|
151
168
|
walk(MIGRATIONS_ROOT);
|
|
169
|
+
|
|
170
|
+
for (const seed of seeds) {
|
|
171
|
+
if (!found.includes(seed)) found.push(seed);
|
|
172
|
+
}
|
|
173
|
+
|
|
152
174
|
return found;
|
|
153
175
|
}
|
|
154
176
|
|
|
@@ -285,7 +307,26 @@ function checkManifestCompleteness(serviceRoot, manifestFile, add) {
|
|
|
285
307
|
}
|
|
286
308
|
}
|
|
287
309
|
|
|
288
|
-
|
|
310
|
+
/**
|
|
311
|
+
* The seeds the contract declares AND the repository carries. A declared file
|
|
312
|
+
* that is not there is reported rather than read: the installer would stop on
|
|
313
|
+
* it, so the gate says so with the same words instead of throwing ENOENT out of
|
|
314
|
+
* a header check (`automation-gates.md` §1 requirement 4).
|
|
315
|
+
*/
|
|
316
|
+
function existingSeeds(serviceRoot, database, add) {
|
|
317
|
+
const declared = Array.isArray(database?.seeds) ? database.seeds : [];
|
|
318
|
+
|
|
319
|
+
return declared.filter((relativeFile) => {
|
|
320
|
+
if (existsExactly(serviceRoot, relativeFile.split('/'))) return true;
|
|
321
|
+
|
|
322
|
+
add('SQL_PACKAGE', 'Declared seed does not exist - config/service/integration-contract.json '
|
|
323
|
+
+ `declares "${relativeFile}" in database.seeds and the repository does not carry it. `
|
|
324
|
+
+ 'Fix: add the file, or remove the declaration — the installer applies exactly what is declared.');
|
|
325
|
+
return false;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function checkSqlPackage(serviceRoot, database, add) {
|
|
289
330
|
for (const relativeDir of SQL_DIRECTORIES) {
|
|
290
331
|
const absoluteDir = path.join(serviceRoot, relativeDir);
|
|
291
332
|
|
|
@@ -303,7 +344,7 @@ function checkSqlPackage(serviceRoot, add) {
|
|
|
303
344
|
}
|
|
304
345
|
}
|
|
305
346
|
|
|
306
|
-
const liveFiles = listLiveSqlFiles(serviceRoot);
|
|
347
|
+
const liveFiles = listLiveSqlFiles(serviceRoot, existingSeeds(serviceRoot, database, add));
|
|
307
348
|
|
|
308
349
|
for (const relativeFile of liveFiles) {
|
|
309
350
|
checkSqlHeaders(serviceRoot, relativeFile, add);
|
|
@@ -374,7 +415,7 @@ function verifyInstallContract(serviceRoot, database) {
|
|
|
374
415
|
|
|
375
416
|
const databaseChecked = database !== null && database !== undefined;
|
|
376
417
|
if (databaseChecked) {
|
|
377
|
-
checkSqlPackage(serviceRoot, add);
|
|
418
|
+
checkSqlPackage(serviceRoot, database, add);
|
|
378
419
|
}
|
|
379
420
|
|
|
380
421
|
return {
|
package/src/utils/libCompat.js
CHANGED
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
* to "not gated" (principle 3), and it would let a typo or a retired package
|
|
24
24
|
* name travel into an image with the gate reporting OK (measured 2026-09-14).
|
|
25
25
|
*
|
|
26
|
+
* The narrowing decides one thing only: which packages are COMPARED against an
|
|
27
|
+
* infra version. It never relaxes the exact-pin rule, which `^`, `~` and
|
|
28
|
+
* `latest` break in EVERY `@onlineapps/*` dependency, gated or not
|
|
29
|
+
* (`.claude/rules/architecture-principles.md` § Version pinning) — a floating
|
|
30
|
+
* range makes the same commit install different code on different days whether
|
|
31
|
+
* or not infra happens to ship that package too. Until 2026-09-15 (W413) the
|
|
32
|
+
* exactness test sat inside the gated branch, so a biz-only pin could float
|
|
33
|
+
* past a green gate.
|
|
34
|
+
*
|
|
26
35
|
* Pure module: the rules and the fetch live here, presentation and exit codes
|
|
27
36
|
* live in the CLI.
|
|
28
37
|
*
|
|
@@ -85,40 +94,51 @@ function checkLibCompat(pkg, librarySet) {
|
|
|
85
94
|
const violations = [];
|
|
86
95
|
|
|
87
96
|
for (const [name, version] of ours) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
notGated.push(name);
|
|
97
|
+
const bizOnly = Boolean(infraConsumed) && !infraConsumed.has(name);
|
|
98
|
+
const declared = versions[name];
|
|
99
|
+
|
|
100
|
+
if (bizOnly && declared === undefined) {
|
|
101
|
+
violations.push({
|
|
102
|
+
package: name,
|
|
103
|
+
message: `${name}: unknown to the platform library SSOT `
|
|
104
|
+
+ "- declared in neither 'libraries' nor 'infraConsumed', so no platform version exists to pin against. "
|
|
105
|
+
+ `Fix: publish ${name} and add it to config/libraries.json, or remove the pin.`
|
|
106
|
+
});
|
|
99
107
|
continue;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
if (bizOnly) notGated.push(name);
|
|
111
|
+
else gated.push(name);
|
|
112
|
+
|
|
113
|
+
if (declared === undefined) {
|
|
104
114
|
violations.push({
|
|
105
115
|
package: name,
|
|
106
|
-
message: `${name}:
|
|
107
|
-
+ 'because what resolves today is not what resolved when the image was built.'
|
|
116
|
+
message: `${name}: not present in the infra library set — the platform does not ship this package version contract.`
|
|
108
117
|
});
|
|
109
118
|
continue;
|
|
110
119
|
}
|
|
111
|
-
|
|
120
|
+
|
|
121
|
+
// The exact-pin rule is unconditional: `.claude/rules/architecture-principles.md`
|
|
122
|
+
// § Version pinning bans `^`, `~` and `latest` in EVERY `@onlineapps/*`
|
|
123
|
+
// dependency, so it is checked before the narrowing has any say. Being "not
|
|
124
|
+
// gated" answers one question only — is there an infra version to diverge
|
|
125
|
+
// from — and it never answers whether the pin may float.
|
|
126
|
+
if (!EXACT_VERSION.test(version)) {
|
|
112
127
|
violations.push({
|
|
113
128
|
package: name,
|
|
114
|
-
message: `${name}:
|
|
129
|
+
message: `${name}: version '${version}' is not exact x.y.z — caret/tilde/range pins are banned, `
|
|
130
|
+
+ 'because what resolves today is not what resolved when the image was built. '
|
|
131
|
+
+ `Fix: npm install ${name}@${declared} --save-exact.`
|
|
115
132
|
});
|
|
116
133
|
continue;
|
|
117
134
|
}
|
|
118
|
-
|
|
135
|
+
|
|
136
|
+
if (bizOnly) continue;
|
|
137
|
+
|
|
138
|
+
if (declared !== version) {
|
|
119
139
|
violations.push({
|
|
120
140
|
package: name,
|
|
121
|
-
message: `${name}: biz pins ${version}, infra ships ${
|
|
141
|
+
message: `${name}: biz pins ${version}, infra ships ${declared} — `
|
|
122
142
|
+ 'rebuild the service against the current platform, or deploy the matching infra release first.'
|
|
123
143
|
});
|
|
124
144
|
}
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
const fs = require('fs');
|
|
26
26
|
const path = require('path');
|
|
27
27
|
|
|
28
|
+
const { KIND_STEP, KIND_COOKBOOK_LOAD_FAILURE } = require('./stepFailure');
|
|
29
|
+
|
|
28
30
|
/**
|
|
29
31
|
* A step error is whatever the handler threw or the transport returned — often
|
|
30
32
|
* an object, not a string. The scripts this replaces interpolated it straight
|
|
@@ -42,10 +44,56 @@ function describeStepError(error) {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* One failed record of `results.steps`, in the shape the caller reports.
|
|
49
|
+
*
|
|
50
|
+
* `results.steps` holds TWO kinds of record and they are not both steps: a step
|
|
51
|
+
* result, and the entry `CookbookTestRunner.runCookbooks` pushes for a file the
|
|
52
|
+
* format check rejected — a whole cookbook that produced no step at all. Each
|
|
53
|
+
* DECLARES which it is in `kind`, written where the record is made (d.516b), so
|
|
54
|
+
* nothing here infers it from a missing field
|
|
55
|
+
* (`.claude/rules/architecture-principles.md` §8).
|
|
56
|
+
*
|
|
57
|
+
* This is the switchboard `utils/stepFailure.js` § describeStepFailureWithContext
|
|
58
|
+
* already keeps, for the structured half of the same answer: a cookbook that
|
|
59
|
+
* never ran is named by its FILE, because there is no step in it to name. Until
|
|
60
|
+
* d.518 both kinds were read as steps, so a rejected file reached the reader as
|
|
61
|
+
* `step_id: undefined` — a field naming nothing, about a step that does not
|
|
62
|
+
* exist, which `src/cli/biz-ci-gate.js` prints verbatim.
|
|
63
|
+
*
|
|
64
|
+
* The step's own `step_id` and nothing standing in for it: until d.516 this read
|
|
65
|
+
* `step.step_id || step.operation`, so a step that named no step_id was reported
|
|
66
|
+
* under its OPERATION — a field called `step_id` carrying something that is not
|
|
67
|
+
* one, which the reader has no way to see (§3, No Fallbacks). Since
|
|
68
|
+
* `@onlineapps/cookbook-core` 5.0.0 the schema requires `step_id` on every task
|
|
69
|
+
* step (`schemas/cookbook.v2.schema.json` definitions.TaskStep.required), and
|
|
70
|
+
* since d.460 the orchestrator refuses a task step that names no operation
|
|
71
|
+
* either (`@onlineapps/conn-orch-orchestrator` § _requireStepOperation), so a
|
|
72
|
+
* step arriving here without one is a broken cookbook — and a broken cookbook is
|
|
73
|
+
* what the outcome must show, not a plausible name.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} record a failed entry of `results.steps`
|
|
76
|
+
* @returns {{kind: string, error: string, validationErrors: string[]}} plus
|
|
77
|
+
* `cookbook` for a load failure, `step_id` for a step; WHICH of the two it is
|
|
78
|
+
* stays in `kind`, so a reader names it with `stepFailure.js`
|
|
79
|
+
* § describeRecordIdentity instead of picking a field
|
|
80
|
+
*/
|
|
81
|
+
function describeFailedRecord(record) {
|
|
82
|
+
const reason = {
|
|
83
|
+
error: describeStepError(record.error),
|
|
84
|
+
validationErrors: record.validationErrors || []
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (record.kind === KIND_COOKBOOK_LOAD_FAILURE) {
|
|
88
|
+
return { kind: KIND_COOKBOOK_LOAD_FAILURE, cookbook: record.cookbook, ...reason };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { kind: KIND_STEP, step_id: record.step_id, ...reason };
|
|
92
|
+
}
|
|
93
|
+
|
|
45
94
|
const COOKBOOKS_RELATIVE_DIR = path.join('tests', 'cookbooks');
|
|
46
95
|
const PROOF_RELATIVE_PATH = path.join('conn-runtime', 'validation-proof.json');
|
|
47
96
|
const OPERATIONS_RELATIVE_PATH = path.join('config', 'service', 'operations.json');
|
|
48
|
-
const VALIDATOR_NAME = '@onlineapps/conn-orch-validator';
|
|
49
97
|
const COOKBOOK_TIMEOUT_MS = 30000;
|
|
50
98
|
|
|
51
99
|
/**
|
|
@@ -104,7 +152,6 @@ async function runPreValidation({ serviceRoot, RunnerClass, ProofGeneratorClass,
|
|
|
104
152
|
const runner = new RunnerClass({
|
|
105
153
|
serviceName: packageJson.name,
|
|
106
154
|
servicePath: serviceRoot,
|
|
107
|
-
mockInfrastructure: true,
|
|
108
155
|
timeout: COOKBOOK_TIMEOUT_MS,
|
|
109
156
|
logger
|
|
110
157
|
});
|
|
@@ -117,12 +164,8 @@ async function runPreValidation({ serviceRoot, RunnerClass, ProofGeneratorClass,
|
|
|
117
164
|
// No proof on a failed run. A proof is a claim that the cookbooks passed,
|
|
118
165
|
// so writing one here would make the Registry trust a run that did not.
|
|
119
166
|
const failedSteps = results.steps
|
|
120
|
-
.filter((
|
|
121
|
-
.map(
|
|
122
|
-
step_id: step.step_id || step.operation,
|
|
123
|
-
error: describeStepError(step.error),
|
|
124
|
-
validationErrors: step.validationErrors || []
|
|
125
|
-
}));
|
|
167
|
+
.filter((record) => !record.passed)
|
|
168
|
+
.map(describeFailedRecord);
|
|
126
169
|
|
|
127
170
|
return { ok: false, results, failedSteps, proof: null, proofPath: null, filteredBy };
|
|
128
171
|
}
|
|
@@ -131,11 +174,13 @@ async function runPreValidation({ serviceRoot, RunnerClass, ProofGeneratorClass,
|
|
|
131
174
|
return { ok: true, results, failedSteps: [], proof: null, proofPath: null, filteredBy };
|
|
132
175
|
}
|
|
133
176
|
|
|
177
|
+
// The validator's own name and version are NOT passed in: they are this
|
|
178
|
+
// package's identity and the generator reads them from its own package.json
|
|
179
|
+
// (`src/validatorIdentity.js`, d.524). Passing them made this module carry a
|
|
180
|
+
// second copy of the package name.
|
|
134
181
|
const proofGenerator = new ProofGeneratorClass({
|
|
135
182
|
serviceName: packageJson.name,
|
|
136
|
-
serviceVersion: packageJson.version
|
|
137
|
-
validatorName: VALIDATOR_NAME,
|
|
138
|
-
validatorVersion: require('../../package.json').version
|
|
183
|
+
serviceVersion: packageJson.version
|
|
139
184
|
});
|
|
140
185
|
|
|
141
186
|
const proof = proofGenerator.generateProof(results, packageJson.dependencies || {});
|