@onlineapps/conn-orch-validator 6.0.1 → 8.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 (102) hide show
  1. package/CHANGELOG.md +2591 -2
  2. package/README.md +1075 -7
  3. package/docs/DESIGN.md +3 -1
  4. package/manifests/biz-service.manifest.json +658 -0
  5. package/manifests/library.manifest.json +324 -0
  6. package/package.json +12 -6
  7. package/src/CookbookTestRunner.js +422 -104
  8. package/src/CookbookTestUtils.js +7 -8
  9. package/src/ServiceReadinessValidator.js +78 -42
  10. package/src/ValidationOrchestrator.js +298 -75
  11. package/src/cli/biz-ci-gate.js +176 -33
  12. package/src/cli/oa-lint-scripts.js +221 -0
  13. package/src/cli/oa-sync-template.js +1020 -0
  14. package/src/cli/oa-validate.js +474 -0
  15. package/src/helpers/README.md +12 -2
  16. package/src/helpers/createServiceReadinessTests.js +75 -6
  17. package/src/index.js +33 -3
  18. package/src/lint/scripts/lintScripts.js +298 -0
  19. package/src/manifest/checks/composeRunnerBlock.js +222 -0
  20. package/src/manifest/checks/composeShape.js +165 -0
  21. package/src/manifest/checks/contractBridge.js +181 -0
  22. package/src/manifest/checks/discoveryOrphan.js +50 -0
  23. package/src/manifest/checks/docsLintBridge.js +553 -0
  24. package/src/manifest/checks/fileAbsent.js +35 -0
  25. package/src/manifest/checks/gitTracked.js +204 -0
  26. package/src/manifest/checks/index.js +111 -0
  27. package/src/manifest/checks/libraryContext.js +226 -0
  28. package/src/manifest/checks/libraryDocs.js +75 -0
  29. package/src/manifest/checks/libraryPackage.js +272 -0
  30. package/src/manifest/checks/librarySource.js +274 -0
  31. package/src/manifest/checks/libraryTests.js +121 -0
  32. package/src/manifest/checks/libraryWorkspace.js +293 -0
  33. package/src/manifest/checks/readmeRegion.js +135 -0
  34. package/src/manifest/checks/scriptHeaders.js +79 -0
  35. package/src/manifest/checks/serviceConfig.js +390 -0
  36. package/src/manifest/checks/serviceConnectors.js +81 -0
  37. package/src/manifest/checks/serviceDb.js +388 -0
  38. package/src/manifest/checks/serviceFiles.js +754 -0
  39. package/src/manifest/checks/serviceIdentityRows.js +351 -0
  40. package/src/manifest/checks/serviceRuntime.js +295 -0
  41. package/src/manifest/checks/serviceScripts.js +213 -0
  42. package/src/manifest/deployabilitySignal.js +121 -0
  43. package/src/manifest/discovery.js +386 -0
  44. package/src/manifest/loadManifest.js +62 -0
  45. package/src/manifest/manifestShape.js +446 -0
  46. package/src/manifest/report.js +245 -0
  47. package/src/manifest/runManifest.js +449 -0
  48. package/src/manifest/serviceIdentity.js +140 -0
  49. package/src/manifest/walk.js +74 -0
  50. package/src/manifest/workspaceRoot.js +242 -0
  51. package/src/mocks/MockMQClient.js +13 -30
  52. package/src/mocks/MockRegistry.js +4 -2
  53. package/src/mocks/MockStorage.js +4 -2
  54. package/src/sync/docsRegion.js +463 -0
  55. package/src/sync/generatedRegion.js +228 -0
  56. package/src/sync/readmeLocation.js +182 -0
  57. package/src/sync/readmePointer.js +477 -0
  58. package/src/sync/serviceTemplate.js +583 -0
  59. package/src/sync/sharedEnv.js +162 -0
  60. package/src/sync/uniformFiles.js +474 -0
  61. package/src/utils/bizCiGateContract.js +131 -7
  62. package/src/utils/connectorContract.js +97 -7
  63. package/src/utils/cookbookFormat.js +81 -40
  64. package/src/utils/deployContract.js +213 -13
  65. package/src/utils/envContract.js +57 -1
  66. package/src/utils/handlerRef.js +181 -0
  67. package/src/utils/installContract.js +287 -41
  68. package/src/utils/libCompat.js +29 -7
  69. package/src/utils/migrationOrder.js +163 -0
  70. package/src/utils/preValidation.js +20 -7
  71. package/src/utils/setupDatabase.js +194 -13
  72. package/src/utils/testCoverageContract.js +539 -0
  73. package/src/utils/testNamespace.js +247 -23
  74. package/src/utils/throwawaySchema.js +207 -0
  75. package/src/validators/ServiceStructureValidator.js +21 -20
  76. package/templates/business-service/.dockerignore +42 -0
  77. package/templates/business-service/.gitlab-ci.yml +290 -0
  78. package/templates/business-service/Dockerfile +27 -0
  79. package/templates/business-service/README.md +213 -0
  80. package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
  81. package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
  82. package/templates/business-service/config/env-templates/shared.env +65 -0
  83. package/templates/business-service/config/service/config.json +14 -0
  84. package/templates/business-service/config/service/integration-contract.json +12 -0
  85. package/templates/business-service/config/service/operations.json +41 -0
  86. package/templates/business-service/docker-compose.production.yml +60 -0
  87. package/templates/business-service/docker-compose.yml +93 -0
  88. package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
  89. package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
  90. package/templates/business-service/docs/80-setup/README.md +18 -0
  91. package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
  92. package/templates/business-service/docs/README.md +18 -0
  93. package/templates/business-service/gitignore +42 -0
  94. package/templates/business-service/index.js +10 -0
  95. package/templates/business-service/init.sh +54 -0
  96. package/templates/business-service/jest.config.js +6 -0
  97. package/templates/business-service/package.json.template +31 -0
  98. package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
  99. package/templates/business-service/src/handlers/v3/echo.js +39 -0
  100. package/templates/business-service/tests/cookbooks/echo.json +36 -0
  101. package/templates/business-service/tests/unit/handler.test.js +78 -0
  102. package/src/WorkflowTestRunner.js +0 -402
