@onlineapps/conn-orch-validator 7.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2582 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +409 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +22 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +123 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
|
@@ -0,0 +1,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,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.
|