@onlineapps/conn-orch-validator 9.0.0 → 11.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +546 -0
  2. package/README.md +337 -19
  3. package/docs/DESIGN.md +32 -9
  4. package/manifests/biz-service.manifest.json +56 -6
  5. package/package.json +3 -2
  6. package/src/CookbookTestRunner.js +134 -22
  7. package/src/ValidationOrchestrator.js +312 -73
  8. package/src/cli/biz-ci-gate.js +191 -15
  9. package/src/cli/oa-sync-template.js +23 -8
  10. package/src/cli/oa-validate.js +70 -2
  11. package/src/index.js +21 -13
  12. package/src/lint/scripts/lintScripts.js +65 -18
  13. package/src/manifest/checks/composeRunnerBlock.js +37 -20
  14. package/src/manifest/checks/discoveryOrphan.js +2 -1
  15. package/src/manifest/checks/docsLintBridge.js +79 -21
  16. package/src/manifest/checks/gitTracked.js +14 -28
  17. package/src/manifest/checks/libraryPackage.js +3 -1
  18. package/src/manifest/checks/libraryWorkspace.js +18 -3
  19. package/src/manifest/checks/readmeRegion.js +9 -1
  20. package/src/manifest/checks/serviceConfig.js +29 -12
  21. package/src/manifest/checks/serviceDb.js +176 -7
  22. package/src/manifest/checks/serviceFiles.js +34 -7
  23. package/src/manifest/checks/serviceIdentityRows.js +3 -1
  24. package/src/manifest/checks/serviceRuntime.js +126 -1
  25. package/src/manifest/discovery.js +25 -7
  26. package/src/manifest/gitCheckout.js +84 -0
  27. package/src/manifest/runManifest.js +58 -7
  28. package/src/manifest/workspaceRoot.js +91 -5
  29. package/src/sync/serviceTemplate.js +76 -7
  30. package/src/sync/sharedEnv.js +11 -4
  31. package/src/sync/uniformFiles.js +91 -21
  32. package/src/utils/bizCiGateContract.js +25 -1
  33. package/src/utils/dbAccountGrants.js +126 -0
  34. package/src/utils/envContract.js +36 -6
  35. package/src/utils/envReads.js +102 -0
  36. package/src/utils/installContract.js +46 -5
  37. package/src/utils/libCompat.js +39 -19
  38. package/src/utils/preValidation.js +56 -11
  39. package/src/utils/stepFailure.js +106 -19
  40. package/src/utils/stepReferences.js +278 -0
  41. package/src/utils/testCoverageContract.js +60 -2
  42. package/src/utils/throwawaySchema.js +92 -7
  43. package/src/validatorIdentity.js +31 -0
  44. package/src/validators/ServiceStructureValidator.js +47 -15
  45. package/src/validators/ValidationProofGenerator.js +73 -34
  46. package/templates/business-service/.dockerignore +9 -1
  47. package/templates/business-service/.gitlab-ci.yml +199 -35
  48. package/templates/business-service/Dockerfile +49 -16
  49. package/templates/business-service/README.md +56 -9
  50. package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +17 -5
  51. package/templates/business-service/config/env-templates/shared.env +8 -2
  52. package/templates/business-service/docker-compose.production.yml +9 -0
  53. package/templates/business-service/docker-compose.yml +17 -0
  54. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +1 -1
  55. package/templates/business-service/docs/80-setup/VALIDATION.md +1 -1
  56. package/templates/business-service/jest.config.js +9 -1
  57. package/templates/business-service/package.json.template +1 -1
  58. package/src/mocks/MockStorage.js +0 -188
@@ -40,10 +40,21 @@
40
40
  *
41
41
  * THE SCHEMA IS CHOSEN BY THE CALLER, NEVER BY THE FILE. A migration written
42
42
  * for the install runner commonly opens with `USE \`oagen_<service>\`;` — the
