@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,42 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Runtime data
5
+ config/runtime/
6
+ config/env-active/
7
+
8
+ # Validation proof written by `npm run test:cookbooks` (the validator's own CLI)
9
+ # and by the wrapper on every boot — generated, never committed (same line
10
+ # api_biz/hello-service carries).
11
+ conn-runtime/
12
+
13
+ # Deployability signal written by the manifest step of the boot validation and
14
+ # by `npx oa-validate` (`ci/deployability.json`, the shape of
15
+ # `ci/integration-signal.json`) - generated on every start, never committed.
16
+ ci/
17
+
18
+ # Logs
19
+ logs/
20
+ *.log
21
+ npm-debug.log*
22
+
23
+ # Coverage
24
+ coverage/
25
+
26
+ # Environment (legacy)
27
+ .env
28
+ .env.local
29
+
30
+ # IDE
31
+ .vscode/
32
+ .idea/
33
+ *.swp
34
+ *.swo
35
+ *~
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
40
+
41
+ # Build artifacts
42
+ .oa_drive_deps_hash
@@ -0,0 +1,10 @@
1
+ /**
2
+ * __SERVICE_NAME__ - Main Entry Point
3
+ * Uses unified bootstrap from @onlineapps/service-wrapper
4
+ */
5
+ const { bootstrap } = require('@onlineapps/service-wrapper');
6
+
7
+ bootstrap(__dirname).catch(error => {
8
+ console.error('Failed to start service:', error);
9
+ process.exit(1);
10
+ });
@@ -0,0 +1,54 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ oa_npm_install() {
5
+ npm install --no-audit --no-fund
6
+ }
7
+
8
+ # --- oa-deps-guard v1 (identical in every OA Drive init.sh) ---
9
+ # The dev stack's only skip condition: the md5 of package.json + package-lock.json
10
+ # against the marker the last SUCCESSFUL install wrote. A package-directory probe
11
+ # cannot see a version bump and an mtime probe cannot see a checkout that rewinds
12
+ # package.json, so neither is used here or anywhere else in the file. Enforced byte
13
+ # for byte, within this repo, by tests/scripts/infra-init-scripts.bats — across the
14
+ # eight infra/*/init.sh, this template and the generator's output. A copy pasted
15
+ # into an api_biz/* repo is kept in sync by review only; the gate cannot reach
16
+ # another checkout. A service's own install steps belong in oa_npm_install() above,
17
+ # everything else it needs goes below.
18
+ MARKER_FILE=".oa_drive_deps_hash"
19
+
20
+ HASH_INPUT=""
21
+ if [ -f "package.json" ]; then
22
+ HASH_INPUT="${HASH_INPUT}$(md5sum package.json | awk '{print $1}')"
23
+ fi
24
+ if [ -f "package-lock.json" ]; then
25
+ HASH_INPUT="${HASH_INPUT}$(md5sum package-lock.json | awk '{print $1}')"
26
+ fi
27
+ CURRENT_HASH=$(echo "$HASH_INPUT" | md5sum | awk '{print $1}')
28
+
29
+ PREVIOUS_HASH=""
30
+ if [ -f "$MARKER_FILE" ]; then
31
+ PREVIOUS_HASH=$(cat "$MARKER_FILE" 2>/dev/null || true)
32
+ fi
33
+
34
+ NEEDS_INSTALL=0
35
+ if [ ! -d "node_modules" ] || [ -z "$(ls -A node_modules 2>/dev/null)" ]; then
36
+ NEEDS_INSTALL=1
37
+ fi
38
+
39
+ if [ "$CURRENT_HASH" != "$PREVIOUS_HASH" ] || [ "$NEEDS_INSTALL" = "1" ]; then
40
+ echo "[init.sh] Installing dependencies (hash changed or node_modules missing)..."
41
+ if oa_npm_install; then
42
+ echo "$CURRENT_HASH" > "$MARKER_FILE"
43
+ echo "[init.sh] Dependencies installed, marker $MARKER_FILE updated."
44
+ else
45
+ echo "[init.sh] Dependency install failed - marker $MARKER_FILE left untouched, so the next start retries instead of trusting a half-installed tree. Fix: read the npm error above, then run 'npm install' in this service directory."
46
+ exit 1
47
+ fi
48
+ else
49
+ echo "[init.sh] Dependencies unchanged. Skipping npm install."
50
+ fi
51
+ # --- end oa-deps-guard v1 ---
52
+
53
+ # Run the passed command (e.g. npm run dev or npm run prod).
54
+ exec "$@"
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ testMatch: ['**/tests/**/*.test.js'],
4
+ testTimeout: 30000,
5
+ maxWorkers: 1
6
+ };
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "__REGISTRY_NAME__",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "index.js",
6
+ "engines": {
7
+ "node": ">=24.0.0 <25"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js",
11
+ "dev": "nodemon index.js",
12
+ "test": "jest --config jest.config.js tests/unit --runInBand",
13
+ "test:ci": "jest",
14
+ "test:unit": "jest tests/unit",
15
+ "test:all": "npm run test:unit",
16
+ "test:container": "docker compose run --rm __CONTAINER_NAME___tests npm run test:unit",
17
+ "test:all:container": "docker compose run --rm __CONTAINER_NAME___tests npm run test:all",
18
+ "test:coverage": "jest --coverage",
19
+ "test:cookbooks": "node node_modules/@onlineapps/conn-orch-validator/src/cli/biz-ci-gate.js run-prevalidation",
20
+ "test:cookbooks:container": "docker compose run --rm __CONTAINER_NAME___tests npm run test:cookbooks"
21
+ },
22
+ "dependencies": {
23
+ "@onlineapps/conn-orch-validator": "__SSOT_PIN__",
24
+ "@onlineapps/service-wrapper": "__SSOT_PIN__",
25
+ "@onlineapps/service-common": "__SSOT_PIN__"
26
+ },
27
+ "devDependencies": {
28
+ "jest": "29.7.0",
29
+ "nodemon": "3.1.7"
30
+ }
31
+ }
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env sh
2
+ # Owns: refusing this service's production deploy unless the COMPLETE biz-service uniform run over the checkout being deployed comes back green
3
+ # Usage: verify-deploy-uniform.sh <serviceRoot> <apiRepositoryUrl> <apiRef>
4
+ # Shell: sh
5
+ # Exit: 0 deployable and complete | 1 a blocking finding, or a run that did not look everywhere | 2 the check itself could not run
6
+ # Status: current
7
+ #
8
+ # @see api/docs/governance/confirmations/biz-service-manifest.md
9
+ # @see api/docs/governance/confirmations/server-topology.md
10
+ #
11
+ # WHY IT EXISTS. Confirmation `biz-service-manifest` 006 point 3 and 008: a biz
12
+ # service is deployed by its own repository's CI (`server-topology` 004), so the
13
+ # last place that can refuse an unfit deploy is this repository's own
14
+ # `deploy-production` job, before the SSH step. Deploy is CONFIRMATION, never
15
+ # discovery — the same run has already happened on the developer's machine at
16
+ # push time (006 point 1) — but a confirmation that is skipped when it is
17
+ # inconvenient is not one, so there is no flag, no variable and no branch that
18
+ # turns this off (`automation-gates.md` §1 requirement 5).
19
+ #
20
+ # ONE job (§1.2): measure, and refuse. It fixes nothing, writes nothing into the
21
+ # repository beyond the signal the engine itself records, and judges nothing on
22
+ # its own: every row lives in the manifest inside @onlineapps/conn-orch-validator
23
+ # and the engine's own exit code and signal are the verdict, so the uniform has
24
+ # one implementation (`change-discipline.md` § One rail per concern).
25
+ #
26
+ # WHY A FILE RATHER THAN A HEREDOC IN .gitlab-ci.yml. A heredoc can only ever be
27
+ # run by GitLab: it cannot be executed, tested or read back locally, so the one
28
+ # place the gate would be proven is production. This file is called by the job
29
+ # with three arguments and by `api/tests/scripts/biz-deploy-uniform-gate.bats`
30
+ # with three others, and both run the same bytes.
31
+ #
32
+ # WHY THREE ARGUMENTS AND NOTHING FROM THE ENVIRONMENT. The script knows what to
33
+ # measure, never where the pieces come from (`architecture-principles.md` §1):
34
+ # the job passes the checkout GitLab made, the api repository URL carrying its
35
+ # job token, and the api ref that production follows. Nothing here reads
36
+ # `process.env`, so the same invocation is reproducible outside CI.
37
+ #
38
+ # THE LAYOUT IT NEEDS, AND WHY THE JOB HAS TO ASK FOR IT. The engine answers the
39
+ # rows that read the platform SSOT only for a service that lies at
40
+ # <root>/api_biz/<service> beside <root>/api — measured 2026-09-11 over a
41
+ # converter export: in that layout the run is complete in about a second, and
42
+ # with the same two directories as unrelated siblings seven blocking rows come
43
+ # back NOT RUN (U-ORPHAN, U-IDENTITY, D-PORT, D-NPM, D-SCRIPT, D-RETIRED,
44
+ # D-HEADER) and the signal reads complete: false. A symlink is forbidden
45
+ # (`api/.claude/rules/architecture-principles.md` § No Symlinks) and a second
46
+ # copy of this repository would leave two trees where the reader has to ask which
47
+ # one was measured, so the JOB asks GitLab to check the project out in the right
48
+ # place (GIT_CLONE_PATH) and this script verifies that it did.
49
+
50
+ set -eu
51
+
52
+ CONTEXT="deploy-uniform"
53
+
54
+ # Every failure names what was checked, what was expected and the command that
55
+ # fixes it (`architecture-principles.md` §5, `automation-gates.md` §1.4).
56
+ die() { # <message> <exit-code>
57
+ echo "[$CONTEXT] $1" >&2
58
+ exit "$2"
59
+ }
60
+
61
+ # --- arguments --------------------------------------------------------------
62
+
63
+ if [ "$#" -ne 3 ]; then
64
+ die "Wrong number of arguments - got $#, expected 3. Usage: verify-deploy-uniform.sh <serviceRoot> <apiRepositoryUrl> <apiRef>" 2
65
+ fi
66
+
67
+ SERVICE_ROOT="$1"
68
+ API_URL="$2"
69
+ API_REF="$3"
70
+
71
+ [ -n "$API_URL" ] || die "The api repository URL is empty - the uniform reads the platform SSOT from that checkout, and this script does not guess where it lives. Expected: the URL the job builds from CI_JOB_TOKEN. Fix: pass it as the second argument." 2
72
+ [ -n "$API_REF" ] || die "The api ref is empty - which state of the platform this deploy is measured against is a decision, not a default. Expected: the branch production follows. Fix: pass it as the third argument." 2
73
+ [ -d "$SERVICE_ROOT" ] || die "Service root not found - '$SERVICE_ROOT' is not an existing directory. Expected: the checkout being deployed. Fix: pass \$CI_PROJECT_DIR." 2
74
+
75
+ SERVICE_ROOT=$(cd "$SERVICE_ROOT" && pwd)
76
+ CONTAINER_DIR=$(dirname "$SERVICE_ROOT")
77
+ WORKSPACE_ROOT=$(dirname "$CONTAINER_DIR")
78
+ API_CHECKOUT="$WORKSPACE_ROOT/api"
79
+
80
+ if [ "$(basename "$CONTAINER_DIR")" != "api_biz" ]; then
81
+ die "The checkout is not in the layout the uniform needs - $SERVICE_ROOT lies in '$(basename "$CONTAINER_DIR")', and the engine answers the rows that read the platform SSOT only for a service under <root>/api_biz/ beside <root>/api. Expected: the job checks this project out there. Fix: in .gitlab-ci.yml set the job variable GIT_CLONE_PATH: \$CI_BUILDS_DIR/oa-uniform/api_biz/<service>, and confirm the runner allows a custom build directory ([runners.custom_build_dir] enabled)." 2
82
+ fi
83
+
84
+ command -v git >/dev/null 2>&1 || die "Missing tool - 'git' does not run, and the api checkout this run reads cannot be fetched without it. Fix: install git in the job image (apk add --no-cache git)." 2
85
+ command -v node >/dev/null 2>&1 || die "Missing tool - 'node' does not run, and the run's signal (ci/deployability.json) cannot be read without it. Fix: run this job on the platform node image." 2
86
+
87
+ # --- the api checkout -------------------------------------------------------
88
+ #
89
+ # Read-only and shallow: the uniform reads files, never history. A runner reuses
90
+ # its builds directory between pipelines, so a checkout left by an earlier run
91
+ # would silently measure this deploy against a stale SSOT — it is removed and
92
+ # fetched again, and only when it is recognisably this script's own artefact.
93
+ if [ -e "$API_CHECKOUT" ]; then
94
+ if [ -d "$API_CHECKOUT/.git" ] && [ -f "$API_CHECKOUT/config/services.json" ]; then
95
+ rm -rf "$API_CHECKOUT"
96
+ else
97
+ die "Cannot place the api checkout - $API_CHECKOUT already exists and is not an api clone this script made (no .git and config/services.json). Expected: that path free, or an earlier clone of the api repository. Fix: remove it, or point the job's GIT_CLONE_PATH at a build directory this pipeline owns." 2
98
+ fi
99
+ fi
100
+
101
+ # The token never reaches the log: messages print the URL with its userinfo cut out.
102
+ SAFE_URL=$(printf '%s' "$API_URL" | sed -e 's#//[^@/]*@#//#')
103
+
104
+ if ! git clone --depth 1 --single-branch --branch "$API_REF" "$API_URL" "$API_CHECKOUT"; then
105
+ die "The api checkout could not be cloned - git clone --branch $API_REF $SAFE_URL failed, so api/config/services.json cannot be read and the uniform would answer a fraction of its rows. A partial answer is not a deploy permit, so this refuses rather than continues. Expected: read access for this pipeline's job token. Fix: in GitLab open the api project, Settings > CI/CD > Job token permissions, and add this project to the allowlist (owner's step, confirmation biz-service-manifest 008 point 2); check also that the ref '$API_REF' exists there." 2
106
+ fi
107
+
108
+ # --- the engine -------------------------------------------------------------
109
+ #
110
+ # The pin decides which uniform this service is measured against, so the engine
111
+ # is the one this repository installed — never a floating npx download, which
112
+ # would measure a version this service never proved.
113
+ ENGINE="$SERVICE_ROOT/node_modules/.bin/oa-validate"
114
+ if [ ! -x "$ENGINE" ]; then
115
+ die "Missing engine - $ENGINE is not there, so the uniform cannot be measured, and this step does not wave through a check it could not run. Expected: @onlineapps/conn-orch-validator installed from the pin in package.json. Fix: run 'npm ci' in this job before this step." 2
116
+ fi
117
+
118
+ echo "[$CONTEXT] Checking the checkout being deployed against the biz-service uniform (confirmation biz-service-manifest 008):"
119
+ echo " service: $SERVICE_ROOT"
120
+ echo " workspace: $WORKSPACE_ROOT"
121
+ echo " api: $SAFE_URL ($API_REF)"
122
+
123
+ # The table is printed whatever the verdict: a green run is evidence too, and a
124
+ # red one is only actionable with its rows in the job log.
125
+ set +e
126
+ "$ENGINE" "$SERVICE_ROOT" --workspace "$WORKSPACE_ROOT"
127
+ ENGINE_RC=$?
128
+ set -e
129
+
130
+ if [ "$ENGINE_RC" -ne 0 ]; then
131
+ echo "" >&2
132
+ die "Refused - this service does not wear the uniform (engine exit $ENGINE_RC); the table above lists every row that found something, with the fix for each. Expected: no finding of a severity that blocks. Fix: follow the fix column in this repository, push, and deploy the commit that is green." 1
133
+ fi
134
+
135
+ # --- completeness -----------------------------------------------------------
136
+ #
137
+ # `deployable` says nothing blocked among the rows that RAN; `complete` says no
138
+ # row that CAN block was skipped. Reading only the first would honour a skip
139
+ # nobody flagged (`automation-gates.md` §1.5). The signal file is transport
140
+ # inside this job and nothing else: it carries no sha, so a file found in a
141
+ # working tree proves nothing about which tree it measured (008 point 1).
142
+ SIGNAL="$SERVICE_ROOT/ci/deployability.json"
143
+ if [ ! -f "$SIGNAL" ]; then
144
+ die "Missing signal - the run finished but wrote no $SIGNAL, so whether it looked everywhere is unknown, and an unanswered question is not a pass. Fix: run '$ENGINE $SERVICE_ROOT --workspace $WORKSPACE_ROOT' by hand and read the error it prints." 2
145
+ fi
146
+
147
+ set +e
148
+ VERDICT=$(node -e '
149
+ const fs = require("fs");
150
+ let signal;
151
+ try {
152
+ signal = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
153
+ } catch (error) {
154
+ process.stderr.write(error.message + "\n");
155
+ process.exit(2);
156
+ }
157
+ if (signal.complete !== true) {
158
+ for (const row of signal.notRun || []) {
159
+ process.stdout.write(` ${row.id} ${row.reason}\n`);
160
+ }
161
+ process.exit(3);
162
+ }
163
+ if (signal.deployable !== true) process.exit(4);
164
+ process.exit(0);
165
+ ' "$SIGNAL")
166
+ SIGNAL_RC=$?
167
+ set -e
168
+
169
+ case "$SIGNAL_RC" in
170
+ 0) ;;
171
+ 2) die "Cannot read the signal - $SIGNAL is not readable JSON, so whether the run looked everywhere is unknown. Fix: delete it and run '$ENGINE $SERVICE_ROOT --workspace $WORKSPACE_ROOT' again." 2 ;;
172
+ 3)
173
+ echo "" >&2
174
+ echo "$VERDICT" >&2
175
+ die "Refused - the run was incomplete: the rows above could not look, and a run that answered part of the manifest proves nothing about the rest. Expected: complete=true in $SIGNAL. Fix: give the run the layout it names - the api checkout beside this one under <root>/api_biz/<service>." 1 ;;
176
+ 4) die "Refused - the engine reported no blocking finding and the signal says this service is not deployable; the two disagree, and the refusing answer is the one this step takes. Fix: read $SIGNAL and the table above." 1 ;;
177
+ *) die "Cannot read the signal - reading $SIGNAL ended with exit $SIGNAL_RC, which this step has no meaning for. Fix: run '$ENGINE $SERVICE_ROOT --workspace $WORKSPACE_ROOT' by hand and read what it prints." 2 ;;
178
+ esac
179
+
180
+ echo "[$CONTEXT] DEPLOYABLE - the uniform run over this checkout is complete and carries no blocking finding."
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Echo handler (v3 invocation model) — the example operation this template
5
+ * ships so a new service starts from a shape that already works.
6
+ *
7
+ * A biz service binds no port (ADR 0005): an operation is a plain exported
8
+ * function, reached over MQ in production and called directly by the Tier-1
9
+ * runner. There is no route, no request and no response object here.
10
+ *
11
+ * The behaviour is deliberately total and deterministic — the same input
12
+ * always yields the same output — so the cookbook beside it can assert a
13
+ * CONCRETE VALUE rather than that something truthy came back
14
+ * (.claude/rules/architecture-principles.md §10a).
15
+ *
16
+ * Input shape is guaranteed by SchemaValidator against the `input` schema in
17
+ * config/service/operations.json, so this function does not re-check it.
18
+ *
19
+ * @see api/docs/biz/10-invocation/handler-dispatch.md
20
+ * @see api/docs/biz/30-operations/schema-v3.md § handler
21
+ *
22
+ * @param {{text: string}} input - validated against the operation's input schema
23
+ * @param {Object} ctx - built by ContextBuilder in production, by the Tier-1 runner in validation
24
+ * @returns {Promise<{text: string, length: number}>}
25
+ */
26
+ async function echo(input, ctx) {
27
+ const { text } = input;
28
+ const { logger, operation_name, correlation_id } = ctx;
29
+
30
+ logger.info('returning submitted text unchanged', {
31
+ length: text.length,
32
+ operation_name,
33
+ correlation_id
34
+ });
35
+
36
+ return { text, length: text.length };
37
+ }
38
+
39
+ module.exports = { echo };
@@ -0,0 +1,36 @@
1
+ {
2
+ "version": "2.1.0",
3
+ "name": "echo",
4
+ "description": "Tier-1 cookbook for the example echo operation: the submitted text comes back unchanged, with its length.",
5
+ "test": {
6
+ "mode": "offline",
7
+ "timeout": 5000
8
+ },
9
+ "steps": [
10
+ {
11
+ "step_id": "echo_text",
12
+ "type": "task",
13
+ "service": "__REPO_NAME__",
14
+ "operation": "echo",
15
+ "input": {
16
+ "text": "cookbook"
17
+ },
18
+ "expect": {
19
+ "status": "success",
20
+ "output": {
21
+ "text": {
22
+ "type": "string",
23
+ "equals": "cookbook"
24
+ },
25
+ "length": {
26
+ "type": "number",
27
+ "equals": 8
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ],
33
+ "delivery": {
34
+ "handler": "none"
35
+ }
36
+ }
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ // Unit test for the example v3 handler this template ships. A biz service has
4
+ // no app to mount and no port to bind (ADR 0005): a handler is a plain
5
+ // function, so its test requires the module and calls it directly — no
6
+ // supertest, no HTTP, no DB.
7
+ //
8
+ // Replace this file when you replace src/handlers/v3/echo.js with your own
9
+ // operation. Keep the shape: assert CONCRETE VALUES, cover the failure path,
10
+ // and keep a control case that must not move
11
+ // (.claude/rules/architecture-principles.md §10a).
12
+
13
+ const { echo } = require('../../src/handlers/v3/echo');
14
+
15
+ /** The ctx slots the Tier-1 runner and ContextBuilder both fill. */
16
+ function makeCtx(overrides = {}) {
17
+ const lines = [];
18
+ return {
19
+ ctx: {
20
+ logger: { info: (msg, meta) => lines.push({ msg, meta }), warn: () => {}, error: () => {} },
21
+ operation_name: 'echo',
22
+ correlation_id: 'test-correlation-id',
23
+ ...overrides
24
+ },
25
+ lines
26
+ };
27
+ }
28
+
29
+ describe('echo handler @unit', () => {
30
+ it('returns the submitted text unchanged, with its length', async () => {
31
+ const { ctx } = makeCtx();
32
+
33
+ const result = await echo({ text: 'cookbook' }, ctx);
34
+
35
+ expect(result).toEqual({ text: 'cookbook', length: 8 });
36
+ });
37
+
38
+ it('counts an empty string as length 0 rather than reporting nothing', async () => {
39
+ const { ctx } = makeCtx();
40
+
41
+ const result = await echo({ text: '' }, ctx);
42
+
43
+ expect(result).toEqual({ text: '', length: 0 });
44
+ });
45
+
46
+ it('logs the length it computed, on the injected logger', async () => {
47
+ const { ctx, lines } = makeCtx();
48
+
49
+ await echo({ text: 'abc' }, ctx);
50
+
51
+ expect(lines).toHaveLength(1);
52
+ expect(lines[0].meta).toEqual({
53
+ length: 3,
54
+ operation_name: 'echo',
55
+ correlation_id: 'test-correlation-id'
56
+ });
57
+ });
58
+
59
+ // Failure path: ctx is injected, never defaulted. A handler that fell back to
60
+ // console here would log outside the service's own stream and the caller
61
+ // would never learn the ctx was wrong (architecture-principles.md §3).
62
+ it('fails loudly when ctx carries no logger, instead of falling back', async () => {
63
+ await expect(echo({ text: 'abc' }, {})).rejects.toThrow(TypeError);
64
+ });
65
+
66
+ // Control: the handler declares no state, so a second call with the same
67
+ // input returns the same value. This must not change when the operation
68
+ // beside it does.
69
+ it('CONTROL: is deterministic — the same input twice gives the same output', async () => {
70
+ const { ctx } = makeCtx();
71
+
72
+ const first = await echo({ text: 'same' }, ctx);
73
+ const second = await echo({ text: 'same' }, ctx);
74
+
75
+ expect(second).toEqual(first);
76
+ expect(first).toEqual({ text: 'same', length: 4 });
77
+ });
78
+ });