@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,658 @@
1
+ {
2
+ "uniform": "biz-service",
3
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §2",
4
+ "owner": "BIZ-general",
5
+ "blocking_severities": [
6
+ "boot",
7
+ "deploy"
8
+ ],
9
+ "verdict": {
10
+ "blocked": "NOT DEPLOYABLE",
11
+ "clear": "DEPLOYABLE",
12
+ "incomplete": "INCOMPLETE: {count} deploy row(s) not run ({ids}). Fix: run the uniform from the api checkout — npx oa-validate --workspace <workspace root>"
13
+ },
14
+ "discovery": {
15
+ "biz-service": {
16
+ "concern": "which directories wear this uniform, and who says they exist",
17
+ "pattern": "api_biz/*/package.json",
18
+ "from": {
19
+ "path": "api/config/services.json",
20
+ "list": "businessServices.services",
21
+ "key": "directory"
22
+ },
23
+ "rows": [
24
+ {
25
+ "id": "U-ORPHAN",
26
+ "check": "discovery-orphan",
27
+ "severity": "deploy",
28
+ "owner": "BIZ-general",
29
+ "why": "completeness is discovery: a directory no SSOT knows, or one that matches no pattern, wears no uniform and nothing checks it",
30
+ "fix": "declare the directory in api/config/services.json (businessServices.services[].directory), or remove it from api_biz/",
31
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-005"
32
+ },
33
+ {
34
+ "id": "U-IDENTITY",
35
+ "check": "ssot-identity",
36
+ "severity": "deploy",
37
+ "owner": "BIZ-general",
38
+ "why": "one service, one name: naming.md derives every spelling from a single short name, and the gateway compares service_code from the entitlement data against the service name in a cookbook step character by character - a second spelling is not untidiness, it is a request that is refused; directory, repo and container are the three spellings the SSOT owns, and this row reads the same reference U-ORPHAN does - the SSOT knows the directory, this asks whether it knows the service",
39
+ "fix": "make directory, repo and container in api/config/services.json read <shortname>, .../biz-<shortname> and api_service_<shortname>, where <shortname> is what the repository's config/service/config.json declares - or correct service.name if the SSOT is right",
40
+ "doc": "api/docs/biz/60-templates/naming.md"
41
+ }
42
+ ]
43
+ }
44
+ },
45
+ "files": {
46
+ "identical": [
47
+ {
48
+ "id": "F-INIT",
49
+ "check": "template-block",
50
+ "path": "init.sh",
51
+ "block": "oa-deps-guard v1",
52
+ "from": { "package": "templates/business-service/init.sh", "text": true },
53
+ "severity": "deploy",
54
+ "owner": "BIZ-general",
55
+ "why": "one install decision for the whole platform; four versions of this block were measured across the repositories, and a copy is kept in sync by review only — this row is what api/tests/scripts/infra-init-scripts.bats already does for api/infra, reaching the biz repositories",
56
+ "fix": "npx oa-sync-template init.sh --target . — it splices the block and leaves this file's own oa_npm_install() untouched",
57
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §2"
58
+ },
59
+ {
60
+ "id": "F-JEST",
61
+ "check": "template-file",
62
+ "path": "jest.config.js",
63
+ "from": { "package": "templates/business-service/jest.config.js", "text": true },
64
+ "severity": "deploy",
65
+ "owner": "BIZ-general",
66
+ "why": "how a suite is run is a platform decision, not a per-service one; eight of nine copies differed on 2026-09-09",
67
+ "fix": "npx oa-sync-template jest.config.js --target .",
68
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §2"
69
+ },
70
+ {
71
+ "id": "F-RUNNER",
72
+ "check": "compose-runner",
73
+ "path": "docker-compose.yml",
74
+ "block": "oa-test-runner v1",
75
+ "runner_memory": "1g",
76
+ "from": { "package": "templates/business-service/docker-compose.yml", "text": true },
77
+ "severity": "deploy",
78
+ "owner": "BIZ-general",
79
+ "why": "suites run in the service's own one-shot runner, which shares its build, env and networks but never its memory budget — two loads in one cgroup killed the service mid-test; what it shares is compared against the service beside it, everything else against the template block",
80
+ "fix": "npx oa-sync-template docker-compose.yml --target . — it renders the block with this repository's build, env_file and networks, and inserts it under services: when the file carries none yet",
81
+ "doc": "api/docs/governance/confirmations/biz-test-container.md"
82
+ }
83
+ ],
84
+ "contains": [
85
+ {
86
+ "id": "F-GITIGNORE",
87
+ "check": "template-entries",
88
+ "path": ".gitignore",
89
+ "from": { "package": "templates/business-service/gitignore", "text": true },
90
+ "severity": "deploy",
91
+ "owner": "BIZ-general",
92
+ "why": "every path this platform WRITES into a repository is a path git must not see, and the measured one is ci/deployability.json - oa-validate and the pre-push hook of confirmation 006 both write it, and in four of the eight services (converter, invoicing, meta, property) every run left it untracked while the template has carried the line since d.212. The class is contains and not identical because a repository ignores its own scratch directories too, and nine different files were measured across the eight; what has to be true here is a SET of paths, in any order, with every other line of the file left alone. A narrower pattern does not satisfy the entry it narrows: config/env-active/*.env leaves every non-.env file in a directory of live secrets committable",
93
+ "fix": "npx oa-sync-template .gitignore --target . - it appends the missing entries under one labelled block and rewrites nothing this repository wrote",
94
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a72"
95
+ },
96
+ {
97
+ "id": "F-DOCKERIGNORE",
98
+ "check": "dockerignore-entries",
99
+ "path": ".dockerignore",
100
+ "from": { "package": "templates/business-service/gitignore", "text": true },
101
+ "severity": "deploy",
102
+ "owner": "BIZ-general",
103
+ "why": "Dockerfile line COPY . . takes everything .dockerignore does not exclude, and being ignored by git excludes nothing: measured by BIZ-hello 2026-09-11, a LOCAL production image of hello came to 6.6 GB - 5.2 GB of it logs/ - and carried config/env-active/*.env with DB_PASSWORD, JWT_SECRET, the MinIO keys and RABBITMQ_URL. A CI image is clean by accident, because a fresh checkout has no ignored file to copy, so the defect is invisible exactly where the image is built. The entries are DERIVED from the declaration .gitignore already reads rather than written a second time (change-discipline.md \u00a7 One rail per concern), and translated: a slash-less .gitignore pattern matches at every depth while a .dockerignore one is matched from the context root, so *.log there would catch debug.log and not logs/debug.log. The criterion is a comparison and never a list of directories - the files in /app of an image built from the working tree, node_modules aside, are the files of an image built from git archive HEAD",
104
+ "fix": "npx oa-sync-template .dockerignore --target . - it writes the derived entries under one labelled block and rewrites nothing this repository wrote",
105
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a72"
106
+ }
107
+ ],
108
+ "own": {
109
+ "concern": "the files of the template whose CONTENT the service decides, and the reason each is not held to the platform's copy",
110
+ "why": "a file added to the template is a decision about who guards it, and this class is the decision written down: no row holds these to the template, because their content IS the service. Dockerfile is the exception that proves the rule - its one platform fact, the node major, is R-NODE's row, and everything else in it is this service's build. docs/README.md is the same decision for the map of the documentation tree: which branches a service has is the service's, and what the platform holds the tree to is not that prose but D-LINT, which asks the documentation lint whether the tree is clean. It is the map alone, not docs/ - the three installation documents under docs/80-setup/ ARE held to a skeleton, and a class covering them would exempt what a row requires. The class exists so that the completeness check of finding 18 has an answer other than silence for every file the template carries (automation-gates.md \u00a75)",
111
+ "allowed": [
112
+ "Dockerfile",
113
+ "index.js",
114
+ "package.json",
115
+ "src/",
116
+ "tests/",
117
+ "docs/README.md"
118
+ ],
119
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a72"
120
+ },
121
+ "generated": [
122
+ {
123
+ "id": "G-PROD",
124
+ "check": "template-render",
125
+ "path": "docker-compose.production.yml",
126
+ "from": { "package": "templates/business-service/docker-compose.production.yml", "text": true },
127
+ "severity": "deploy",
128
+ "owner": "BIZ-general",
129
+ "why": "one truth for the production topology: which networks the biz box has, which env files exist on it and what the image is called are facts of the platform, not of a service, so the file is RENDERED from the template and the service's name — measured 2026-09-10, the seven old-shape copies were byte-identical once the name was normalised, and adding or removing a service is then a scaffold plus an SSOT entry rather than a hand-written file",
130
+ "fix": "npx oa-sync-template docker-compose.production.yml --target .",
131
+ "doc": "api/docs/governance/confirmations/server-topology.md § Confirmation 20260910-server-topology-005"
132
+ },
133
+ {
134
+ "id": "G-CI",
135
+ "check": "delimited-block-render",
136
+ "path": ".gitlab-ci.yml",
137
+ "block": "oa-ci v1",
138
+ "from": { "package": "templates/business-service/.gitlab-ci.yml", "text": true },
139
+ "severity": "deploy",
140
+ "owner": "BIZ-general",
141
+ "why": "the platform half of a biz pipeline belongs to the platform, not to the service: it builds the same way, identifies the image by the FULL commit sha (R5), hands the deploy the digest CI itself pushed (R1/R2), and since confirmation 008 it runs the uniform gate before the SSH step - a service that edits any of that edits the gate that judges it. It is a BLOCK and not the whole file because the other half is genuinely the service's. Measured over the eight repositories on 2026-09-11: the template is 151 lines and 7 root keys, the services 176 to 198 lines and 9, and every difference is theirs - the test job that stands up MariaDB, Redis and RabbitMQ as service containers, runs five ci:gate:* steps and publishes ci/integration-signal.json, plus workflow: and verify-installation-contract:. Which database and which gates an integration needs is a fact about the service. A whole-file fix would have deleted all of it and left eight integration tiers running against no database, which is a finding nobody can carry out (automation-gates.md §3, last bullet; controller finding 20)",
142
+ "fix": "npx oa-sync-template .gitlab-ci.yml --target .",
143
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a7 Confirmation 20260910-biz-service-manifest-008"
144
+ },
145
+ {
146
+ "id": "G-PROD-IMAGE",
147
+ "check": "deploy-contract",
148
+ "requirement": "R1",
149
+ "path": "docker-compose.production.yml",
150
+ "severity": "deploy",
151
+ "owner": "BIZ-general",
152
+ "why": "the production image is the one CI built and proved; a mutable tag makes the deploy nondeterministic and leaves a rollback no target",
153
+ "fix": "pin the image by digest: image: <repo>@${BIZ_IMAGE_DIGEST:?BIZ_IMAGE_DIGEST is required}",
154
+ "doc": "api/docs/operations/biz-rework-deployment-requirements.md"
155
+ },
156
+ {
157
+ "id": "G-README",
158
+ "check": "readme-uniform-current",
159
+ "path": "README.md",
160
+ "from": { "package": "manifests/biz-service.manifest.json", "text": true },
161
+ "severity": "deploy",
162
+ "owner": "BIZ-general",
163
+ "why": "which uniform a repository wears, and which of its paths fall under which duty, is discoverability that is RENDERED from this manifest and never typed (005 point 2); a generated region with a generator and no gate goes stale the day a row moves, and nothing anybody runs on the repository says so",
164
+ "fix": "npx oa-sync-template readme-uniform --target .",
165
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md § Confirmation 20260909-biz-service-manifest-005"
166
+ },
167
+ {
168
+ "id": "G-SETUP",
169
+ "check": "install-contract",
170
+ "requirement": "SETUP_DOCS",
171
+ "path": "docs/80-setup/",
172
+ "severity": "deploy",
173
+ "owner": "BIZ-general",
174
+ "why": "every repository carries the three installation documents an operator installs it from",
175
+ "fix": "add docs/80-setup/{INSTALL,PLATFORM_MATRIX,VALIDATION}.md; a repo on the older docs/setup/ branch renames it",
176
+ "doc": "api/docs/standards/repository-installation-sql-contract.md"
177
+ },
178
+ {
179
+ "id": "G-SETUP-INSTALL",
180
+ "check": "template-skeleton",
181
+ "path": "docs/80-setup/INSTALL.md",
182
+ "skeleton_only": true,
183
+ "from": { "package": "templates/business-service/docs/80-setup/INSTALL.md", "text": true },
184
+ "severity": "deploy",
185
+ "owner": "BIZ-general",
186
+ "why": "an operator installs from the sections, not from the prose: a document missing \"Environment preparation\" leaves them to guess at the step, and the prose of every section is the service's own",
187
+ "fix": "npx oa-sync-template docs/80-setup/INSTALL.md --target . creates the document from the skeleton; an existing one gets the missing section added by hand, because its prose is this service's",
188
+ "doc": "api/docs/standards/repository-installation-sql-contract.md"
189
+ },
190
+ {
191
+ "id": "G-SETUP-MATRIX",
192
+ "check": "template-skeleton",
193
+ "path": "docs/80-setup/PLATFORM_MATRIX.md",
194
+ "skeleton_only": true,
195
+ "from": { "package": "templates/business-service/docs/80-setup/PLATFORM_MATRIX.md", "text": true },
196
+ "severity": "deploy",
197
+ "owner": "BIZ-general",
198
+ "why": "the matrix answers one question per platform the service is installed on; a section that is absent reads as a platform nobody has anything to say about",
199
+ "fix": "npx oa-sync-template docs/80-setup/PLATFORM_MATRIX.md --target . creates the document from the skeleton; an existing one gets the missing section added by hand",
200
+ "doc": "api/docs/standards/repository-installation-sql-contract.md"
201
+ },
202
+ {
203
+ "id": "G-SETUP-VALIDATION",
204
+ "check": "template-skeleton",
205
+ "path": "docs/80-setup/VALIDATION.md",
206
+ "skeleton_only": true,
207
+ "from": { "package": "templates/business-service/docs/80-setup/VALIDATION.md", "text": true },
208
+ "severity": "deploy",
209
+ "owner": "BIZ-general",
210
+ "why": "the document that says what a finished install must show is the one the owner found instructing a curl on a listener no biz service opens; its sections are what keep the answer complete",
211
+ "fix": "npx oa-sync-template docs/80-setup/VALIDATION.md --target . creates the document from the skeleton; an existing one gets the missing section added by hand",
212
+ "doc": "api/docs/standards/repository-installation-sql-contract.md"
213
+ }
214
+ ],
215
+ "tracked": [
216
+ {
217
+ "id": "X-IGNORED",
218
+ "check": "git-tracked",
219
+ "severity": "deploy",
220
+ "owner": "BIZ-general",
221
+ "why": "every file row above reads the WORKING TREE, and what reaches a clone, a CI checkout and the production image is what git TRACKS - measured 2026-09-10 on a copy of converter outside the repository: init.sh and jest.config.js added to .gitignore and left on disk gave F-INIT and F-JEST zero findings, and the same two files deleted (the state a clean clone is in) gave both, NOT DEPLOYABLE. Developer green, CI red, which is the class shared-working-tree.md already records one layer up. The row reads no list of its own: the paths are the ones files.identical, files.contains and files.generated already name, a directory among them covering its whole tree (004 point 2). Measured 2026-09-11 over the eight services: zero violations, so the row lands with compliance rather than ahead of it (automation-gates.md \u00a73)",
222
+ "fix": "git add the file the finding names, and delete the .gitignore line covering it when the finding says one does",
223
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a72"
224
+ }
225
+ ],
226
+ "forbidden": [
227
+ {
228
+ "id": "X-HOOKS",
229
+ "check": "file-absent",
230
+ "path": "hooks/pre-commit",
231
+ "severity": "deploy",
232
+ "owner": "BIZ-general",
233
+ "why": "owner 2026-09-05: deleted, weaker duplicate of the pre-push gate",
234
+ "fix": "rm -f hooks/pre-commit",
235
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §5"
236
+ },
237
+ {
238
+ "id": "X-PREVAL",
239
+ "check": "file-absent",
240
+ "path": "scripts/run-pre-validation.js",
241
+ "severity": "deploy",
242
+ "owner": "BIZ-general",
243
+ "why": "replaced by the library command: oa-biz-ci-gate run-prevalidation",
244
+ "fix": "rm -f scripts/run-pre-validation.js",
245
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §5"
246
+ },
247
+ {
248
+ "id": "X-DB-CONFIG",
249
+ "check": "file-absent",
250
+ "path": "src/config/database.js",
251
+ "severity": "deploy",
252
+ "owner": "BIZ-general",
253
+ "why": "owner 2026-09-10 (confirmation 009): a private connection factory beside @onlineapps/conn-base-db, which is the one factory",
254
+ "fix": "replace the importers with createSequelize from @onlineapps/conn-base-db and delete the file: rm -f src/config/database.js",
255
+ "doc": "api/docs/biz/70-contracts/database-contract.md \u00a710"
256
+ }
257
+ ]
258
+ },
259
+ "scripts": {
260
+ "concern": "how a suite is run, and what may never run without being named",
261
+ "required": [
262
+ {
263
+ "id": "S-TEST",
264
+ "check": "script-body",
265
+ "name": "test",
266
+ "body": "jest --config jest.config.js tests/unit --runInBand",
267
+ "severity": "deploy",
268
+ "owner": "BIZ-general",
269
+ "why": "one entry point to the unit suite, with the config and the worker count the platform runs it with",
270
+ "fix": "set scripts.test to the body this row names",
271
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §2"
272
+ },
273
+ {
274
+ "id": "S-ALL-C",
275
+ "check": "script-body",
276
+ "name": "test:all:container",
277
+ "body": "docker compose run --rm ${runner} npm run test:all",
278
+ "severity": "deploy",
279
+ "owner": "BIZ-general",
280
+ "why": "the runner is the only rail a suite runs on; ${runner} is the compose service carrying profiles: [test], which is the one name that is per-repository",
281
+ "fix": "add scripts[\"test:all:container\"] wrapping the runner this repository declares",
282
+ "doc": "api/docs/governance/confirmations/biz-test-container.md"
283
+ },
284
+ {
285
+ "id": "S-INT-C",
286
+ "check": "script-body",
287
+ "name": "test:integration:container",
288
+ "body": "docker compose run --rm ${runner} npm run test:integration",
289
+ "only_with": "tests/integration",
290
+ "severity": "deploy",
291
+ "owner": "BIZ-general",
292
+ "why": "the same rail for the integration suite, and only where there is one: a script pointing at an absent suite is a declaration with nothing behind it, and jest exits 1 on a path that matches no test",
293
+ "fix": "add scripts[\"test:integration:container\"] wrapping the runner this repository declares",
294
+ "doc": "api/docs/governance/confirmations/biz-test-container.md"
295
+ },
296
+ {
297
+ "id": "S-COOKBOOKS",
298
+ "check": "script-body",
299
+ "name": "test:cookbooks",
300
+ "body": "node node_modules/@onlineapps/conn-orch-validator/src/cli/biz-ci-gate.js run-prevalidation",
301
+ "bin": "oa-biz-ci-gate",
302
+ "severity": "deploy",
303
+ "owner": "BIZ-general",
304
+ "why": "the cookbook run is the library's command; a per-service copy of it drifts from the library that owns the dispatch",
305
+ "fix": "set scripts[\"test:cookbooks\"] to the library command and delete the per-service script",
306
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §5"
307
+ }
308
+ ],
309
+ "forbidden": [
310
+ {
311
+ "id": "S-HOST",
312
+ "check": "script-forbidden",
313
+ "name": "test:host",
314
+ "severity": "deploy",
315
+ "owner": "BIZ-general",
316
+ "why": "owner biz-test-container 001: the runner is the only rail",
317
+ "fix": "remove scripts[\"test:host\"]; run the suite through test:all:container",
318
+ "doc": "api/docs/governance/confirmations/biz-test-container.md"
319
+ },
320
+ {
321
+ "id": "S-HOOKS",
322
+ "check": "script-implicit",
323
+ "severity": "deploy",
324
+ "owner": "BIZ-general",
325
+ "why": "a script that fires as a side effect of another command is invisible in the log that matters, and one whose target does not exist fires on nothing at all",
326
+ "fix": "delete the hook and call the script by name where it belongs",
327
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md §2"
328
+ }
329
+ ]
330
+ },
331
+ "tooling": {
332
+ "concern": "the repository's own scripts/ \u2014 what a reader can learn about a script before running it",
333
+ "rows": [
334
+ {
335
+ "id": "S-SCRIPTS",
336
+ "check": "scripts-lint",
337
+ "path": "scripts/",
338
+ "severity": "deploy",
339
+ "owner": "BIZ-general",
340
+ "why": "a script nobody can understand is the failure this platform actually has: the five header fields say what it owns, how it is called, what it runs on and what its exit codes mean, and the two closed vocabularies keep a parseable-but-foreign value from reading as an answer \u2014 measured 2026-09-10 over the eight services, 86 scripts carry no header at all, and the rules had until then lived in a file of the api checkout, which a service image does not have",
341
+ "fix": "write the five header fields the finding names, in the order SCRIPTS-STANDARD \u00a71 gives them, immediately after the shebang; the lint's own message says which field it expected where",
342
+ "doc": "api/docs/standards/SCRIPTS-STANDARD.md"
343
+ }
344
+ ]
345
+ },
346
+ "config": {
347
+ "concern": "the files a service is configured by, and the declarations they must carry",
348
+ "applies_to": "*",
349
+ "required": [
350
+ {
351
+ "id": "C-IDENTITY",
352
+ "check": "service-identity",
353
+ "path": "docker-compose.yml",
354
+ "production_path": "docker-compose.production.yml",
355
+ "severity": "deploy",
356
+ "owner": "BIZ-general",
357
+ "why": "one service, one name: naming.md derives every spelling from a single short name, and the gateway compares service_code from the entitlement data against the service name in a cookbook step character by character - a second spelling is not untidiness, it is a request that is refused; measured 2026-09-10, one of the eight services wears four names and nothing said a word - this row holds every spelling INSIDE the repository (npm name, container_name in both composes, the env template and the env_file loading it, the database schema where one is declared) to the short name config/service/config.json declares",
358
+ "fix": "spell every one of them after the short name in config/service/config.json: npm name biz-<shortname>, container_name api_service_<shortname>, config/env-templates/<shortname>.env loaded as <shortname>.env, database.schema oagen_<shortname>",
359
+ "doc": "api/docs/biz/60-templates/naming.md"
360
+ },
361
+ {
362
+ "id": "C-SERVICE",
363
+ "check": "config-service",
364
+ "path": "config/service/config.json",
365
+ "severity": "boot",
366
+ "owner": "BIZ-general",
367
+ "why": "the three keys of this file a reader demands: service.name is the identity the platform knows the service by, service.workspaceScoped decides whether it must implement list-workspaces and the wrapper refuses to boot without an explicit boolean, service.specificationEndpoint is published in the service specification",
368
+ "fix": "declare service.name, service.workspaceScoped (an explicit true/false) and service.specificationEndpoint in config/service/config.json",
369
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a72"
370
+ },
371
+ {
372
+ "id": "C-OPS",
373
+ "check": "config-operations",
374
+ "path": "config/service/operations.json",
375
+ "severity": "boot",
376
+ "owner": "BIZ-general",
377
+ "why": "the v3 dispatch table; the per-operation rules (handler, bundle_scope, forbidden v2 fields) are step 4 of the boot validation, which this row does not repeat",
378
+ "fix": "declare the service's operations in config/service/operations.json",
379
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a72"
380
+ },
381
+ {
382
+ "id": "C-CONTRACT",
383
+ "check": "config-contract",
384
+ "path": "config/service/integration-contract.json",
385
+ "severity": "boot",
386
+ "owner": "BIZ-general",
387
+ "why": "the first file every platform gate reads: the connectors, the database, the env declaration and the integration minimum; the row does not restate its rules, it calls the validator that owns them (utils/bizCiGateContract.js), so the CI gate and this table can never disagree about the same document",
388
+ "fix": "add config/service/integration-contract.json, or repair the declaration the message names; the template in this package carries the shape",
389
+ "doc": "api/docs/biz/70-contracts/env-contract.md"
390
+ },
391
+ {
392
+ "id": "C-CONNECTORS",
393
+ "check": "connector-contract",
394
+ "severity": "deploy",
395
+ "owner": "BIZ-general",
396
+ "why": "a service declares its connections twice — config/service/config.json configures wrapper.cache / wrapper.state / wrapper.mq, config/service/integration-contract.json declares requiredConnectors — and the two describe one fact; while they disagree ci:gate:wait does not wait for a connector the service needs and the suite races it, which is how biz-converter shipped requiredConnectors.db=false while owning six migrations. The comparison has existed at boot since F12 and had no consequence there: the finding went into results.warnings, the service started, and no CI job ran it at all. This row is what acts on it (confirmation connector-contract-check 001); the boot keeps reporting and keeps starting. The environment half of that check — is REDIS_URL set — stays at boot, where an environment exists; this row asks only what a checkout can answer",
397
+ "fix": "make the two declarations agree: set requiredConnectors.<connector> to what the service really needs, or add the wrapper section that configures it / remove the one nothing requires",
398
+ "doc": "api/docs/governance/confirmations/connector-contract-check.md"
399
+ },
400
+ {
401
+ "id": "C-SERVICE-DEAD",
402
+ "check": "config-dead-keys",
403
+ "severity": "deploy",
404
+ "owner": "BIZ-general",
405
+ "why": "service.version (ConfigLoader overwrites it from package.json on every load), service.port (a biz service binds none), wrapper.registry.url (d.244: registration travels over MQ to registry.register and discovery reads the Redis projection, confirmation biz-discovery-redis 002), wrapper.tenantContext (its only consumer createTenantContextMiddleware and the runtime default were deleted in b5e36431, confirmation wrapper-tenant-middleware 001) and contractVersion have no reader anywhere in the platform; service.url joins them from wrapper 8.0.0 and not before \u2014 the published 7.0.0 still throws \"Missing configuration - service.url is required\", so that key travels WITH the pin (confirmation biz-service-port-url 001) and a rule claiming it has no reader today would send a service into a failed boot; a required-looking declaration nothing consumes tells the next author something depends on it, and every new service copies it forward",
406
+ "fix": "delete the key from the file that declares it",
407
+ "doc": "api/docs/governance/confirmations/biz-service-port-url.md"
408
+ },
409
+ {
410
+ "id": "C-ENV",
411
+ "check": "env-templates",
412
+ "path": "config/env-templates",
413
+ "severity": "boot",
414
+ "owner": "BIZ-general",
415
+ "why": "one platform template and one of the service's own, so a key has exactly one home; what the service's file is CALLED is the scaffold's rename, tested by api/tests/scripts/add-service.bats",
416
+ "fix": "keep config/env-templates/shared.env and exactly one <service>.env beside it",
417
+ "doc": "api/docs/biz/70-contracts/env-contract.md"
418
+ },
419
+ {
420
+ "id": "G-SHARED-ENV",
421
+ "check": "shared-env-generated",
422
+ "path": "config/env-templates/shared.env",
423
+ "from": { "package": "templates/business-service/config/env-templates/shared.env", "text": true },
424
+ "severity": "deploy",
425
+ "owner": "BIZ-general",
426
+ "why": "the shared key set is one fact with one owner; measured on 2026-09-09, none of the nine copies matched the platform file and one key existed in no copy at all. The owner is still api/config/shared-env.json: what this row reads is the render this package carries, kept equal to that manifest by the packaged template's own gate, so the row also answers inside a service container, where api/config/ is not there",
427
+ "fix": "npx oa-sync-template shared-env --target .",
428
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a718"
429
+ },
430
+ {
431
+ "id": "C-ENV-READS",
432
+ "check": "env-contract",
433
+ "severity": "boot",
434
+ "owner": "BIZ-general",
435
+ "why": "an environment name the code reads and nothing declares is a key that is missing on the day the service moves, and nobody can say what it was for",
436
+ "fix": "declare the name in config/service/integration-contract.json under env.required or env.optional, each with its why",
437
+ "doc": "api/docs/biz/70-contracts/env-contract.md"
438
+ }
439
+ ]
440
+ },
441
+ "runtime": {
442
+ "concern": "what the service is allowed to be at run time — the platform's numbers, not this service's preferences",
443
+ "rows": [
444
+ {
445
+ "id": "R-NODE",
446
+ "check": "node-major",
447
+ "from": { "package": "package.json", "text": true },
448
+ "severity": "deploy",
449
+ "owner": "BIZ-general",
450
+ "why": "R3 of the deploy contract compares CI, Dockerfile and engines.node against each other, so a service that is consistently on an older major is green there; this row is the comparison against the platform major, read from the engines.node of @onlineapps/conn-orch-validator itself - the library uniform's L-ENGINES holds that field equal to api/.nvmrc, so it is a checked projection of the SSOT and not a second copy of it, and it travels with the pin into the container and the service's own CI, where api/.nvmrc does not exist",
451
+ "fix": "build on the platform node major and declare the same range in engines.node - the number is api/.nvmrc, and node_modules/@onlineapps/conn-orch-validator/package.json carries it wherever the package is installed",
452
+ "doc": "api/docs/governance/confirmations/node-runtime-version.md"
453
+ },
454
+ {
455
+ "id": "R-MEM",
456
+ "check": "memory-limit",
457
+ "path": "docker-compose.yml",
458
+ "production_path": "docker-compose.production.yml",
459
+ "service_memory": "512M",
460
+ "severity": "deploy",
461
+ "owner": "BIZ-general",
462
+ "why": "the platform norm for a resident biz service, in dev and in production alike; the runner's own budget is F-RUNNER's row, because the split is the point",
463
+ "fix": "set deploy.resources.limits.memory to 512M in both compose files",
464
+ "doc": "api/docs/governance/confirmations/biz-memory-limits.md"
465
+ },
466
+ {
467
+ "id": "R-PID1",
468
+ "check": "compose-command",
469
+ "path": "docker-compose.yml",
470
+ "service_command": "node index.js",
471
+ "severity": "deploy",
472
+ "owner": "BIZ-general",
473
+ "why": "PID 1 decides whether a stop is a shutdown or a kill: with command: [\"npm\",\"start\"] the process the container signals is npm, which does not forward SIGTERM, so docker waits its ten seconds and SIGKILLs a service that never closed a channel. Confirmation biz-compose-pid1 001 settled the substance and 002 settled the delivery - it is not a per-repo choice but part of the uniform biz service shape, so it is a row here rather than a sentence eight threads have to remember. Measured 2026-09-14 over the eight dev composes: six say npm, two say node. The one-shot test runner is outside this row - it declares no command because docker compose run supplies one, and its shape is F-RUNNER's",
474
+ "fix": "in docker-compose.yml set the service block's command to [\"node\", \"index.js\"] (the entrypoint init.sh execs it, so node becomes PID 1), then recreate the container and smoke it",
475
+ "doc": "api/docs/governance/confirmations/biz-compose-pid1.md"
476
+ },
477
+ {
478
+ "id": "R-PORTS-DEV",
479
+ "check": "compose-no-ports",
480
+ "path": "docker-compose.yml",
481
+ "severity": "deploy",
482
+ "owner": "BIZ-general",
483
+ "why": "R4 refuses a published port in the production compose and never reads the dev one, where a scaffolded service used to publish by default",
484
+ "fix": "remove the ports block from docker-compose.yml",
485
+ "doc": "api/docs/biz/00-model/service-shape.md"
486
+ },
487
+ {
488
+ "id": "R-PORTS",
489
+ "check": "deploy-contract",
490
+ "requirement": "R4",
491
+ "path": "docker-compose.production.yml",
492
+ "severity": "deploy",
493
+ "owner": "BIZ-general",
494
+ "why": "docker publishes past the firewall, so a published port is reachable from the internet; the only public entrypoint is doorman",
495
+ "fix": "remove the ports block from docker-compose.production.yml",
496
+ "doc": "api/docs/operations/biz-rework-deployment-requirements.md"
497
+ }
498
+ ]
499
+ },
500
+ "db": {
501
+ "concern": "whether this service has a database, and whether the repository agrees with what it declared",
502
+ "rows": [
503
+ {
504
+ "id": "D-DB-CONSISTENT",
505
+ "check": "db-consistent",
506
+ "forbidden_packages": ["mysql2", "mariadb", "sequelize", "pg"],
507
+ "severity": "deploy",
508
+ "owner": "BIZ-general",
509
+ "why": "a contract that disagrees with the repository sends the installer the wrong way in both directions: nothing to install where a database was promised, and an unmanaged schema where none was declared",
510
+ "fix": "make requiredConnectors.db agree with the repository: add migrations/, or drop the declaration and the database client",
511
+ "doc": "api/docs/governance/confirmations/biz-service-manifest.md \u00a7 Confirmation 20260909-biz-service-manifest-005"
512
+ },
513
+ {
514
+ "id": "D-DB-COLLATION",
515
+ "check": "db-collation",
516
+ "path": "config/service/integration-contract.json",
517
+ "key": "collation",
518
+ "severity": "deploy",
519
+ "owner": "BIZ-general",
520
+ "why": "owner 2026-09-14: the collation a schema is created with is the service's own declaration, and the CI gate and the production runbook read the same key - without it each of them invents one, which is how six schemas came to carry the image default utf8mb4_general_ci while every table inside them declares utf8mb4_bin or utf8mb4_unicode_ci (measured 2026-09-14: 122 of 123 permanent tables declare their collation explicitly). The row asks only whether the key is THERE: whether the value is a collation name is the contract validator's rule, which C-CONTRACT calls, and refusing to build without it is the setup-db gate's - a row restating either would be the duplicate that already cost D-DB-DECL its place. It reads the database block rather than requiredConnectors.db, because the block is what the gate builds from",
521
+ "fix": "add \"collation\" to the \"database\" block of config/service/integration-contract.json - the collation this service's tables already declare, e.g. \"utf8mb4_bin\"; the CI gate then creates the schema with it and refuses one created with another",
522
+ "doc": "api/docs/biz/70-contracts/database-contract.md \u00a7 1"
523
+ },
524
+ {
525
+ "id": "D-DB-PACKAGE",
526
+ "check": "install-contract",
527
+ "requirement": "SQL_PACKAGE",
528
+ "path": "migrations/",
529
+ "severity": "deploy",
530
+ "owner": "BIZ-general",
531
+ "why": "a service with a database is installed from its own SQL package; an absent or empty one makes a fresh install undefined",
532
+ "fix": "create migrations/BASELINE and migrations/SEED/{production_like,test_only} with their SQL files",
533
+ "doc": "api/docs/standards/repository-installation-sql-contract.md"
534
+ },
535
+ {
536
+ "id": "D-DB-NAMING",
537
+ "check": "db-migration-naming",
538
+ "path": "migrations/",
539
+ "naming": "^\\d{3}_[a-z][a-z0-9]*(?:_[a-z0-9]+)*\\.sql$",
540
+ "severity": "deploy",
541
+ "owner": "BIZ-general",
542
+ "why": "what a migration is called is the only thing a reader has before opening it and the only thing the install order depends on. The shape is not this row's to invent: database-contract.md \u00a75 owns it, and the row reads that shape back - three digits, an underscore, a lowercase description, no second word demanded, which is why 000_baseline.sql is a name and not a finding (\u00a78 makes a regenerated baseline an applied migration). No leading verb is demanded either, because the measured counter-example is right - converter's 009_cnv_batch_drop_pending_status.sql opens with a table prefix; and the three digits are not decoration - 999a_ sorts after 999_ only by accident of the byte after the digits. The convention reaches migrations/*.sql and stops there: what BASELINE/ and SEED/** must carry is the installation contract's \u00a74, which D-DB-HEADERS already checks, and that contract names no shape for their file names at all",
543
+ "fix": "rename the file to the shape database-contract.md \u00a75 prescribes - three digits, an underscore, the subject in lowercase snake_case; keep the ordinal the package is already installed in order of",
544
+ "doc": "api/docs/biz/70-contracts/database-contract.md \u00a7 5"
545
+ },
546
+ {
547
+ "id": "D-DB-README",
548
+ "check": "db-readme",
549
+ "path": "migrations/README.md",
550
+ "severity": "deploy",
551
+ "owner": "BIZ-general",
552
+ "why": "the installation contract \u00a73 asks a repository with a database for four things, and D-DB-PACKAGE's SQL_PACKAGE requirement checks three of them - the BASELINE and the two SEED directories. The fourth is this document, the documented execution order that reproduces a working instance (\u00a75 point 4), and until this row nothing looked for it",
553
+ "fix": "add migrations/README.md saying in which order the package is run and what a finished install shows",
554
+ "doc": "api/docs/standards/repository-installation-sql-contract.md"
555
+ },
556
+ {
557
+ "id": "D-DB-ACCOUNT",
558
+ "check": "db-account",
559
+ "path": "config/env-templates",
560
+ "key": "DB_USER",
561
+ "severity": "deploy",
562
+ "owner": "BIZ-general",
563
+ "why": "owner 2026-08-28: every service reaches the database as its own account with grants on its own schemas, and root stops being an operational identity - it was the identity of nine containers across eight repositories, so rotating it was a cross-repo batch nobody could run. The name is the SAME derivation C-IDENTITY holds database.schema to, read from the same file with the same prefix, so the account and the schema cannot drift into two spellings of one service. A repository that declares a database and no DB_USER is a finding and not a silence: an account nobody declared is one somebody invents at install time",
564
+ "fix": "set DB_USER in this service's env template to oagen_<shortname>, where <shortname> is what config/service/config.json declares; the account and its grants are created by the operator installing the schema",
565
+ "doc": "api/docs/governance/confirmations/db-accounts-per-service.md"
566
+ },
567
+ {
568
+ "id": "D-DB-HEADERS",
569
+ "check": "install-contract",
570
+ "requirement": "SQL_HEADERS",
571
+ "path": "migrations/",
572
+ "severity": "deploy",
573
+ "owner": "BIZ-general",
574
+ "why": "test data that reads as production-safe is the one mistake that reaches a real database",
575
+ "fix": "add the Dataset-Class, Target-DB, Safe-For-Production and Idempotency headers to the SQL file the finding names",
576
+ "doc": "api/docs/standards/repository-installation-sql-contract.md"
577
+ }
578
+ ]
579
+ },
580
+ "docs": {
581
+ "concern": "what this service's own documentation tree may not say; every rule is the documentation lint's own, cited by id and never restated here",
582
+ "applies_to": "*",
583
+ "tree_config_row": "C-LINT",
584
+ "required": [
585
+ {
586
+ "id": "C-LINT",
587
+ "check": "file-present",
588
+ "path": "config/biz-docs-lint.tree.json",
589
+ "severity": "deploy",
590
+ "owner": "BIZ-general",
591
+ "why": "the documentation lint refuses to run over a tree that declares no budget of its own, so a tree without this file is linted by nothing; the rows below are invoked with the budget this row's path names, which is why the path is written once and read from here",
592
+ "fix": "add config/biz-docs-lint.tree.json; node_modules/@onlineapps/conn-orch-validator/templates/business-service/config/biz-docs-lint.tree.json carries the shape",
593
+ "doc": "api/docs/biz/DOC-STANDARD.md"
594
+ }
595
+ ],
596
+ "rules": [
597
+ {
598
+ "id": "D-PORT",
599
+ "check": "docs-lint",
600
+ "rule": "F002:http-ports",
601
+ "severity": "deploy",
602
+ "owner": "BIZ-general",
603
+ "why": "no biz service listens on a port, so a document telling an operator to curl one sends them to a dead address and the install reads as broken; the ban carries a probe into the compose files, so it switches itself off the day a service does publish a port",
604
+ "fix": "delete the localhost:33xxx address from the document and say what the reader checks instead (the queue, the registry entry); node_modules/@onlineapps/conn-orch-validator/templates/business-service/docs/80-setup/VALIDATION.md carries the shape",
605
+ "doc": "api/docs/biz/00-model/service-shape.md"
606
+ },
607
+ {
608
+ "id": "D-NPM",
609
+ "check": "docs-lint",
610
+ "rule": "L016",
611
+ "severity": "deploy",
612
+ "owner": "BIZ-general",
613
+ "why": "an npm script a live document tells the reader to run, and the repository does not declare, is an instruction nobody can carry out; L016 resolves a cited name against the package.json above this tree, which for a service repository is exactly one file",
614
+ "fix": "cite the script this repository's package.json declares, or delete the instruction",
615
+ "doc": "api/docs/biz/DOC-STANDARD.md"
616
+ },
617
+ {
618
+ "id": "D-SCRIPT",
619
+ "check": "docs-lint",
620
+ "rule": "L007",
621
+ "severity": "deploy",
622
+ "owner": "BIZ-general",
623
+ "why": "a cited script that is not there is an instruction nobody can carry out; L007 resolves a bare scripts/... citation against THIS repository and an api/scripts/... one against the api checkout, so the row reaches both halves",
624
+ "fix": "correct the path the finding names, or delete the citation when the script is gone",
625
+ "doc": "api/docs/biz/DOC-STANDARD.md"
626
+ },
627
+ {
628
+ "id": "D-RETIRED",
629
+ "check": "docs-lint",
630
+ "rule": "F002",
631
+ "severity": "deploy",
632
+ "owner": "BIZ-general",
633
+ "why": "a retired concept taught by a live document sends the next author back to it; the words and their probes are the lint's banned_tokens, which this row cites and never copies (004 point 2) — D-PORT claims the one of them the owner gave its own fix sentence",
634
+ "fix": "rewrite the sentence in the terms the register names as the replacement; a document recording history rather than instructing belongs under 80-decisions/ or 90-migration/, where the ban does not apply",
635
+ "doc": "api/docs/biz/RETIRED-VOCABULARY.md"
636
+ },
637
+ {
638
+ "id": "D-HEADER",
639
+ "check": "docs-lint",
640
+ "rule": "S001,S002",
641
+ "severity": "deploy",
642
+ "owner": "BIZ-general",
643
+ "why": "the two header lines are what make a tree navigable and a fact attributable: Parent: is the only way up, and Owns: is the sentence saying which fact this node — and no other — answers for",
644
+ "fix": "open the document with Parent: [text](link) on line 1 and Owns: <one line> on line 2 (DOC-STANDARD rule 5)",
645
+ "doc": "api/docs/biz/DOC-STANDARD.md"
646
+ },
647
+ {
648
+ "id": "D-LINT",
649
+ "check": "docs-lint-clean",
650
+ "severity": "deploy",
651
+ "owner": "BIZ-general",
652
+ "why": "the documentation lint reports nothing over this service's tree; the rows above name the five classes the owner gave a fix sentence of their own, and every OTHER finding the lint raises lands here — measured 2026-09-09 over the eight repositories, 80 findings of which 16 fell to a row, so a tree the documentation gate called broken came back deployable (automation-gates.md §5). The row cites no rule id, because a list here would have to be held equal to the linter's and would fall behind it the first time BIZ-DOCS wrote a rule (004 point 2: the manifest cites, it never restates). It counts only what the LINT graded error — the set its own --severity error prints — so a rule BIZ-DOCS adds at warn is visible in their run and blocks no deploy until they raise it on their dated transition, which is how a gate lands with compliance rather than ahead of it (automation-gates.md §3)",
653
+ "fix": "repair what the finding names, in the terms DOC-STANDARD sets for that rule; node scripts/ci/lint-biz-docs.mjs --root <service>/docs --tree-config <service>/config/biz-docs-lint.tree.json --skip-code-rules prints the same line",
654
+ "doc": "api/docs/biz/DOC-STANDARD.md"
655
+ }
656
+ ]
657
+ }
658
+ }