43
- * production schema by name. Applied verbatim against a throwaway, that one line
44
- * sends the whole file into the live schema instead. Those lines are therefore
45
- * stripped, and a schema select that survives the strip stops the build rather
46
- * than being applied.
43
+ * production schema by name (measured 2026-09-16: 69 live migrations, in
44
+ * api_biz/meta and api_biz/property). Applied verbatim against a throwaway, that
45
+ * one line sends the whole file into the live schema instead. Those lines are
46
+ * therefore stripped, and a schema select that survives the strip stops the
47
+ * build rather than being applied.
48
+ *
49
+ * Stripping is only half of it, because it moves the statements that carry no
50
+ * schema of their own. A name qualified with a schema — `INSERT INTO
51
+ * \`oagen_meta\`.persons …` — goes where it says whatever the runner selected,
52
+ * so it is refused outright rather than stripped: the rule is "the throwaway, or
53
+ * nothing", and the single exception is `information_schema`, which is read-only
54
+ * and whose `DATABASE()` follows the connection. Refusing is right even for a
55
+ * name that a strip could rewrite: what a test rewrites, it also has to be
56
+ * trusted to rewrite correctly, and a shim over somebody else's SQL is the
57
+ * workaround `change-discipline.md` forbids.
47
58
  *
48
59
  * @see src/utils/setupDatabase.js — the CI build, and the shared decisions
49
60
  * @see src/utils/deployContract.js — R8, which permits a file that imports this
@@ -66,6 +77,37 @@ const LEADING_SCHEMA_SELECT = /^\s*USE\s+/i;
66
77
  const SURVIVING_SCHEMA_SELECT = /(?:^|;)\s*USE\s+/i;
67
78
  /** An SQL comment line — prose cannot select a schema. */