@@ -0,0 +1,293 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The rows a library cannot answer alone: whether the SSOT knows it, whether its
5
+ * declared category survives contact with the dependency graph, whether its pins
6
+ * are the versions the SSOT declares, whether anybody uses it at all, and whether
7
+ * a tooling package has stayed out of the services.
8
+ *
9
+ * None of them can answer from the package root alone, and they split by WHAT
10
+ * they are about. `U-ORPHAN` and `U-MISMATCH` are about the whole set, so they
11
+ * are `scope: 'workspace'`. `L-PINS`, `L-CONSUMER` and `L-TOOLING` are facts of
12
+ * ONE package — its pins against the SSOT, who pins it, which service carries
13
+ * it — that reach the answer only through the workspace, so they are
14
+ * `scope: 'bearer'` and run once per package. Carried as `workspace` they had to
15
+ * read a null package root and return `[]`, and the whole-set run then said
16
+ * nothing about them at all.
17
+ * Inside a container none of these files is present, and a row that cannot look
18
+ * is reported NOT RUN and never as passing
19
+ * (`.claude/rules/automation-gates.md` §5). The same holds one level down: a
20
+ * row reading a directory this checkout does not carry declares it in
21
+ * `requiresSiblings`, and the runner reports NOT RUN before the check runs.
22
+ *
23
+ * @see api/docs/governance/confirmations/biz-service-manifest.md §11
24
+ */
25
+
26
+ const path = require('path');
27
+
28
+ const {
29
+ discoverBearers, resolveFromReference, resolveFromMap, expandPattern, rootOfPattern
30
+ } = require('../discovery');
31
+ const { resolveWorkspacePath } = require('../workspaceRoot');
32
+ const {
33
+ readJson, readPackage, whereOf, declaredCategory, appliesTo, scopedDeps, SCOPE
34
+ } = require('./libraryContext');
35
+
36
+ /** The section that decides the layer: what the package pulls into a runtime. */
37
+ const RUNTIME_SECTIONS = Object.freeze(['dependencies']);
38
+
39
+ /** The sections `scripts/ci/verify-manifest-pins.mjs` compares against the SSOT. */
40
+ const PINNED_SECTIONS = Object.freeze(['dependencies', 'devDependencies']);
41
+
42
+ /** Why a workspace-scoped row prints nothing instead of a verdict. */
43
+ const notRunBecause = (what) => `the workspace root is not reachable, so ${what} cannot be read`;
44
+
45
+ /**
46
+ * Every package the uniform's discovery pattern finds, indexed by the name its
47
+ * own `package.json` carries. That name — not the directory — is what the SSOT
48
+ * owns and what a dependency writes.
49
+ *
50
+ * @param {{ block: object, workspaceRoot: string }} params
51
+ * @returns {Map<string, {relativeDir: string, dir: string, category: string|null}>}
52
+ */
53
+ function indexBearers({ block, workspaceRoot }) {
54
+ const { bearers } = discoverBearers({ block, workspaceRoot });
55
+ const index = new Map();
56
+
57
+ for (const bearer of bearers) {
58
+ const json = readJson(resolveWorkspacePath(workspaceRoot, bearer.file));
59
+ if (json === null || typeof json.name !== 'string') continue;
60
+ index.set(json.name, {
61
+ relativeDir: bearer.relativeDir,
62
+ dir: bearer.dir,
63
+ category: declaredCategory(json)
64
+ });
65
+ }
66
+ return index;
67
+ }
68
+
69
+ const libraryOrphan = Object.freeze({
70
+ scope: 'workspace',
71
+ requires: Object.freeze([]),
72
+
73
+ requiresSiblings({ block }) {
74
+ return [rootOfPattern(block.pattern)];
75
+ },
76
+
77
+ describeNotRun({ block }) {
78
+ return notRunBecause(block && block.from ? block.from.path : 'the referenced SSOT');
79
+ },
80
+
81
+ run({ block, workspaceRoot }) {
82
+ const onDisk = indexBearers({ block, workspaceRoot });
83
+ const declared = new Set(resolveFromReference({ from: block.from, workspaceRoot }));
84
+ const findings = [];
85
+
86
+ for (const [name, bearer] of onDisk) {
87
+ if (declared.has(name)) continue;
88
+ findings.push({
89
+ where: bearer.relativeDir,
90
+ what: `${name} is not declared in ${block.from.path} (${block.from.list}) — nothing pins it and nothing publishes it`
91
+ });
92
+ }
93
+
94
+ for (const name of declared) {
95
+ if (onDisk.has(name)) continue;
96
+ findings.push({
97
+ where: block.from.path,
98
+ what: `${name} is declared with a version but no package matches ${block.pattern}`
99
+ });
100
+ }
101
+
102
+ return findings;
103
+ }
104
+ });
105
+
106
+ const libraryCategory = Object.freeze({
107
+ scope: 'workspace',
108
+ requires: Object.freeze([]),
109
+
110
+ requiresSiblings({ block }) {
111
+ return [rootOfPattern(block.pattern)];
112
+ },
113
+
114
+ describeNotRun() {
115
+ return notRunBecause('the categories of the packages this one depends on');
116
+ },
117
+
118
+ run({ block, serviceRoot, workspaceRoot }) {
119
+ const { json } = readPackage(serviceRoot);
120
+ if (json === null) return [];
121
+ const where = whereOf({ scope: 'workspace', serviceRoot, workspaceRoot, relative: 'package.json' });
122
+
123
+ const declared = declaredCategory(json);
124
+ if (declared === null) {
125
+ return [{
126
+ where,
127
+ what: 'declares no "oa"."category" — the layer a package is meant to sit in cannot be derived from anything else'
128
+ }];
129
+ }
130
+
131
+ const definition = block.categories[declared];
132
+ if (definition === undefined) {
133
+ return [{
134
+ where,
135
+ what: `declares category "${declared}", which this uniform does not define `
136
+ + `(defined: ${Object.keys(block.categories).join(', ')})`
137
+ }];
138
+ }
139
+
140
+ const index = indexBearers({ block, workspaceRoot });
141
+ const allowed = new Set(definition.may_depend_on);
142
+
143
+ return scopedDeps(json, RUNTIME_SECTIONS).map(({ name }) => {
144
+ const dependency = index.get(name);
145
+ if (dependency === undefined) {
146
+ return {
147
+ where,
148
+ what: `depends on ${name}, which no package under ${block.pattern} provides — its category cannot be read`
149
+ };
150
+ }
151
+ if (dependency.category === null) {
152
+ return {
153
+ where,
154
+ what: `depends on ${name}, which declares no category — the layer of "${declared}" cannot be verified`
155
+ };
156
+ }
157
+ if (!allowed.has(dependency.category)) {
158
+ return {
159
+ where,
160
+ what: `declares category "${declared}" but depends on ${name}, which is "${dependency.category}" `
161
+ + `(may_depend_on: ${definition.may_depend_on.join(', ') || 'nothing'})`
162
+ };
163
+ }
164
+ return null;
165
+ }).filter((finding) => finding !== null);
166
+ }
167
+ });
168
+
169
+ const libraryPins = Object.freeze({
170
+ scope: 'bearer',
171
+ requires: Object.freeze(['from']),
172
+
173
+ describeNotRun({ row }) {
174
+ return notRunBecause(row.from.path);
175
+ },
176
+
177
+ run({ row, block, serviceRoot, workspaceRoot }) {
178
+ const { json } = readPackage(serviceRoot);
179
+ if (json === null || !appliesTo(block, json)) return [];
180
+
181
+ const ssot = resolveFromMap({ from: row.from, workspaceRoot });
182
+ const where = whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: 'package.json' });
183
+
184
+ return scopedDeps(json, PINNED_SECTIONS)
185
+ .map(({ name, spec, section }) => {
186
+ const owned = ssot[name];
187
+ if (owned === undefined) {
188
+ return { where, what: `${section} pins ${name}, which ${row.from.path} does not declare` };
189
+ }
190
+ if (spec !== owned) {
191
+ return { where, what: `${section} pins ${name} at "${spec}" but ${row.from.path} declares ${owned}` };
192
+ }
193
+ return null;
194
+ })
195
+ .filter((finding) => finding !== null);
196
+ }
197
+ });
198
+
199
+ const libraryConsumer = Object.freeze({
200
+ scope: 'bearer',
201
+ requires: Object.freeze(['consumer_patterns']),
202
+
203
+ // The row already says where the consumers live; the roots it needs are read
204
+ // off that, never restated. In a checkout carrying `api/` alone, `api_biz` is
205
+ // absent and "pinned by nothing" would be a lie about a file nobody opened.
206
+ requiresSiblings({ row }) {
207
+ return row.consumer_patterns.map(rootOfPattern);
208
+ },
209
+
210
+ describeNotRun({ row }) {
211
+ return notRunBecause(`the consumers under ${row.consumer_patterns.join(', ')}`);
212
+ },
213
+
214
+ run({ row, block, serviceRoot, workspaceRoot }) {
215
+ const { json, file } = readPackage(serviceRoot);
216
+ if (json === null || !appliesTo(block, json)) return [];
217
+
218
+ const own = path.resolve(file);
219
+ const consumers = [];
220
+
221
+ for (const pattern of row.consumer_patterns) {
222
+ for (const relative of expandPattern(workspaceRoot, pattern)) {
223
+ const absolute = resolveWorkspacePath(workspaceRoot, relative);
224
+ if (path.resolve(absolute) === own) continue;
225
+ const consumer = readJson(absolute);
226
+ if (consumer === null) continue;
227
+ const declared = { ...(consumer.dependencies || {}), ...(consumer.devDependencies || {}) };
228
+ if (Object.prototype.hasOwnProperty.call(declared, json.name)) consumers.push(relative);
229
+ }
230
+ }
231
+
232
+ if (consumers.length > 0) return [];
233
+ return [{
234
+ where: whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: 'package.json' }),
235
+ what: `${json.name} is pinned by nothing under ${row.consumer_patterns.join(', ')} — it is unwired or dead`
236
+ }];
237
+ }
238
+ });
239
+
240
+ const libraryTooling = Object.freeze({
241
+ scope: 'bearer',
242
+ requires: Object.freeze(['service_pattern']),
243
+
244
+ requiresSiblings({ row }) {
245
+ return [rootOfPattern(row.service_pattern)];
246
+ },
247
+
248
+ describeNotRun({ row }) {
249
+ return notRunBecause(`the services under ${row.service_pattern}`);
250
+ },
251
+
252
+ run({ row, block, serviceRoot, workspaceRoot }) {
253
+ const { json } = readPackage(serviceRoot);
254
+ if (json === null || !appliesTo(block, json)) return [];
255
+
256
+ const where = whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: 'package.json' });
257
+ const findings = [];
258
+
259
+ const bin = json.bin;
260
+ const hasBin = typeof bin === 'string' ? bin.length > 0 : (bin !== null && typeof bin === 'object' && Object.keys(bin).length > 0);
261
+ if (!hasBin) {
262
+ findings.push({ where, what: 'declares no "bin" — tooling is reached by a command, not by an import' });
263
+ }
264
+
265
+ for (const relative of expandPattern(workspaceRoot, row.service_pattern)) {
266
+ const service = readJson(resolveWorkspacePath(workspaceRoot, relative));
267
+ if (service === null) continue;
268
+ if (!Object.prototype.hasOwnProperty.call(service.dependencies || {}, json.name)) continue;
269
+ // The finding is reported at the LIBRARY, not at the service: it is the
270
+ // library's duty and the library's table, and a finding placed under a
271
+ // neighbour's root is one a service-mode run would drop entirely
272
+ // (`runManifest.js` § The two modes).
273
+ findings.push({
274
+ where,
275
+ what: `${relative} carries ${json.name} in dependencies — tooling never runs inside a service`
276
+ });
277
+ }
278
+
279
+ return findings;
280
+ }
281
+ });
282
+
283
+ module.exports = {
284
+ checks: [
285
+ { name: 'library-orphan', check: libraryOrphan },
286
+ { name: 'library-category', check: libraryCategory },
287
+ { name: 'library-pins', check: libraryPins },
288
+ { name: 'library-consumer', check: libraryConsumer },
289
+ { name: 'library-tooling', check: libraryTooling }
290
+ ],
291
+ indexBearers,
292
+ SCOPE
293
+ };
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The `Uniform:` region of a README, as a ROW rather than only as a `--check`.
5
+ *
6
+ * Owner decision 2026-09-09 (answering the kontrolor's d.215c note): that region
7
+ * is of the class `generated` — confirmation `biz-service-manifest` 001 §2 makes
8
+ * such a class a GENERATOR plus a row carrying the `--check` as its gate, the
9
+ * precedent being `G-SHARED-ENV`. Until this module the generator existed and
10
+ * the gate did not, so a README whose pointer had gone stale was reported by
11
+ * nothing anybody runs on a repository — the false guarantee of
12
+ * `.claude/rules/automation-gates.md` §5.
13
+ *
14
+ * ONE IMPLEMENTATION, TWO READERS. Nothing here decides what the region says:
15
+ * it renders through `readmePointer.js` and compares through the very
16
+ * `checkUniformRegion` the `--check` run calls, so the row and the command
17
+ * cannot disagree about the same file (`.claude/rules/change-discipline.md`
18
+ * § One rail per concern). Where the region's link points is
19
+ * `readmeLocation.js`, for the same reason.
20
+ *
21
+ * WHAT THE FINDING SAYS. The `--check` run prints the whole region twice, as a
22
+ * `-`/`+` block; a row's `what` is a table cell, so it names the first line that
23
+ * differs and what the file says there — the locator shape `F-INIT` and the sync
24
+ * already use. The full diff is one command away, and that command is the row's
25
+ * `fix`.
26
+ *
27
+ * @see api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-005
28
+ */
29
+
30
+ const { KINDS, extractRegion, checkUniformRegion } = require('../../sync/readmePointer');
31
+ const {
32
+ readReadme, serviceRegion, libraryRegion, PACKAGE_IN_WORKSPACE
33
+ } = require('../../sync/readmeLocation');
34
+ const { whereOf, declaredCategory, readPackage } = require('./libraryContext');
35
+
36
+ /**
37
+ * The first line at which the region on disk stops being the rendered one, and
38
+ * what the file says there.
39
+ *
40
+ * @param {string} expected the rendered region
41
+ * @param {string} actual the region on disk
42
+ * @returns {string}
43
+ */
44
+ function firstDifference(expected, actual) {
45
+ const want = expected.split('\n');
46
+ const have = actual.split('\n');
47
+ const length = Math.max(want.length, have.length);
48
+ for (let index = 0; index < length; index += 1) {
49
+ if (want[index] === have[index]) continue;
50
+ return `at region line ${index + 1}: ${JSON.stringify(have[index] === undefined ? '<end of region>' : have[index].trim())}`;
51
+ }
52
+ return 'nowhere — the region matches line for line';
53
+ }
54
+
55
+ /**
56
+ * One README measured against the region its manifest renders.
57
+ *
58
+ * @param {{ kind: string, dir: string, region: string, where: string, path: string }} params
59
+ * @returns {Array<{where: string, what: string}>}
60
+ */
61
+ function compare({ kind, dir, region, where, path: relative }) {
62
+ const text = readReadme(dir);
63
+ if (text === null) {
64
+ return [{ where, what: `absent — the uniform pointer is a generated region of ${relative}` }];
65
+ }
66
+
67
+ const result = checkUniformRegion(text, region, { kind });
68
+ if (result.ok) return [];
69
+
70
+ const actual = extractRegion(text, { kind });
71
+ if (actual === null) {
72
+ return [{
73
+ where,
74
+ what: 'carries no generated uniform region — which uniform a repository wears is rendered from the '
75
+ + 'manifest, never typed and never absent'
76
+ }];
77
+ }
78
+ return [{ where, what: `the generated uniform region is not what the manifest renders, ${firstDifference(region, actual)}` }];
79
+ }
80
+
81
+ const readmeUniformCurrent = Object.freeze({
82
+ scope: 'service',
83
+ requires: Object.freeze(['path', 'from']),
84
+
85
+ run({ row, serviceRoot }) {
86
+ return compare({
87
+ kind: KINDS.service,
88
+ dir: serviceRoot,
89
+ path: row.path,
90
+ where: row.path,
91
+ region: serviceRegion(serviceRoot)
92
+ });
93
+ }
94
+ });
95
+
96
+ const libraryReadmeRegion = Object.freeze({
97
+ scope: 'bearer',
98
+ requires: Object.freeze(['path']),
99
+
100
+ // The link this region carries points at the library manifest as it lies in
101
+ // the checkout being read, so the row can only answer where that checkout
102
+ // holds a copy of this package.
103
+ requiresSiblings: () => (PACKAGE_IN_WORKSPACE === null ? [] : [PACKAGE_IN_WORKSPACE]),
104
+
105
+ describeNotRun() {
106
+ return 'the workspace root is not reachable, so the manifest the region links at cannot be located';
107
+ },
108
+
109
+ run({ row, serviceRoot, workspaceRoot }) {
110
+ const { json } = readPackage(serviceRoot);
111
+
112
+ // A package declaring no category is `U-MISMATCH`, blocking, with its own
113
+ // fix — and the region cannot be rendered without one, because the duty
114
+ // sections it lists ARE the category's. Saying so twice would give one
115
+ // defect two owners; this is the same silence `oa-sync-template
116
+ // readme-uniform --all` keeps, and for the same reason.
117
+ if (json === null || declaredCategory(json) === null) return [];
118
+
119
+ return compare({
120
+ kind: KINDS.library,
121
+ dir: serviceRoot,
122
+ path: row.path,
123
+ where: whereOf({ scope: 'bearer', serviceRoot, workspaceRoot, relative: row.path }),
124
+ region: libraryRegion({ packageDir: serviceRoot, pkg: json, workspaceRoot })
125
+ });
126
+ }
127
+ });
128
+
129
+ module.exports = {
130
+ checks: [
131
+ { name: 'readme-uniform-current', check: readmeUniformCurrent },
132
+ { name: 'library-readme-region', check: libraryReadmeRegion }
133
+ ],
134
+ firstDifference
135
+ };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `S-SCRIPTS` — what a reader can learn about a script before running it.
5
+ *
6
+ * Confirmation `biz-service-manifest` 003 §19 makes the repository's own
7
+ * `scripts/` a duty of the uniform, checked by the lint "shipped in the
8
+ * validator package". That last clause is the whole design: the rules used to
9
+ * be a file of the `api` checkout, and a service has no such directory in its
10
+ * own CI and none at all inside its image, so the row could only ever have been
11
+ * NOT RUN where it is meant to answer. `src/lint/scripts/lintScripts.js` holds
12
+ * them now; this module is the bridge from the row to that module and decides
13
+ * nothing itself — the same shape `contractBridge.js` and `docsLintBridge.js`
14
+ * keep (004 point 4: a rule that exists is cited, never restated).
15
+ *
16
+ * ONE THING NEEDS THE WORKSPACE, AND ONLY WHEN IT IS USED. The header rules
17
+ * read the repository alone, so the row is `scope: service` and answers in a
18
+ * container. The exception is `S006`: a script may cite `api/docs/…`, and that
19
+ * path names the api checkout beside this repository, not a `docs/` of its own.
20
+ * With the workspace reachable the citation is resolved there; without it the
21
+ * lint returns the target as UNRESOLVED and this module reports the row NOT RUN
22
+ * with the targets named — a run that could not look must not report a pass
23
+ * (`.claude/rules/automation-gates.md` §5). A repository citing no such path is
24
+ * decided in full wherever it lies, which is the common case: measured
25
+ * 2026-09-10, 4 of the 47 `@see` lines under the eight services open with the
26
+ * prefix.
27
+ *
28
+ * @see api/docs/standards/SCRIPTS-STANDARD.md
29
+ * @see api/docs/governance/confirmations/biz-service-manifest.md
30
+ */
31
+
32
+ const fs = require('fs');
33
+
34
+ const { lintScripts, SCRIPT_SCOPE } = require('../../lint/scripts/lintScripts');
35
+ const { resolveWorkspacePath, WORKSPACE_MARKER } = require('../workspaceRoot');
36
+
37
+ /**
38
+ * The api checkout, by the name the workspace marker already spells. Written
39
+ * once, from the marker, so "which directory is the api one" has one owner
40
+ * (`api/.claude/rules/single-source-of-truth.md`).
41
+ */
42
+ const API_CHECKOUT = WORKSPACE_MARKER.split('/')[0];
43
+
44
+ /**
45
+ * Where the citations of one repository can be resolved, or null.
46
+ *
47
+ * @param {string|null} workspaceRoot
48
+ * @returns {string|null}
49
+ */
50
+ function apiCheckout(workspaceRoot) {
51
+ if (workspaceRoot === null || workspaceRoot === undefined) return null;
52
+ const target = resolveWorkspacePath(workspaceRoot, API_CHECKOUT);
53
+ return fs.existsSync(target) && fs.statSync(target).isDirectory() ? target : null;
54
+ }
55
+
56
+ const scriptsLint = Object.freeze({
57
+ scope: 'service',
58
+ requires: Object.freeze(['path']),
59
+
60
+ run({ serviceRoot, workspaceRoot }) {
61
+ const result = lintScripts({ root: serviceRoot, apiRoot: apiCheckout(workspaceRoot) });
62
+
63
+ const findings = result.findings.map((finding) => ({
64
+ where: `${finding.file}:${finding.line}`,
65
+ what: `${finding.id} — ${finding.message}`
66
+ }));
67
+
68
+ if (result.unresolved.length === 0) return findings;
69
+
70
+ return {
71
+ findings,
72
+ notRun: `${result.unresolved.length} @see target(s) name the ${API_CHECKOUT} checkout, which this run `
73
+ + `cannot reach: ${result.unresolved.join(', ')}. Run the uniform from a workspace that carries `
74
+ + `${API_CHECKOUT}/.`
75
+ };
76
+ }
77
+ });
78
+
79
+ module.exports = { checks: [{ name: 'scripts-lint', check: scriptsLint }], SCRIPT_SCOPE, API_CHECKOUT };