@onlineapps/conn-orch-validator 7.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +2582 -2
  2. package/README.md +1038 -4
  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 +408 -101
  8. package/src/CookbookTestUtils.js +7 -8
  9. package/src/ServiceReadinessValidator.js +10 -35
  10. package/src/ValidationOrchestrator.js +219 -71
  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 +2 -1
  16. package/src/helpers/createServiceReadinessTests.js +60 -4
  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 +140 -9
  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 +2 -1
  76. package/templates/business-service/.dockerignore +42 -0
  77. package/templates/business-service/.gitlab-ci.yml +409 -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 +22 -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 +123 -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,10 @@
1
+ {
2
+ "_note": "Tree-bound overlay for linting api_biz/__SERVICE_NAME__/docs with api/scripts/ci/lint-biz-docs.mjs. Run from api/: node scripts/ci/lint-biz-docs.mjs --root ../api_biz/__SERVICE_NAME__/docs --tree-config ../api_biz/__SERVICE_NAME__/config/biz-docs-lint.tree.json --skip-code-rules --format text. Only the tree-bound keys belong here; every other rule is a platform rule inherited from api/config/biz-docs-lint.json. Manifest row C-LINT requires the file: without it the lint refuses to run over the tree, and a tree nothing lints is a tree nothing checks.",
3
+ "canonical_facts": [],
4
+ "_canonical_facts_note": "Empty on purpose, and it stays empty until this service owns a cross-cutting fact of its own. canonical_facts names the node that OWNS such a fact; everything a new service says is owned by the platform tree api/docs/biz/** and this tree only links to it.",
5
+ "default_max_words": 3687,
6
+ "_default_max_words_note": "The platform per-node norm (api/config/biz-docs-lint.json). A service tree does not get a laxer node cap than the platform tree it hangs off.",
7
+ "tree_max_words": 4000,
8
+ "_tree_max_words_note": "The scaffolded tree measures 1666 words in five nodes (INSTALL 544, VALIDATION 524, PLATFORM_MATRIX 412, 80-setup/README 94, README 92), counted with node api/scripts/ci/lib/countWords.js on 2026-09-14 \u2014 the two maps joined with D-LINT, which reports the orphan nodes a tree without them leaves behind. The budget is deliberately larger than that measurement, because a new service writes its own nodes into an otherwise empty tree; RAISE IT WITH A MEASUREMENT once the tree is real, the way api_biz/converter and api_biz/pdfgen state theirs, and never to make a failing run pass.",
9
+ "max_words_overrides": {}
10
+ }
@@ -0,0 +1,22 @@
1
+ # __SERVICE_NAME__ environment
2
+ # Copy to config/env-active/__SERVICE_NAME__.env and adjust values.
3
+
4
+ SERVICE_NAME=__SERVICE_NAME__
5
+
6
+ # Schema migrations at deploy time. The deploy job (block `oa-ci v1` of
7
+ # .gitlab-ci.yml) runs api/scripts/lib/mariadb-migrations.sh between `compose
8
+ # pull` and `compose up`, and that runner REQUIRES both names - fail-fast, no
9
+ # default value (owner decision
10
+ # api/docs/governance/confirmations/db-migrations-first-deploy.md 001, phase 2).
11
+ # The account is the SAME one DB_USER names: its grants cover this service's
12
+ # schema and nothing else, so a migration that reached into a neighbouring
13
+ # database is refused by the server instead of quietly applied
14
+ # (db-accounts-per-service 001). Pattern: api/config/env-templates/auth.env.
15
+ #
16
+ # Commented out because this scaffold has no database: its
17
+ # config/service/integration-contract.json declares no `database` block, the
18
+ # deploy step prints NOT APPLICABLE and reads neither name. A service that
19
+ # declares that block uncomments both here and fills the values in
20
+ # config/env-active/ only - per machine, never in git.
21
+ # MARIADB_MIGRATION_USER=
22
+ # MARIADB_MIGRATION_PASSWORD=
@@ -0,0 +1,65 @@
1
+ # GENERATED FILE - do not edit. Edit the manifest instead:
2
+ # api/config/shared-env.json, then run `npx oa-sync-template shared-env --target <dir>`.
3
+
4
+ # The runtime environment name every platform library resolves through its own runtime-config schema; without it a library cannot say which environment it is reporting from.
5
+ NODE_ENV=development
6
+
7
+ # The log level an infra service's config/logging.json resolves through ${LOG_LEVEL}; one platform value so a stack does not log at four different levels.
8
+ LOG_LEVEL=info
9
+
10
+ # The HMAC-SHA256 secret the gateway and auth sign and verify access tokens with; it is shared because both ends of one token must hold the same value (docs/standards/JWT_AUTH.md).
11
+ JWT_SECRET=CHANGE_ME
12
+
13
+ # The ordered list of SecretBox master keys every process that resolves a secret reference decrypts with - entries of <key_id>:<base64 32-byte key> separated by commas, newest FIRST: the first entry seals every new write, every entry opens what it sealed (selected by the blob's own key_id), and a blob naming a key outside the list is refused rather than guessed at (owner decision docs/governance/confirmations/secretbox-master-keys.md 001). It is shared because one keyring has to reach every bearer at once - a rotation is a switch of this one list, and a service left on yesterday's list stops reading what its neighbour has already re-encrypted; the measured cost of the single key it replaces is six of ten rows of oagen_meta.secret unreadable for two weeks after the August 2026 rotation. The value is per-machine and lives only in config/env-active/shared.env. @onlineapps/conn-infra-secrets owns the name and the format and never reads the environment itself (principle 1): the bearer reads this key and hands the string to the connector.
14
+ SECRETS_MASTER_KEYS=CHANGE_ME
15
+
16
+ # The address of the platform cache on the docker network; declared once because every service reaches the same instance. The instance demands a password (server-topology 004: Redis publishes on wg0 with authentication), so the URL carries it: the value is per-machine and lives only in config/env-active/shared.env, and it is the same password as NODE_CACHE_PASSWORD in config/env-active/compose-infra.env - one fact reaching the container that enforces it and the services that authenticate to it, the same two-delivery split MINIO_ACCESS_KEY and MINIO_SECRET_KEY already have.
17
+ REDIS_URL=redis://:CHANGE_ME@api_node_cache:6379
18
+
19
+ # The broker every service opens its MQ connection with; the credentials are per-machine and live only in config/env-active/shared.env - the platform account is oa_dev, declared by infra/api_services_queuer/definitions.json.
20
+ RABBITMQ_URL=amqp://CHANGE_ME:CHANGE_ME@api_services_queuer:5672
21
+
22
+ # HTTP address of the registry for administrative operations from the host - scripts/bump-infra-version.sh announces an infrastructure version bump through it. No service reads it: registration travels over MQ to registry.register and discovery reads the Redis projection (confirmation biz-discovery-redis 002).
23
+ REGISTRY_URL=http://api_services_registry:33100
24
+
25
+ # The host AWS SigV4 signatures are computed against - deliberately the loopback name, not the reachable one, because MinIO validates the Host header (infra/api_minio_proxy/README.md).
26
+ MINIO_ENDPOINT=127.0.0.1
27
+
28
+ # Where the bytes actually go once the signature is computed against MINIO_ENDPOINT; the other half of the host-header split (infra/api_minio_proxy/README.md).
29
+ MINIO_ACTUAL_HOST=api_minio_proxy
30
+
31
+ # The S3 API port of the object store, shared with the endpoint it belongs to.
32
+ MINIO_PORT=9000
33
+
34
+ # Whether the object-store client opens its connection with TLS. It is declared here because until d.411 it was declared nowhere and every reader carried its own default, so one platform value had as many owners as it had readers (.claude/rules/change-discipline.md - One rail per concern). The platform value is false: MinIO publishes its S3 port on the private wg0 overlay only and never on a public interface (docs/governance/confirmations/server-topology.md 002 and 004), so the hop that would carry TLS is an encrypted one already; a deployment that puts TLS in front of the object store changes this key, and no library.
35
+ MINIO_USE_SSL=false
36
+
37
+ # The object-store account every service reads and writes under; the same pair is also interpolated into compose for the MinIO container itself (config/env-templates/compose-infra.env), two delivery mechanisms that must carry one value.
38
+ MINIO_ACCESS_KEY=CHANGE_ME
39
+
40
+ # The secret of that same object-store account; shares the compose-interpolation note of MINIO_ACCESS_KEY.
41
+ MINIO_SECRET_KEY=CHANGE_ME
42
+
43
+ # How often (ms) a business service publishes its MQ heartbeat, and the ONE source of truth for how fresh that signal has to be: every staleness threshold is a multiple of this cadence (BIZ_HEARTBEAT_STALE_FACTOR), never a second duration set by hand - owner decision docs/governance/confirmations/biz-health-freshness.md 001, which supersedes the absolute INFRASTRUCTURE_HEALTH_BUSINESS_STALE_AFTER of batch 111a. It is shared because the publisher and the judge are different processes: the registry derives its monitor and janitor thresholds from it today, and @onlineapps/service-wrapper reads it as the only cadence there is (the wrapper.registry.heartbeatInterval config key is retired and refused by name), so a cadence change cannot reach one side only.
44
+ BIZ_HEARTBEAT_INTERVAL_MS=30000
45
+
46
+ # How many heartbeat cadences a business service may stay silent before it is judged stale; the only knob on top of BIZ_HEARTBEAT_INTERVAL_MS, and the reason no threshold is written as a duration anywhere (owner decision docs/governance/confirmations/biz-health-freshness.md 001: the factor is config, not doctrine - changing it is an ordinary config change). May be fractional; the derived threshold is whole milliseconds.
47
+ BIZ_HEARTBEAT_STALE_FACTOR=2.5
48
+
49
+ # The tenant a validation or test run writes into; it is platform-wide and never overridden per service, because a service that picks its own namespace can pick a live one. The classes and their meaning are owned by docs/standards/tenant-allocation.md.
50
+ TESTING_TENANT_ID=99
51
+
52
+ # The workspace half of that same namespace; it travels with TESTING_TENANT_ID because a run that knows one without the other addresses a namespace it cannot name.
53
+ TESTING_WORKSPACE_ID=200
54
+
55
+ # Process locale. No code in the platform reads it; the same three values are also baked into every infra Dockerfile as ENV, so this declaration has no measured consumer of its own.
56
+ LANG=en_US.UTF-8
57
+
58
+ # Process locale. No code in the platform reads it; the same three values are also baked into every infra Dockerfile as ENV, so this declaration has no measured consumer of its own.
59
+ LC_ALL=en_US.UTF-8
60
+
61
+ # Process locale. No code in the platform reads it; the same three values are also baked into every infra Dockerfile as ENV, so this declaration has no measured consumer of its own.
62
+ LANGUAGE=en_US.UTF-8
63
+
64
+ # The timezone every container renders local time in, declared ONCE here and inherited by every bearer of shared.env rather than as a per-service key (owner decision docs/governance/confirmations/container-timezone.md 001, on the BIZ-invoicing finding of 2026-08-29 that a document issued after local midnight carried the PREVIOUS day's date, because every container ran UTC). Timestamps stored in databases and in message envelopes stay UTC; this key changes only what local-time rendering resolves to - an invoice date, a log line a human reads - and code that derives a business date must still say which zone it means. Node honours it from its own ICU data, so a container needs no tzdata package for Date/Intl; libc tools (date, shell) follow it only where the image ships tzdata.
65
+ TZ=Europe/Prague
@@ -0,0 +1,14 @@
1
+ {
2
+ "service": {
3
+ "name": "__REGISTRY_NAME__",
4
+ "description": "__SERVICE_DESCRIPTION__",
5
+ "specificationEndpoint": "/specification",
6
+ "workspaceScoped": false
7
+ },
8
+ "wrapper": {
9
+ "mq": {
10
+ "url": "${RABBITMQ_URL}",
11
+ "prefetch": 10
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "serviceName": "__REGISTRY_NAME__",
3
+ "requiredConnectors": {
4
+ "db": false,
5
+ "redis": false,
6
+ "mq": true,
7
+ "minio": false
8
+ },
9
+ "integrationMinimum": {
10
+ "minTestFiles": 1
11
+ }
12
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "schema_version": "3.0",
3
+ "operations": {
4
+ "echo": {
5
+ "description": "Return the submitted text unchanged. The example operation a new service replaces with its own.",
6
+ "handler": "handlers/v3/echo#echo",
7
+ "mutates": false,
8
+ "resource_type": null,
9
+ "bundle_scope": "platform",
10
+ "minRole": "EDITOR",
11
+ "input": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "required": ["text"],
15
+ "properties": {
16
+ "text": {
17
+ "type": "string",
18
+ "description": "Text to return unchanged",
19
+ "minLength": 1,
20
+ "maxLength": 1000,
21
+ "examples": ["cookbook"]
22
+ }
23
+ }
24
+ },
25
+ "output": {
26
+ "type": "object",
27
+ "required": ["text", "length"],
28
+ "properties": {
29
+ "text": {
30
+ "type": "string",
31
+ "description": "The submitted text, byte for byte"
32
+ },
33
+ "length": {
34
+ "type": "integer",
35
+ "description": "Number of UTF-16 code units in the submitted text"
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,60 @@
1
+ # Production compose for __SERVICE_NAME__
2
+ #
3
+ # The image is the one CI built and pushed, pinned by its DIGEST (requirement R1,
4
+ # api/docs/operations/biz-rework-deployment-requirements.md). The build job
5
+ # publishes BIZ_IMAGE_DIGEST as a dotenv artifact and the deploy job exports it;
6
+ # with the value missing, the ${VAR:?…} marker fails the run by name instead of
7
+ # resolving to a nameless image. A mutable tag is refused because two concurrent
8
+ # pipelines make it nondeterministic between the push and the pull, and a
9
+ # rollback then has no target to roll back to.
10
+ #
11
+ # HOST CONTRACT OF THE BIZ BOX — owner decision
12
+ # api/docs/governance/confirmations/server-topology.md 004:
13
+ # * ../shared.env is the ONE host-level env file, at
14
+ # /var/www/html/oadrive/api_biz/shared.env. Its values are the infra box's
15
+ # property, so it is RENDERED there from that box's own env-active by
16
+ # api/scripts/provision/render-biz-shared-env.sh and carried to the biz box by
17
+ # the operator (server-topology 004 and 005) — role-biz writes no env file.
18
+ # This compose REFERENCES it and never copies it: one file per box, not one
19
+ # copy per repository.
20
+ # * ./config/env-active/__SERVICE_NAME__.env carries only this service's own
21
+ # keys.
22
+ # * biz_network is external and created by role-biz. It is the ONLY network
23
+ # that box has: the infra box's networks do not exist there, so naming one
24
+ # of them here would make the service undeployable — which is why the gate
25
+ # and tests assert that no other network name appears in this file at all.
26
+ #
27
+ # Usage: BIZ_IMAGE_DIGEST=sha256:… docker compose -f docker-compose.production.yml up -d
28
+
29
+ services:
30
+ __CONTAINER_NAME__:
31
+ image: "registry.gitlab.com/onlineapps/oadrive/__REPO_NAME__@${BIZ_IMAGE_DIGEST:?BIZ_IMAGE_DIGEST is required - the CI deploy job must export the digest of the pushed image}"
32
+ # There is no build section on purpose: docker refuses a build tag that
33
+ # carries a digest, and an image built here would in any case not be the one
34
+ # CI proved. Build locally through docker-compose.yml instead.
35
+ container_name: __CONTAINER_NAME__
36
+ deploy:
37
+ resources:
38
+ limits:
39
+ # Same norm as the dev compose, and deliberately the same number —
40
+ # owner decision api/docs/governance/confirmations/biz-memory-limits.md
41
+ # 001 names dev and production together, so production is not the
42
+ # place to discover a different limit.
43
+ #
44
+ # WHAT IT COVERS: the resident service process. There is no test
45
+ # runner here — suites do not run in production — so nothing else
46
+ # shares this cgroup by construction.
47
+ #
48
+ # This file used to declare no limit at all, which meant a new service
49
+ # reached production unbounded while its dev container was capped.
50
+ memory: 512M
51
+ env_file:
52
+ - ../shared.env
53
+ - ./config/env-active/__SERVICE_NAME__.env
54
+ restart: unless-stopped
55
+ networks:
56
+ - biz_network
57
+
58
+ networks:
59
+ biz_network:
60
+ external: true
@@ -0,0 +1,93 @@
1
+ services:
2
+ __CONTAINER_NAME__:
3
+ build:
4
+ context: .
5
+ container_name: __CONTAINER_NAME__
6
+ user: "1000:1000"
7
+ # No `ports:`. A biz service binds nothing — work arrives over MQ and is
8
+ # dispatched in-process (ADR 0005), and
9
+ # docs/biz/00-model/service-shape.md § No-HTTP tests states the rule for the
10
+ # compose file directly. The scaffold used to publish a port here, so every
11
+ # new service broke the shape at the moment it was created.
12
+ deploy:
13
+ resources:
14
+ limits:
15
+ # The platform norm for a biz service in operation — owner decision
16
+ # api/docs/governance/confirmations/biz-memory-limits.md 001, which
17
+ # holds in dev AND in production. It is a prescribed value, not one
18
+ # measured on this service or on any other.
19
+ #
20
+ # WHAT IT COVERS: the resident service process, and nothing else. No
21
+ # test suite may share this cgroup — that is why the runner below has
22
+ # a budget of its own, and why raising this number is never the fix
23
+ # for a suite that runs out of memory.
24
+ #
25
+ # Revise it when this service exceeds 60% of the limit in operation,
26
+ # measured the way the norm was: cgroup memory.stat:anon, sampled
27
+ # inside the running container.
28
+ memory: 512M
29
+ volumes:
30
+ - .:/app
31
+ env_file:
32
+ - ./config/env-active/shared.env
33
+ - ./config/env-active/__SERVICE_NAME__.env
34
+ networks:
35
+ - api_network
36
+ restart: unless-stopped
37
+ entrypoint: ["/bin/sh", "init.sh"]
38
+ # PID 1 is node, never npm: init.sh execs what it is given, and npm does
39
+ # not forward SIGTERM - a stop would be ten seconds of waiting and then a
40
+ # SIGKILL through a service that never closed a channel. Row R-PID1,
41
+ # confirmation biz-compose-pid1 001/002.
42
+ command: ["node", "index.js"]
43
+
44
+ # --- oa-test-runner v1
45
+ # One-shot test runner — part of the unified biz service shape
46
+ # (owner decision 2026-09-07, docs/governance/confirmations/biz-test-container.md 001).
47
+ #
48
+ # Same build, same env_file, same networks as the service above, so
49
+ # "tests run in a container" stays true — but its OWN memory budget.
50
+ #
51
+ # WHY: two loads were sharing one budget, not one load being too big. Measured
52
+ # by the BIZ-META thread on 2026-09-06/07 inside the containers, from
53
+ # /sys/fs/cgroup/memory.peak (api_biz/meta/docker-compose.yml carries the full
54
+ # numbers): the unit suite peaked at 343 MiB and the service's own peak was
55
+ # 132 MiB, against a shared 384 MiB limit — so a `docker exec` test run was
56
+ # killed by the kernel AND took the service down with it, while the
57
+ # integration suite failed intermittently on a different file each time.
58
+ #
59
+ # The 1g below is the platform NORM for the runner — owner decision
60
+ # api/docs/governance/confirmations/biz-memory-limits.md 001, the same
61
+ # decision that sets the service's 512M above. It applies in dev and CI only,
62
+ # and it assumes one suite at a time (`--maxWorkers=1`). It is no longer
63
+ # "meta's number carried over": measurement is what produced the norm, the
64
+ # norm is what this file now states.
65
+ #
66
+ # `mem_limit`, not the `deploy.resources.limits` form: both were measured to
67
+ # apply on `docker compose run` with Compose v5.1.3, and this is the plain
68
+ # Compose one rather than the swarm section Compose honours in part.
69
+ #
70
+ # NOT started by a plain `docker compose up` — the `test` profile keeps it
71
+ # out. No `restart`, no healthcheck: it runs once and exits. The one
72
+ # documented way to run it is the npm scripts in package.json, which wrap:
73
+ # docker compose run --rm __CONTAINER_NAME___tests npm run test:unit
74
+ __CONTAINER_NAME___tests:
75
+ build:
76
+ context: .
77
+ profiles:
78
+ - test
79
+ user: "1000:1000"
80
+ mem_limit: 1g
81
+ volumes:
82
+ - .:/app
83
+ env_file:
84
+ - ./config/env-active/shared.env
85
+ - ./config/env-active/__SERVICE_NAME__.env
86
+ networks:
87
+ - api_network
88
+ entrypoint: ["/bin/sh", "init.sh"]
89
+ # --- end oa-test-runner v1
90
+
91
+ networks:
92
+ api_network:
93
+ external: true
@@ -0,0 +1,123 @@
1
+ Parent: [../../README.md](../../README.md)
2
+ Owns: the installation flow for a new __REPO_NAME__ instance — prerequisites, environment preparation, the bootstrap sequence, and what this service does NOT need.
3
+
4
+ # INSTALL
5
+
6
+ ## Scope
7
+
8
+ Bootstraps `__REPO_NAME__` — a business service with no database and no object
9
+ storage. It binds no port: work arrives over MQ and is dispatched in-process
10
+ (ADR 0005, `api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md`), so
11
+ there is no HTTP endpoint to install or to call afterwards.
12
+
13
+ What the service needs is declared once, in
14
+ `config/service/integration-contract.json` → `requiredConnectors`: `mq` only.
15
+ `db`, `redis` and `minio` are `false`, which is why this document has no SQL
16
+ step — the installation contract derives the SQL half from the contract's
17
+ `database` block, and this service declares none.
18
+
19
+ ## Prerequisites
20
+
21
+ - Docker and Docker Compose v2.
22
+ - Node.js — the major this repository declares in `package.json` `engines.node`.
23
+ The same major is in the `Dockerfile` base image and in `.gitlab-ci.yml`;
24
+ requirement R3 of the deploy contract fails the gate if the three disagree, so
25
+ read the major there rather than from a number written here.
26
+ - A reachable RabbitMQ and service registry. Their URLs arrive as
27
+ `${RABBITMQ_URL}` and `${REGISTRY_URL}`, the two placeholders
28
+ `config/service/config.json` carries; ConfigLoader substitutes them at boot and
29
+ fails by name if either is unset.
30
+ - The external Docker network the compose file names (`api_network` in dev), and
31
+ on the production biz box `biz_network` — both are created outside this
32
+ repository.
33
+
34
+ ## Environment preparation
35
+
36
+ ```bash
37
+ mkdir -p config/env-active
38
+ cp config/env-templates/shared.env config/env-active/shared.env
39
+ cp config/env-templates/__SERVICE_NAME__.env config/env-active/__SERVICE_NAME__.env
40
+ ```
41
+
42
+ Then edit both files: `shared.env` carries the infrastructure URLs and the
43
+ testing namespace (`TESTING_TENANT_ID`, `TESTING_WORKSPACE_ID`), the service file
44
+ carries what belongs to this service alone. `config/env-active/` is gitignored —
45
+ the templates are the versioned half, the active files are per-instance.
46
+
47
+ Every variable a handler reads must additionally be declared in
48
+ `config/service/integration-contract.json` → `env`, with a reason per name. The
49
+ scaffold declares no `env` block because it reads none: the only two names it
50
+ needs are the `${…}` placeholders above, and the contract must not repeat what a
51
+ config placeholder already covers. The first handler that reads an environment
52
+ variable adds the block.
53
+
54
+ On the production biz box the second env file is a host-level `../shared.env`,
55
+ written once per box and referenced rather than copied (owner decision
56
+ `api/docs/governance/confirmations/server-topology.md` 004).
57
+
58
+ ## Bootstrap sequence
59
+
60
+ 1. Prepare the env files (above).
61
+ 2. Start the dependencies this service declares required — MQ and the registry.
62
+ They are not part of this repository.
63
+ 3. Start the service:
64
+
65
+ ```bash
66
+ docker compose up -d --build
67
+ ```
68
+
69
+ `init.sh` installs dependencies on first start and skips the install when
70
+ neither `package.json` nor `package-lock.json` changed.
71
+
72
+ That command builds the SERVICE only. The one-shot test runner carries
73
+ `profiles: [test]`, so it is outside a plain build as well as outside a plain
74
+ `up` — measured 2026-09-14: `docker compose build --dry-run` builds one image,
75
+ `docker compose --profile test build --dry-run` builds two. Build it when the
76
+ suites are to run against this checkout:
77
+
78
+ ```bash
79
+ docker compose --profile test build
80
+ ```
81
+ 4. Verify the install — [VALIDATION.md](VALIDATION.md) states the checks and what
82
+ each one must print.
83
+
84
+ ## Production
85
+
86
+ The production compose pins the image by digest, so it is pulled, never built:
87
+
88
+ ```bash
89
+ BIZ_IMAGE_DIGEST=sha256:… docker compose -f docker-compose.production.yml pull
90
+ BIZ_IMAGE_DIGEST=sha256:… docker compose -f docker-compose.production.yml up -d
91
+ ```
92
+
93
+ CI publishes that digest from the build stage and the deploy job refuses to run
94
+ without it (requirement R1). A first deploy also clones the checkout on the box
95
+ if it is missing, so onboarding a new service needs no manual step there.
96
+
97
+ ### Migrations at deploy time
98
+
99
+ Between that `pull` and that `up` the deploy job applies this service's pending
100
+ MariaDB migrations — the ordering is the safety: the new image is already on the
101
+ box, the previous container is still serving, and a migration that fails stops
102
+ the deploy before the switch. Two facts decide what happens:
103
+
104
+ - `config/service/integration-contract.json` → `database`. This scaffold declares
105
+ no such block, so the step prints `NOT APPLICABLE (no database block)` and the
106
+ deploy carries on. A service that declares one has its `database.schema`
107
+ migrated from the directory `database.migrations` names.
108
+ - `config/env-active/__SERVICE_NAME__.env` → `MARIADB_MIGRATION_USER` and
109
+ `MARIADB_MIGRATION_PASSWORD`, the service's own account. The step creates
110
+ neither the schema nor the account and refuses the deploy when that account
111
+ cannot open the schema, naming the runbook instead.
112
+
113
+ Who creates them, with which grants and which collation, is the runbook's step
114
+ and is not repeated here: `api/docs/setup/INSTALL.md`, the business-schema step.
115
+ The decision behind both halves — two phases, the service account as the
116
+ isolation guard, one runner and one tracker across them — is owner confirmation
117
+ `api/docs/governance/confirmations/db-migrations-first-deploy.md` 001.
118
+
119
+ ## Status
120
+
121
+ Current. One of the three files the installation contract requires by name —
122
+ `biz-ci-gate verify-install-contract` (and `verify-contract`, which folds it in)
123
+ fails when it is missing.
@@ -0,0 +1,65 @@
1
+ Parent: [../../README.md](../../README.md)
2
+ Owns: the execution differences between Linux, macOS and a CI/cloud runner when installing or validating __REPO_NAME__.
3
+
4
+ # PLATFORM MATRIX
5
+
6
+ Every command in this document runs from the repository root
7
+ (`api_biz/__SERVICE_NAME__/`). Relative paths in prose and in links resolve from
8
+ this document's own directory (`docs/80-setup/`) instead — the two bases differ,
9
+ so a path copied from one into the other does not resolve.
10
+
11
+ The service itself runs inside the image the `Dockerfile` builds, so the host
12
+ platform decides how the containers are reached, never what runs in them. That
13
+ is why the differences below are about the shell around the container, not about
14
+ the service.
15
+
16
+ ## Linux
17
+
18
+ - Reference environment.
19
+ - The compose file runs the service as `user: "1000:1000"`. On a host whose own
20
+ uid is not 1000, files the container writes into the bind mount (`node_modules`,
21
+ `conn-runtime/`) belong to that uid — run the container as your own uid, or
22
+ accept the ownership.
23
+ - Start and validate:
24
+
25
+ ```bash
26
+ docker compose up -d --build
27
+ npm run test:container
28
+ npm run test:cookbooks:container
29
+ ```
30
+
31
+ ## macOS
32
+
33
+ - Same commands; Docker Desktop maps the bind mount, so the `user: "1000:1000"`
34
+ line above has no visible effect on file ownership.
35
+ - Run the suites through the container scripts above rather than on the host: the
36
+ runner has the env files and the same image the service uses. A bare `npm test`
37
+ on the host runs against whatever Node and environment the shell happens to
38
+ carry.
39
+ - The dependency guard in `init.sh` uses `md5sum`, which runs inside the Alpine
40
+ container, not on the host — macOS shipping `md5` instead is irrelevant here.
41
+
42
+ ## CI / cloud runner
43
+
44
+ - Non-interactive: no prompts, and nothing may depend on a developer's shell.
45
+ `.gitlab-ci.yml` installs with `npm ci` and runs `npm run test:ci`.
46
+ - The contract gates run from the installed validator, not from a checkout of the
47
+ platform repository:
48
+
49
+ ```bash
50
+ node node_modules/@onlineapps/conn-orch-validator/src/cli/biz-ci-gate.js verify-contract
51
+ ```
52
+
53
+ `verify-contract` needs `LIBRARIES_SSOT_URL` (requirement R6 compares the
54
+ `@onlineapps/*` pins against the platform library set) and the service's
55
+ dependencies installed — the test-coverage half asks the service's own jest
56
+ which files it would run.
57
+ - The build stage publishes the image digest as a dotenv artifact and the deploy
58
+ stage refuses to run without it; both stages are declared in `.gitlab-ci.yml`.
59
+ - Windows is outside the supported matrix.
60
+
61
+ ## Status
62
+
63
+ Current. One of the three files the installation contract requires by name —
64
+ `biz-ci-gate verify-install-contract` (and `verify-contract`, which folds it in)
65
+ fails when it is missing.
@@ -0,0 +1,18 @@
1
+ Parent: [../README.md](../README.md)
2
+ Owns: the map of the installation branch — which of the three installation documents answers which question
3
+
4
+ # Setup
5
+
6
+ An operator installs this repository from these three documents, in this order.
7
+
8
+ - [INSTALL](INSTALL.md) — prerequisites, environment preparation and the bootstrap sequence
9
+ - [PLATFORM_MATRIX](PLATFORM_MATRIX.md) — what differs between Linux, macOS and a CI runner
10
+ - [VALIDATION](VALIDATION.md) — the checks that prove the install, and what each failure means
11
+
12
+ The three are the platform installation contract (`SETUP_DOCS`), so the set is
13
+ fixed: a repository carries all three or it does not satisfy the contract.
14
+
15
+ ## Status
16
+
17
+ Current. The map of this branch: `M001` of the documentation lint reports a leaf
18
+ this file does not link, and `S008` reports a branch that carries no map at all.
@@ -0,0 +1,78 @@
1
+ Parent: [../../README.md](../../README.md)
2
+ Owns: the mandatory checks after installing __REPO_NAME__, the output each one must produce, and the failure signature that says which check did not hold.
3
+
4
+ # VALIDATION
5
+
6
+ ## Required checks after install
7
+
8
+ 1. The repository satisfies the platform contract — deploy, installation,
9
+ environment and test coverage.
10
+ 2. The unit suite passes in the test runner container.
11
+ 3. The cookbook run dispatches the example operation and asserts its output by
12
+ value.
13
+ 4. The container reaches the end of boot and registers. **There is no health
14
+ endpoint to call** — a biz container serves no HTTP (ADR 0005), so the
15
+ container log and the registry are the only readiness evidence.
16
+
17
+ ## Command sequence
18
+
19
+ Every command runs from the repository root (`api_biz/__SERVICE_NAME__/`).
20
+
21
+ ```bash
22
+ # 1 — contract gates (deploy R1-R5/R7-R8, installation docs, env, test coverage, R6)
23
+ node node_modules/@onlineapps/conn-orch-validator/src/cli/biz-ci-gate.js verify-contract
24
+
25
+ # 2 — unit suite, in the one-shot runner beside the service
26
+ npm run test:container
27
+
28
+ # 3 — cookbooks, in the same runner (it has the env files)
29
+ npm run test:cookbooks:container
30
+
31
+ # 4 — boot evidence
32
+ docker compose logs --tail=50 __CONTAINER_NAME__
33
+ ```
34
+
35
+ ## Expected outcome
36
+
37
+ | Check | Passes when |
38
+ |---|---|
39
+ | 1 contract gates | the run ends with `[BizCiGate] OK verify-contract` and exits `0`; each half prints its own `OK` line first |
40
+ | 2 unit suite | jest reports no failing test and the command exits `0` |
41
+ | 3 cookbooks | the run prints `Failed: 0`, ends with `=== Pre-Validation Completed Successfully ===` and writes `conn-runtime/validation-proof.json` |
42
+ | 4 boot | the log shows registration and handler validation, and the container stays up (`docker compose ps` reports it running, not restarting) |
43
+
44
+ The cookbook check asserts a VALUE, not that something came back: the shipped
45
+ cookbook requires the echoed text to equal what it sent and its length to equal
46
+ `8`. Keep that property when the example operation is replaced — an assertion of
47
+ truthiness proves the dispatch worked and nothing about the result
48
+ (`.claude/rules/architecture-principles.md` §10a).
49
+
50
+ ## Failure signatures
51
+
52
+ - `[BizCiGate] Missing file - …/config/service/integration-contract.json` — the
53
+ contract is gone; every gate reads it first.
54
+ - `[BizCiGate] FAIL … SETUP_DOCS — Missing required file - docs/80-setup/…` — one
55
+ of these three documents was deleted or renamed.
56
+ - `[BizCiGate] FAIL … ENV_COMPLETENESS — "X" is read in … but is neither declared
57
+ … nor covered` — a handler started reading an environment variable; declare it
58
+ in the contract's `env` block with the reason, or delete the read.
59
+ - `[BizCiGate] FAIL … TEST_RUNS_NOWHERE — Test file runs nowhere - …` — a test
60
+ file exists that the `test:all` chain does not run; put it in the chain, or
61
+ declare its script in `stackTiers` with the reason it cannot be there.
62
+ - `[BizCiGate] Missing package script - test:all …` — the chain the coverage gate
63
+ measures against was removed.
64
+ - `[SetupDatabase] Missing database.collation - the schema is created with the
65
+ collation the service declares, and there is no default.` — `setup-db` was asked
66
+ to build a schema for a service whose contract carries a `database` block
67
+ without `collation`. Add the key with the collation this service's tables
68
+ already use; the same key the production runbook reads, and the one uniform row
69
+ `D-DB-COLLATION` asks for.
70
+ - `[ConfigLoader] Missing environment variable - X is required (config placeholder
71
+ ${X})` at boot — the env-active files were not prepared, or that name is missing
72
+ from them.
73
+
74
+ ## Status
75
+
76
+ Current. One of the three files the installation contract requires by name —
77
+ `biz-ci-gate verify-install-contract` (and `verify-contract`, which folds it in)
78
+ fails when it is missing.
@@ -0,0 +1,18 @@
1
+ Parent: [../README.md](../README.md)
2
+ Owns: the map of this repository's documentation tree — which branch answers which question
3
+
4
+ # __REPO_NAME__ — documentation
5
+
6
+ Every document in this tree is reached from here by clicking down; a node no map
7
+ links to is a node nobody finds (DOC-STANDARD rule 1).
8
+
9
+ - [Setup](80-setup/README.md) — installing this repository and proving the install
10
+
11
+ The platform-wide concepts this service is built on — the service shape, the
12
+ tenancy model, the contracts it speaks — live in the platform tree
13
+ `api/docs/biz/`, and this tree links to them rather than restating them.
14
+
15
+ ## Status
16
+
17
+ Current. The map of this tree: `L006` of the documentation lint reports every node
18
+ this file does not reach by clicking down.