@onlineapps/conn-orch-validator 7.0.0 → 8.1.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 +2582 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +409 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +22 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +123 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether this service has a database — and whether the repository agrees with
|
|
5
|
+
* what it declared.
|
|
6
|
+
*
|
|
7
|
+
* The fact has an owner already: `config/service/integration-contract.json` →
|
|
8
|
+
* `requiredConnectors.db`, which `utils/installContract.js` derives the whole
|
|
9
|
+
* SQL half of its contract from. So this uniform introduces NO second
|
|
10
|
+
* declaration (no `oa.db` field): confirmation 005 point 3 asks for a
|
|
11
|
+
* declaration only where the fact cannot be derived, and here it is already
|
|
12
|
+
* declared, once (lead decision `api/shared/TODO.md` §0.2b-9 point 1, reported
|
|
13
|
+
* to the owner as this reading of 005 under 004).
|
|
14
|
+
*
|
|
15
|
+
* Whether the declaration is THERE and is a boolean is decided by `C-CONTRACT`,
|
|
16
|
+
* which calls the contract validator that owns every rule of this file
|
|
17
|
+
* (`utils/bizCiGateContract.js`): a missing or non-boolean `requiredConnectors.db`
|
|
18
|
+
* is its `[BizCiGate] Invalid contract field` message. This uniform once carried
|
|
19
|
+
* a `D-DB-DECL` row saying the same thing in its own words; two rows for one
|
|
20
|
+
* rule is the duplicate `change-discipline.md` § One rail per concern forbids,
|
|
21
|
+
* and it was removed when `C-CONTRACT` began calling the validator (d.217b, lead
|
|
22
|
+
* decision).
|
|
23
|
+
*
|
|
24
|
+
* What the row below adds is the comparison the contract cannot make on its own:
|
|
25
|
+
* `false` against a repository that has migrations or a database client anyway,
|
|
26
|
+
* and `true` against a repository with nothing to install. Both directions,
|
|
27
|
+
* because a contract that lies in either direction sends the installer the
|
|
28
|
+
* wrong way — measured precedent: three repositories carried `REPO_HAS_DB=false`
|
|
29
|
+
* while their contract declared a database, and the SQL half checked nothing
|
|
30
|
+
* while printing PASS.
|
|
31
|
+
*
|
|
32
|
+
* @see api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-005
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
|
|
38
|
+
const { readIdentity, IDENTITY_FILE } = require('../serviceIdentity');
|
|
39
|
+
const { DATABASE_PREFIX } = require('./serviceIdentityRows');
|
|
40
|
+
|
|
41
|
+
/** Where the fact lives, and the key that carries it. */
|
|
42
|
+
const CONTRACT_PATH = 'config/service/integration-contract.json';
|
|
43
|
+
const MIGRATIONS_DIR = 'migrations';
|
|
44
|
+
|
|
45
|
+
/** The document an operator runs the SQL package from; the contract §3 requires it. */
|
|
46
|
+
const MIGRATIONS_README = 'migrations/README.md';
|
|
47
|
+
|
|
48
|
+
/** Where a service keeps its env templates, and the one that is not its own. */
|
|
49
|
+
const ENV_TEMPLATE_DIR = 'config/env-templates';
|
|
50
|
+
const SHARED_ENV = 'shared.env';
|
|
51
|
+
|
|
52
|
+
/** The key an env template declares the database account under. */
|
|
53
|
+
const ACCOUNT_KEY = 'DB_USER';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} serviceRoot repository root
|
|
57
|
+
* @returns {{ db: boolean }|{ problem: string }}
|
|
58
|
+
*/
|
|
59
|
+
function declaredDb(serviceRoot) {
|
|
60
|
+
const file = path.join(serviceRoot, ...CONTRACT_PATH.split('/'));
|
|
61
|
+
if (!fs.existsSync(file)) return { problem: 'the contract is absent' };
|
|
62
|
+
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { problem: `the contract is not valid JSON — ${error.message}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const value = (parsed.requiredConnectors || {}).db;
|
|
71
|
+
if (typeof value !== 'boolean') return { problem: 'requiredConnectors.db is missing' };
|
|
72
|
+
return { db: value };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hasMigrations = (serviceRoot) => fs.existsSync(path.join(serviceRoot, MIGRATIONS_DIR));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} serviceRoot repository root
|
|
79
|
+
* @param {ReadonlyArray<string>} packages the database clients the row names
|
|
80
|
+
* @returns {string[]} the ones this repository depends on
|
|
81
|
+
*/
|
|
82
|
+
function databaseClients(serviceRoot, packages) {
|
|
83
|
+
const file = path.join(serviceRoot, 'package.json');
|
|
84
|
+
if (!fs.existsSync(file)) return [];
|
|
85
|
+
const dependencies = JSON.parse(fs.readFileSync(file, 'utf8')).dependencies || {};
|
|
86
|
+
return packages.filter((name) => dependencies[name] !== undefined);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const dbConsistent = Object.freeze({
|
|
90
|
+
scope: 'service',
|
|
91
|
+
requires: Object.freeze(['forbidden_packages']),
|
|
92
|
+
|
|
93
|
+
run({ row, serviceRoot }) {
|
|
94
|
+
const declaration = declaredDb(serviceRoot);
|
|
95
|
+
// The declaration itself is C-CONTRACT's finding; this row says nothing
|
|
96
|
+
// about a fact it could not read, rather than guessing which way it went.
|
|
97
|
+
if (declaration.problem !== undefined) return [];
|
|
98
|
+
|
|
99
|
+
if (declaration.db === true) {
|
|
100
|
+
if (hasMigrations(serviceRoot)) return [];
|
|
101
|
+
return [{
|
|
102
|
+
where: CONTRACT_PATH,
|
|
103
|
+
what: 'requiredConnectors.db is true and there is no migrations/ directory — '
|
|
104
|
+
+ 'a fresh install of this service has nothing to run'
|
|
105
|
+
}];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const findings = [];
|
|
109
|
+
if (hasMigrations(serviceRoot)) {
|
|
110
|
+
findings.push({
|
|
111
|
+
where: CONTRACT_PATH,
|
|
112
|
+
what: 'requiredConnectors.db is false and migrations/ exists — one of the two is wrong'
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const client of databaseClients(serviceRoot, row.forbidden_packages)) {
|
|
116
|
+
findings.push({
|
|
117
|
+
where: 'package.json',
|
|
118
|
+
what: `requiredConnectors.db is false and ${JSON.stringify(client)} is a dependency — one of the two is wrong`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return findings;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The migration files of ONE level: `migrations/*.sql`, sorted.
|
|
128
|
+
*
|
|
129
|
+
* One level and no deeper, because that is exactly as far as the convention
|
|
130
|
+
* reaches. `BASELINE/` and `SEED/**` are the SQL package the installation
|
|
131
|
+
* contract owns, and what it says about them is what they must CONTAIN (§4, the
|
|
132
|
+
* four header fields — `D-DB-HEADERS`) and never what they are called;
|
|
133
|
+
* `archive/` and `superseded/` are history a repository keeps. Writing a regex
|
|
134
|
+
* over any of the three would be this row inventing a rule
|
|
135
|
+
* (`.claude/rules/truth-over-agreement.md` §6).
|
|
136
|
+
*
|
|
137
|
+
* @param {string} serviceRoot repository root
|
|
138
|
+
* @returns {string[]} file names
|
|
139
|
+
*/
|
|
140
|
+
function migrationFiles(serviceRoot, relative = MIGRATIONS_DIR) {
|
|
141
|
+
const dir = path.join(serviceRoot, ...relative.replace(/\/$/, '').split('/'));
|
|
142
|
+
if (!fs.existsSync(dir)) return [];
|
|
143
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
144
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.sql'))
|
|
145
|
+
.map((entry) => entry.name)
|
|
146
|
+
.sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The short name every per-service name is built from, or null when the
|
|
151
|
+
* repository has not said who it is — which is `C-SERVICE`'s finding, not this
|
|
152
|
+
* section's (`change-discipline.md` § One rail per concern).
|
|
153
|
+
*
|
|
154
|
+
* @param {string} serviceRoot repository root
|
|
155
|
+
* @returns {string|null}
|
|
156
|
+
*/
|
|
157
|
+
function shortNameOf(serviceRoot) {
|
|
158
|
+
const identity = readIdentity(serviceRoot);
|
|
159
|
+
return identity.problem === undefined ? identity.params.service_name : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The value one env template declares for a key, or null.
|
|
164
|
+
*
|
|
165
|
+
* Read as the file is written — `KEY=value`, the first declaration winning, the
|
|
166
|
+
* way a shell reading it would. A commented-out line declares nothing, which is
|
|
167
|
+
* the case `D-DB-ACCOUNT` reports rather than passing over.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} text the template
|
|
170
|
+
* @param {string} key the name
|
|
171
|
+
* @returns {string|null}
|
|
172
|
+
*/
|
|
173
|
+
function declaredValue(text, key) {
|
|
174
|
+
for (const line of text.split('\n')) {
|
|
175
|
+
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/);
|
|
176
|
+
if (match !== null && match[1] === key) return match[2].trim();
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The env template a service owns — the one named after it. `C-ENV` decides
|
|
183
|
+
* whether it is there and whether there is exactly one, and `C-IDENTITY`
|
|
184
|
+
* whether it carries this service's name; this function only finds it.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} serviceRoot repository root
|
|
187
|
+
* @param {string} shortName
|
|
188
|
+
* @returns {{relative: string, text: string}|null}
|
|
189
|
+
*/
|
|
190
|
+
function ownEnvTemplate(serviceRoot, shortName) {
|
|
191
|
+
const relative = `${ENV_TEMPLATE_DIR}/${shortName}.env`;
|
|
192
|
+
const file = path.join(serviceRoot, ...relative.split('/'));
|
|
193
|
+
if (fs.existsSync(file)) return { relative, text: fs.readFileSync(file, 'utf8') };
|
|
194
|
+
|
|
195
|
+
const dir = path.join(serviceRoot, ...ENV_TEMPLATE_DIR.split('/'));
|
|
196
|
+
if (!fs.existsSync(dir)) return null;
|
|
197
|
+
const others = fs.readdirSync(dir).filter((name) => name !== SHARED_ENV && name.endsWith('.env')).sort();
|
|
198
|
+
if (others.length !== 1) return null;
|
|
199
|
+
const only = `${ENV_TEMPLATE_DIR}/${others[0]}`;
|
|
200
|
+
return { relative: only, text: fs.readFileSync(path.join(serviceRoot, ...only.split('/')), 'utf8') };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The three rows below ask their question only of a repository that DECLARES a
|
|
205
|
+
* database, and say nothing about a declaration they could not read — that one
|
|
206
|
+
* is `C-CONTRACT`'s, which calls the validator owning every rule of that file.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} serviceRoot repository root
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
const hasDatabase = (serviceRoot) => declaredDb(serviceRoot).db === true;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* `D-DB-NAMING` — what a migration is CALLED is the only thing a reader has
|
|
215
|
+
* before opening it, and the only thing an ordering depends on.
|
|
216
|
+
*
|
|
217
|
+
* The shape is not this row's to invent: it is read back from the node that
|
|
218
|
+
* owns it, `api/docs/biz/70-contracts/database-contract.md` §5 — three digits,
|
|
219
|
+
* an underscore, a lowercase description. No second word is demanded, which is
|
|
220
|
+
* why `000_baseline.sql` passes: §5 prints it as an example and §8 makes a
|
|
221
|
+
* regenerated baseline an applied migration. No leading verb is demanded
|
|
222
|
+
* either, and the reason is measured rather than assumed —
|
|
223
|
+
* `converter/migrations/009_cnv_batch_drop_pending_status.sql` opens with a
|
|
224
|
+
* table prefix and is right. The three digits are not decoration: `999a_…`
|
|
225
|
+
* sorts after `999_…` only by accident of the byte after the digits, and a
|
|
226
|
+
* package installed in the wrong order is the failure the whole SQL contract
|
|
227
|
+
* exists to prevent.
|
|
228
|
+
*
|
|
229
|
+
* @see api/docs/biz/70-contracts/database-contract.md
|
|
230
|
+
*/
|
|
231
|
+
const dbMigrationNaming = Object.freeze({
|
|
232
|
+
scope: 'service',
|
|
233
|
+
requires: Object.freeze(['path', 'naming']),
|
|
234
|
+
|
|
235
|
+
run({ row, serviceRoot }) {
|
|
236
|
+
if (!hasDatabase(serviceRoot)) return [];
|
|
237
|
+
|
|
238
|
+
const shape = new RegExp(row.naming);
|
|
239
|
+
const dir = row.path.replace(/\/$/, '');
|
|
240
|
+
return migrationFiles(serviceRoot, dir)
|
|
241
|
+
.filter((name) => !shape.test(name))
|
|
242
|
+
.map((name) => ({
|
|
243
|
+
where: `${dir}/${name}`,
|
|
244
|
+
what: 'the name does not read NNN_snake_case.sql — three digits, an underscore, then a '
|
|
245
|
+
+ `lowercase description (${row.naming})`
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* `D-DB-README` — the document that says in which order the package is run.
|
|
252
|
+
*
|
|
253
|
+
* `D-DB-PACKAGE` calls the installation contract's `SQL_PACKAGE` requirement,
|
|
254
|
+
* and that requirement checks the three directories and that each holds a `.sql`
|
|
255
|
+
* file. The contract §3 asks for a fourth thing it does not check:
|
|
256
|
+
* `migrations/README.md`. So the row is not a second owner of `SQL_PACKAGE` —
|
|
257
|
+
* it is the half of §3 nothing was looking at.
|
|
258
|
+
*/
|
|
259
|
+
const dbReadme = Object.freeze({
|
|
260
|
+
scope: 'service',
|
|
261
|
+
requires: Object.freeze(['path']),
|
|
262
|
+
|
|
263
|
+
run({ row, serviceRoot }) {
|
|
264
|
+
if (!hasDatabase(serviceRoot)) return [];
|
|
265
|
+
if (fs.existsSync(path.join(serviceRoot, ...row.path.split('/')))) return [];
|
|
266
|
+
|
|
267
|
+
return [{
|
|
268
|
+
where: row.path,
|
|
269
|
+
what: 'the SQL package carries no README — the documented execution order that reproduces a working '
|
|
270
|
+
+ 'instance is the fourth thing the installation contract §3 requires, and the only one nothing '
|
|
271
|
+
+ 'else checks'
|
|
272
|
+
}];
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* `D-DB-ACCOUNT` — the service reaches its schema as ITSELF.
|
|
278
|
+
*
|
|
279
|
+
* Confirmation `db-accounts-per-service` 001: every service has its own account
|
|
280
|
+
* with grants on its own schemas, and root stops being an operational identity.
|
|
281
|
+
* The name is the same derivation `C-IDENTITY` holds `database.schema` to, read
|
|
282
|
+
* from the same file and built with the same prefix, so the account and the
|
|
283
|
+
* schema cannot drift into two spellings of one service.
|
|
284
|
+
*
|
|
285
|
+
* A repository that declares a database and no `DB_USER` is a FINDING, not a
|
|
286
|
+
* silence: an account nobody declared is one somebody will invent at install
|
|
287
|
+
* time, and that is how root became the identity of nine containers.
|
|
288
|
+
*/
|
|
289
|
+
const dbAccount = Object.freeze({
|
|
290
|
+
scope: 'service',
|
|
291
|
+
requires: Object.freeze(['path', 'key']),
|
|
292
|
+
|
|
293
|
+
run({ row, serviceRoot }) {
|
|
294
|
+
if (!hasDatabase(serviceRoot)) return [];
|
|
295
|
+
|
|
296
|
+
const shortName = shortNameOf(serviceRoot);
|
|
297
|
+
if (shortName === null) return [];
|
|
298
|
+
|
|
299
|
+
const expected = `${DATABASE_PREFIX}${shortName}`;
|
|
300
|
+
const template = ownEnvTemplate(serviceRoot, shortName);
|
|
301
|
+
if (template === null) {
|
|
302
|
+
return [{
|
|
303
|
+
where: row.path,
|
|
304
|
+
what: `no env template of this service's own declares ${row.key}, and this service's account is `
|
|
305
|
+
+ `${JSON.stringify(expected)}`
|
|
306
|
+
}];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const declared = declaredValue(template.text, row.key);
|
|
310
|
+
if (declared === null) {
|
|
311
|
+
return [{
|
|
312
|
+
where: template.relative,
|
|
313
|
+
what: `declares no ${row.key}, and this service's account is ${JSON.stringify(expected)}`
|
|
314
|
+
}];
|
|
315
|
+
}
|
|
316
|
+
if (declared === expected) return [];
|
|
317
|
+
|
|
318
|
+
return [{
|
|
319
|
+
where: template.relative,
|
|
320
|
+
what: `${row.key} is ${JSON.stringify(declared)}, and the account derived from ${IDENTITY_FILE} `
|
|
321
|
+
+ `is ${JSON.stringify(expected)}`
|
|
322
|
+
}];
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* `D-DB-COLLATION` — the schema is created with a collation this service CHOSE.
|
|
328
|
+
*
|
|
329
|
+
* Owner decision 2026-09-14 (`db-collation-declaration` 001): the collation of a
|
|
330
|
+
* biz schema is the service's own declaration, one key in the `database` block,
|
|
331
|
+
* and the CI gate and the production runbook both read it. Without it each of
|
|
332
|
+
* them invents one — which is how six schemas came to exist with the image
|
|
333
|
+
* default `utf8mb4_general_ci` while every table inside them declares
|
|
334
|
+
* `utf8mb4_bin` or `utf8mb4_unicode_ci` (measured 2026-09-14: 122 of 123
|
|
335
|
+
* permanent tables declare their collation explicitly).
|
|
336
|
+
*
|
|
337
|
+
* This row asks ONE question — is the key there — and deliberately not the other
|
|
338
|
+
* two. Whether the VALUE is a collation name is the contract validator's rule,
|
|
339
|
+
* which `C-CONTRACT` calls; refusing to BUILD without it is
|
|
340
|
+
* `utils/setupDatabase.js`, at the moment it would have to interpolate the
|
|
341
|
+
* value. A row restating either would be the duplicate that already cost
|
|
342
|
+
* `D-DB-DECL` its place (see the head of this file).
|
|
343
|
+
*
|
|
344
|
+
* It reads the `database` BLOCK rather than `requiredConnectors.db`, because the
|
|
345
|
+
* block is what the gate builds from: a contract without one has no schema to
|
|
346
|
+
* create, and `biz-ci-gate setup-db` says NOT APPLICABLE over it.
|
|
347
|
+
*
|
|
348
|
+
* @see api/docs/governance/confirmations/db-collation-declaration.md
|
|
349
|
+
*/
|
|
350
|
+
const dbCollation = Object.freeze({
|
|
351
|
+
scope: 'service',
|
|
352
|
+
requires: Object.freeze(['path', 'key']),
|
|
353
|
+
|
|
354
|
+
run({ row, serviceRoot }) {
|
|
355
|
+
const file = path.join(serviceRoot, ...row.path.split('/'));
|
|
356
|
+
if (!fs.existsSync(file)) return [];
|
|
357
|
+
|
|
358
|
+
let parsed;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
361
|
+
} catch (error) {
|
|
362
|
+
// An unreadable contract is `C-CONTRACT`'s finding, with its own fix.
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const database = parsed.database;
|
|
367
|
+
if (database === undefined || database === null || typeof database !== 'object') return [];
|
|
368
|
+
|
|
369
|
+
const declared = database[row.key];
|
|
370
|
+
if (typeof declared === 'string' && declared.trim() !== '') return [];
|
|
371
|
+
|
|
372
|
+
return [{
|
|
373
|
+
where: row.path,
|
|
374
|
+
what: `the database block declares no database.${row.key} — the CI gate and the production runbook both create `
|
|
375
|
+
+ 'this schema with the collation the service declares, and there is no default'
|
|
376
|
+
}];
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
checks: [
|
|
382
|
+
{ name: 'db-consistent', check: dbConsistent },
|
|
383
|
+
{ name: 'db-collation', check: dbCollation },
|
|
384
|
+
{ name: 'db-migration-naming', check: dbMigrationNaming },
|
|
385
|
+
{ name: 'db-readme', check: dbReadme },
|
|
386
|
+
{ name: 'db-account', check: dbAccount }
|
|
387
|
+
]
|
|
388
|
+
};
|