68
79
  const SQL_COMMENT = /^\s*(?:--|#|\/\*|\*)/;
80
+ /** Everything a `--` or `#` opens on a line is prose too, wherever it starts. */
81
+ const TRAILING_COMMENT = /(--|#).*$/;
82
+
83
+ /**
84
+ * The positions where `a.b` is `<schema>.<object>` and not `<alias>.<column>`:
85
+ * a name in the TARGET position of a statement. Aliases live in expressions —
86
+ * after `ON`, after `SET`, in a `WHERE`, in the select list — and never
87
+ * directly after one of these keywords. That is why the check anchors on the
88
+ * keyword instead of matching every dotted token: measured over the live SQL of
89
+ * `api_biz/*` (2026-09-16), adding `ON` to the list turned 19 column references
90
+ * into findings, and matching dotted tokens outright would turn every alias in
91
+ * the tree into one.
92
+ */
93
+ const QUALIFIED_TARGET = new RegExp(
94
+ '\\b(?:FROM|JOIN|INTO|UPDATE|TABLE|REFERENCES|TRUNCATE|VIEW|TRIGGER|PROCEDURE|FUNCTION|CALL)\\s+'
95
+ + '(?:IF\\s+(?:NOT\\s+)?EXISTS\\s+)?'
96
+ + '`?([A-Za-z0-9_$]+)`?\\s*\\.\\s*`?[A-Za-z0-9_$]+`?',
97
+ 'gi'
98
+ );
99
+
100
+ /**
101
+ * The one qualifier a file may name besides the throwaway: the server's own
102
+ * catalog. It is read-only — the server refuses a write to it — and reading it
103
+ * is how a portable migration asks about its own shape
104
+ * (`SELECT … FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE()`,
105
+ * where `DATABASE()` is the schema the runner selected, i.e. the throwaway).
106
+ * Measured 2026-09-16: 22 live migration files in converter, ingest, meta and
107
+ * property read it that way, and every one of them is correct — a refusal there
108
+ * would be a finding nobody could fix (`automation-gates.md` §3).
109
+ */
110
+ const CATALOG_SCHEMA = 'information_schema';
69
111
 
70
112
  function require_(value, name, fix) {
71
113
  if (value === undefined || value === null || value === '') {
@@ -74,13 +116,44 @@ function require_(value, name, fix) {
74
116
  return value;
75
117
  }
76
118
 
119
+ /**
120
+ * The schemas a file names in a statement's target position, other than the one
121
+ * the runner selected. Comments are prose and are removed first, both the lines
122
+ * that are wholly comment and the tail a `--` opens on a line of SQL.
123
+ *
124
+ * @param {string} sql the SQL being applied
125
+ * @param {string} schema the throwaway the runner selected
126
+ * @returns {string[]} distinct qualifiers, in the order they appear
127
+ */
128
+ function foreignQualifiers(sql, schema) {
129
+ const found = [];
130
+
131
+ for (const line of sql.split('\n')) {
132
+ if (SQL_COMMENT.test(line)) continue;
133
+ const code = line.replace(TRAILING_COMMENT, '');
134
+
135
+ for (const match of code.matchAll(QUALIFIED_TARGET)) {
136
+ const qualifier = match[1];
137
+ if (qualifier === schema) continue;
138
+ if (qualifier.toLowerCase() === CATALOG_SCHEMA) continue;
139
+ if (!found.includes(qualifier)) found.push(qualifier);
140
+ }
141
+ }
142
+
143
+ return found;
144
+ }
145
+
77
146
  /**
78
147
  * The SQL of one migration, with the install runner's schema select removed.
79
148
  *
80
149
  * @param {string} file absolute path to the .sql file
150
+ * @param {string} schema the throwaway schema the statements are applied to
81
151
  * @returns {string}
82
152
  */
83
- function sqlWithoutSchemaSelect(file) {
153
+ function sqlWithoutSchemaSelect(file, schema) {
154
+ require_(schema, 'schema', 'pass the throwaway schema the statements are applied to; '
155
+ + 'without it there is nothing to compare a qualified name against.');
156
+
84
157
  const stripped = fs.readFileSync(file, 'utf8')
85
158
  .split('\n')
86
159
  .map((line) => (LEADING_SCHEMA_SELECT.test(line) ? '' : line))
@@ -99,6 +172,18 @@ function sqlWithoutSchemaSelect(file) {
99
172
  + 'schema for the file.');
100
173
  }
101
174
 
175
+ const foreign = foreignQualifiers(stripped, schema);
176
+ if (foreign.length > 0) {
177
+ throw new Error(`[ThrowawaySchema] ${path.basename(file)} names the schema `
178
+ + `"${foreign.join('", "')}" in a statement, and the throwaway is "${schema}" - refusing to `
179
+ + 'apply it.\n'
180
+ + ' Stripping the USE lines only moves the statements that carry no schema of their own; '
181
+ + 'a qualified name sends its statement somewhere else whatever the runner selected, and in a '
182
+ + 'migration that somewhere is the live schema.\n'
183
+ + ' Fix: write the name unqualified - the runner selects the schema for the file. '
184
+ + `Only ${CATALOG_SCHEMA} may be named, because it is read-only and follows DATABASE().`);
185
+ }
186
+
102
187
  return stripped;
103
188
  }
104
189
 
@@ -171,7 +256,7 @@ async function createThrowawaySchema({
171
256
 
172
257
  const apply = async (file, kind) => {
173
258
  const result = await exec({
174
- connection: conn, schema, file, sql: sqlWithoutSchemaSelect(file)
259
+ connection: conn, schema, file, sql: sqlWithoutSchemaSelect(file, schema)
175
260
  });
176
261
  if (result.status !== 0) {
177
262
  throw new Error(`[ThrowawaySchema] ${kind} ${path.basename(file)} failed:\n${result.stderr}\n`
@@ -198,7 +283,7 @@ async function createThrowawaySchema({
198
283
  migrationsApplied: toApply.length,
199
284
  seedsApplied: seedFiles.length,
200
285
  migrations: [...plan.migrations],
201
- readMigration: (name) => sqlWithoutSchemaSelect(fileOf(name)),
286
+ readMigration: (name) => sqlWithoutSchemaSelect(fileOf(name), schema),
202
287
  applyMigration: async (name) => apply(fileOf(name), 'Migration'),
203
288
  dispose: async () => run(dropSql)
204
289
  };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Who this validator IS — its own name and its own version.
5
+ *
6
+ * A validation proof and a deployability signal both NAME their author, and
7
+ * that name is not a caller's to choose: it is this package's identity, and the
8
+ * one place it is written down is this package's `package.json`. Read once,
9
+ * here, so the value cannot differ between the documents that carry it.
10
+ *
11
+ * Before this the same fact stood in four places: twice as
12
+ * `require('../package.json').version` inside `ValidationOrchestrator`, once
13
+ * more in `utils/preValidation.js` beside a hand-written copy of the package
14
+ * NAME, and once in `validators/ValidationProofGenerator` as
15
+ * `options.validatorVersion || '1.0.0'` — an invented number that reached a
16
+ * signed document whenever the caller passed none
17
+ * (`.claude/rules/architecture-principles.md` §3, No Fallbacks).
18
+ *
19
+ * This is the package's own metadata, not configuration: nothing about it is
20
+ * environment- or caller-dependent, so there is nothing to inject.
21
+ */
22
+
23
+ const { name, version } = require('../package.json');
24
+
25
+ /** `@onlineapps/conn-orch-validator` — the author every proof names. */
26
+ const VALIDATOR_NAME = name;
27
+
28
+ /** The published version of this package, as its manifest states it. */
29
+ const VALIDATOR_VERSION = version;
30
+
31
+ module.exports = { VALIDATOR_NAME, VALIDATOR_VERSION };
@@ -460,10 +460,15 @@ class ServiceStructureValidator {
460
460
  this.info.push('✓ Found Configuration directory');
461
461
  }
462
462
 
463
+ // `tests/cookbooks` is NOT here: it is a boot condition, raised by
464
+ // `validateTestStructure()` as an error (d.584). These two are
465
+ // recommendations — no row of the biz-service uniform demands that either
466
+ // directory exist (`S-TEST` fixes the body of `npm test`, `S-INT-C` carries
467
+ // `only_with: tests/integration`), so this is the only thing that mentions
468
+ // them and it stays a warning.
463
469
  const recommendedDirs = [
464
470
  { path: 'tests/unit', description: 'Unit tests' },
465
- { path: 'tests/integration', description: 'Integration tests' },
466
- { path: 'tests/cookbooks', description: 'Cookbook tests' }
471
+ { path: 'tests/integration', description: 'Integration tests' }
467
472
  ];
468
473
 
469
474
  for (const dir of requiredDirs) {
@@ -871,23 +876,50 @@ class ServiceStructureValidator {
871
876
  }
872
877
 
873
878
  /**
874
- * Validate test structure
879
+ * Validate test structure — `tests/cookbooks` with at least one recipe.
880
+ *
881
+ * This is a BOOT CONDITION, not a recommendation (d.584): Tier-1 validates
882
+ * the service's operations BY these recipes at phase 0.2, so a service
883
+ * without them does not start. Until d.584 the absence was reported twice
884
+ * and softly — a `MISSING_RECOMMENDED_DIRECTORY` warning when the directory
885
+ * was gone, a `NO_COOKBOOK_TESTS` warning when it was empty — and the run
886
+ * then died four steps later on the proof, which `ValidationProofGenerator`
887
+ * refuses for `testsRun: 0` under the unrelated name NO_TESTS. Late,
888
+ * indirect, and in two voices; now one error, at the step that checks it
889
+ * (`automation-gates.md` §1 requirement 4).
890
+ *
891
+ * The same fact is already stated from the other side by rule
892
+ * F-DOCKERIGNORE, which excludes the whole test tree from the production
893
+ * image and keeps `tests/cookbooks` (d.560). One rail, two readers.
894
+ *
895
+ * What counts as a recipe is what the runner counts —
896
+ * `CookbookTestRunner.runCookbooks` reads the `.json` files of this
897
+ * directory — so the two never disagree about an empty set.
875
898
  */
876
899
  validateTestStructure() {
877
900
  const cookbooksPath = path.join(this.serviceRoot, 'tests/cookbooks');
878
- if (fs.existsSync(cookbooksPath)) {
879
- const cookbookFiles = fs.readdirSync(cookbooksPath).filter(f => f.endsWith('.json'));
880
- if (cookbookFiles.length === 0) {
881
- this.warnings.push({
882
- type: 'NO_COOKBOOK_TESTS',
883
- path: 'tests/cookbooks',
884
- message: 'No cookbook tests found in tests/cookbooks/',
885
- fix: 'Create cookbook tests for pre-validation. See: /docs/standards/TESTING.md'
886
- });
887
- } else {
888
- this.info.push(`✓ Found ${cookbookFiles.length} cookbook test(s)`);
889
- }
901
+ const cookbookFiles = fs.existsSync(cookbooksPath)
902
+ ? fs.readdirSync(cookbooksPath).filter(f => f.endsWith('.json'))
903
+ : [];
904
+
905
+ if (cookbookFiles.length === 0) {
906
+ this.errors.push({
907
+ type: 'MISSING_COOKBOOKS',
908
+ path: 'tests/cookbooks',
909
+ // The problem only. The remedy is `fix`, and the printer composes the
910
+ // two — `formatResult` below writes a `Fix:` line of its own, the
911
+ // service wrapper composes `message (Fix: fix)` inline. A message that
912
+ // carries `Fix:` as well prints the remedy twice, which reads as two
913
+ // instructions; this was the only one of the validator's findings that
914
+ // did it (d.599).
915
+ message: '[ServiceStructure] tests/cookbooks is missing or empty - Tier-1 boot '
916
+ + 'validates operations by these recipes.',
917
+ fix: 'add tests/cookbooks/<op>.json'
918
+ });
919
+ return;
890
920
  }
921
+
922
+ this.info.push(`✓ Found ${cookbookFiles.length} cookbook test(s)`);
891
923
  }
892
924
 
893
925
  /**
@@ -1,6 +1,30 @@
1
1
  'use strict';
2
2
 
3
3
  const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
4
+ const { VALIDATOR_NAME, VALIDATOR_VERSION } = require('../validatorIdentity');
5
+
6
+ /**
7
+ * Read one measurement out of the runner's aggregate, or refuse.
8
+ *
9
+ * Every number in a validation proof is a measurement of a run that happened,
10
+ * so a missing one has no substitute value. The four fields below used to be
11
+ * filled with `testResults.<field> || 0`, which turned "nobody measured this"
12
+ * into "measured, and it was zero" — the same number, and no way for a reader
13
+ * of the proof to tell which of the two it is (`architecture-principles.md` §3,
14
+ * No Fallbacks).
15
+ */
16
+ function readMeasurement(testResults, field, proofField) {
17
+ const value = testResults === null || testResults === undefined ? undefined : testResults[field];
18
+
19
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
20
+ const rendered = typeof value === 'number' ? String(value) : JSON.stringify(value);
21
+ throw new Error(`[ValidationProofGenerator] Missing measurement - testResults.${field} is ${rendered}, `
22
+ + `and the proof field ${proofField} may not be filled from anything else. `
23
+ + 'Fix: hand in the aggregate CookbookTestRunner.runCookbooks() returns, which counts every step it ran.');
24
+ }
25
+
26
+ return value;
27
+ }
4
28
 
5
29
  /**
6
30
  * ValidationProofGenerator - Generates validation proof from test results
@@ -15,10 +39,25 @@ const { ValidationProofCodec } = require('@onlineapps/service-validator-core');
15
39
  */
16
40
  class ValidationProofGenerator {
17
41
  constructor(options = {}) {
42
+ // WHO the validator is is not an option. It used to be one, with
43
+ // `|| '1.0.0'` behind it, so a caller that passed nothing got an invented
44
+ // version inside a signed document — and a caller that passed something got
45
+ // to say this package was whatever it liked. The identity has one owner
46
+ // (`src/validatorIdentity.js`), and an attempt to set it is refused rather
47
+ // than ignored: an option that silently does nothing is the implicit
48
+ // behaviour `.claude/rules/architecture-principles.md` §8 forbids.
49
+ for (const retired of ['validatorName', 'validatorVersion']) {
50
+ if (options[retired] !== undefined) {
51
+ throw new Error(`[ValidationProofGenerator] ${retired} is not an option - the proof names THIS `
52
+ + `package (${VALIDATOR_NAME} ${VALIDATOR_VERSION}), read from its own package.json. `
53
+ + `Fix: drop ${retired} from the constructor options.`);
54
+ }
55
+ }
56
+
18
57
  this.serviceName = options.serviceName;
19
58
  this.serviceVersion = options.serviceVersion;
20
- this.validatorName = options.validatorName || '@onlineapps/conn-orch-validator';
21
- this.validatorVersion = options.validatorVersion || '1.0.0';
59
+ this.validatorName = VALIDATOR_NAME;
60
+ this.validatorVersion = VALIDATOR_VERSION;
22
61
  }
23
62
 
24
63
  /**
@@ -26,9 +65,14 @@ class ValidationProofGenerator {
26
65
  *
27
66
  * @param {Object} testResults - Results from test execution
28
67
  * @param {Object} dependencies - Service dependencies with versions
68
+ * @param {Object} [options] - what the caller MEASURED about the service
69
+ * @param {string} [options.contractFingerprint] - the fingerprint of the
70
+ * contract this run validated, when the caller computed one. Absent means
71
+ * absent: the field is left out rather than filled with a placeholder, and
72
+ * the proof schema marks it optional for exactly that reason.
29
73
  * @returns {Object} Validation proof with metadata
30
74
  */
31
- generateProof(testResults, dependencies = {}) {
75
+ generateProof(testResults, dependencies = {}, { contractFingerprint } = {}) {
32
76
  // Transform test results to ValidationProofSchema format
33
77
  const validationData = {
34
78
  serviceName: this.serviceName,
@@ -36,44 +80,39 @@ class ValidationProofGenerator {
36
80
  validator: this.validatorName,
37
81
  validatorVersion: this.validatorVersion,
38
82
  validatedAt: new Date().toISOString(),
39
- durationMs: testResults.duration || 0,
40
- testsRun: testResults.total || 0,
41
- testsPassed: testResults.passed || 0,
42
- testsFailed: testResults.failed || 0,
83
+ durationMs: readMeasurement(testResults, 'duration', 'durationMs'),
84
+ testsRun: readMeasurement(testResults, 'total', 'testsRun'),
85
+ testsPassed: readMeasurement(testResults, 'passed', 'testsPassed'),
86
+ testsFailed: readMeasurement(testResults, 'failed', 'testsFailed'),
43
87
  dependencies: dependencies || {}
44
88
  };
45
89
 
90
+ if (contractFingerprint !== undefined) {
91
+ validationData.contractFingerprint = contractFingerprint;
92
+ }
93
+
94
+ // A run that executed nothing has nothing to certify. `runPreValidation`
95
+ // writes a proof whenever no step FAILED, and a service whose
96
+ // tests/cookbooks/ holds no .json file has no failing step — so this
97
+ // generator used to author a signed proof claiming a passing validation of
98
+ // zero tests, and the CLI printed `OK run-prevalidation` beside it.
99
+ //
100
+ // The platform already held that such a proof is invalid: the same codec's
101
+ // decode() refuses `testsRun <= 0` as NO_TESTS. Only the party AUTHORING it
102
+ // did not, so the contradiction surfaced at the registry, days later and far
103
+ // from the cause. It is refused here, where it is created
104
+ // (`architecture-principles.md` §4, Fail-Fast).
105
+ if (validationData.testsRun === 0) {
106
+ throw new Error('[ValidationProofGenerator] Refusing a proof for a run that executed nothing - '
107
+ + 'testsRun is 0, so the proof would claim a passing validation of cookbooks that never ran; '
108
+ + 'ValidationProofCodec.decode() refuses exactly this proof as NO_TESTS. '
109
+ + 'Fix: give the service the cookbooks its operations are validated by, under tests/cookbooks/.');
110
+ }
111
+
46
112
  // Delegate encoding to centralized codec
47
113
  // This ensures Generator and Verifier use EXACTLY the same algorithm
48
114
  return ValidationProofCodec.encode(validationData);
49
115
  }
50
-
51
- /**
52
- * Extract test results summary
53
- */
54
- extractTestSummary(testResults) {
55
- return {
56
- total: testResults.total || 0,
57
- passed: testResults.passed || 0,
58
- failed: testResults.failed || 0,
59
- duration: testResults.duration || 0,
60
- coverage: testResults.coverage || 0
61
- };
62
- }
63
-
64
- /**
65
- * Create validation error
66
- */
67
- createValidationError(code, message, details = {}) {
68
- return {
69
- error: {
70
- code,
71
- message,
72
- details,
73
- timestamp: new Date().toISOString()
74
- }
75
- };
76
- }
77
116
  }
78
117
 
79
118
  module.exports = ValidationProofGenerator;
@@ -1,7 +1,12 @@
1
1
  # --- oa-dockerignore v1
2
2
  # Derived from templates/business-service/gitignore, the declaration .gitignore reads too —
3
3
  # what a LOCAL production build must not copy into the image (being ignored by git
4
- # excludes nothing from COPY . .). Everything above this block is this repository's own.
4
+ # excludes nothing from COPY . .), PLUS the test tree: tests stay in the repository
5
+ # and out of the artefact that runs, the same decision the library uniform makes as
6
+ # L-PACK-TESTS. The one exception is tests/cookbooks, which is not a test but a
7
+ # declaration the runtime reads — Tier-1 of the boot runs those cookbooks at phase
8
+ # 0.2, and an image without them refuses its own validation proof as NO_TESTS.
9
+ # Everything above this block is this repository's own.
5
10
  node_modules
6
11
  **/node_modules
7
12
  config/runtime
@@ -38,5 +43,8 @@ Thumbs.db
38
43
  **/Thumbs.db
39
44
  .oa_drive_deps_hash
40
45
  **/.oa_drive_deps_hash
46
+ tests
47
+ **/tests
48
+ !tests/cookbooks
41
49
  .git
42
50
  # --- end oa-dockerignore v1