@sdd-method/author-cli 0.0.0-stage → 0.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 (203) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +6 -2
  4. package/dist/author-bin.js +8 -0
  5. package/dist/author-program.js +99 -0
  6. package/dist/lib/adr-origin.js +59 -0
  7. package/dist/lib/agent-harness.js +37 -0
  8. package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
  9. package/dist/lib/api-surface/analyzers/library.js +95 -0
  10. package/dist/lib/api-surface/analyzers/serverless.js +336 -0
  11. package/dist/lib/api-surface/analyzers/spring.js +237 -0
  12. package/dist/lib/api-surface/analyzers/terraform.js +109 -0
  13. package/dist/lib/api-surface/detect.js +25 -0
  14. package/dist/lib/api-surface/emit.js +109 -0
  15. package/dist/lib/api-surface/file-set.js +43 -0
  16. package/dist/lib/api-surface/index.js +134 -0
  17. package/dist/lib/api-surface/types.js +17 -0
  18. package/dist/lib/briefing/index.js +13 -0
  19. package/dist/lib/briefing/render.js +132 -0
  20. package/dist/lib/briefing/variables.js +92 -0
  21. package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
  22. package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
  23. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
  24. package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
  25. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
  26. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
  27. package/dist/lib/catalogue/archetype/resolver.js +103 -0
  28. package/dist/lib/catalogue/archetype/wireframe.js +418 -0
  29. package/dist/lib/catalogue/baseline-check.js +151 -0
  30. package/dist/lib/catalogue/build.js +1502 -0
  31. package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
  32. package/dist/lib/catalogue/builders/adrs.js +222 -0
  33. package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
  34. package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
  35. package/dist/lib/catalogue/builders/c4-components.js +229 -0
  36. package/dist/lib/catalogue/builders/c4-containers.js +240 -0
  37. package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
  38. package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
  39. package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
  40. package/dist/lib/catalogue/builders/c4-systems.js +98 -0
  41. package/dist/lib/catalogue/builders/capabilities.js +488 -0
  42. package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
  43. package/dist/lib/catalogue/builders/capability-edges.js +256 -0
  44. package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
  45. package/dist/lib/catalogue/builders/capability-spec.js +348 -0
  46. package/dist/lib/catalogue/builders/container-dependency.js +183 -0
  47. package/dist/lib/catalogue/builders/contracts.js +402 -0
  48. package/dist/lib/catalogue/builders/data-items.js +344 -0
  49. package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
  50. package/dist/lib/catalogue/builders/domain-models.js +175 -0
  51. package/dist/lib/catalogue/builders/domains.js +119 -0
  52. package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
  53. package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
  54. package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
  55. package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
  56. package/dist/lib/catalogue/builders/entity-slug.js +47 -0
  57. package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
  58. package/dist/lib/catalogue/builders/event-edges.js +327 -0
  59. package/dist/lib/catalogue/builders/events.js +168 -0
  60. package/dist/lib/catalogue/builders/evidence-class.js +43 -0
  61. package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
  62. package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
  63. package/dist/lib/catalogue/builders/feature-contract.js +77 -0
  64. package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
  65. package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
  66. package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
  67. package/dist/lib/catalogue/builders/feature-groups.js +63 -0
  68. package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
  69. package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
  70. package/dist/lib/catalogue/builders/features.js +300 -0
  71. package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
  72. package/dist/lib/catalogue/builders/glossary.js +204 -0
  73. package/dist/lib/catalogue/builders/implementations.js +214 -0
  74. package/dist/lib/catalogue/builders/index.js +64 -0
  75. package/dist/lib/catalogue/builders/integration-components.js +344 -0
  76. package/dist/lib/catalogue/builders/integrations.js +181 -0
  77. package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
  78. package/dist/lib/catalogue/builders/persona-container.js +133 -0
  79. package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
  80. package/dist/lib/catalogue/builders/personas.js +152 -0
  81. package/dist/lib/catalogue/builders/product-edges.js +106 -0
  82. package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
  83. package/dist/lib/catalogue/builders/products.js +162 -0
  84. package/dist/lib/catalogue/builders/quality-scores.js +635 -0
  85. package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
  86. package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
  87. package/dist/lib/catalogue/builders/repo-edges.js +340 -0
  88. package/dist/lib/catalogue/builders/repositories.js +458 -0
  89. package/dist/lib/catalogue/builders/scaffolds.js +197 -0
  90. package/dist/lib/catalogue/builders/screen-tree.js +222 -0
  91. package/dist/lib/catalogue/builders/screens.js +396 -0
  92. package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
  93. package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
  94. package/dist/lib/catalogue/builders/service-contract.js +74 -0
  95. package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
  96. package/dist/lib/catalogue/builders/services.js +232 -0
  97. package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
  98. package/dist/lib/catalogue/builders/user-journeys.js +253 -0
  99. package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
  100. package/dist/lib/catalogue/cli-version.js +10 -0
  101. package/dist/lib/catalogue/contract-id.js +49 -0
  102. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  103. package/dist/lib/catalogue/coverage-render.js +552 -0
  104. package/dist/lib/catalogue/coverage-run.js +115 -0
  105. package/dist/lib/catalogue/csv-reader.js +97 -0
  106. package/dist/lib/catalogue/csv-writer.js +96 -0
  107. package/dist/lib/catalogue/data-plane-coherence.js +77 -0
  108. package/dist/lib/catalogue/ddl-generator.js +233 -0
  109. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  110. package/dist/lib/catalogue/engine-validator.js +239 -0
  111. package/dist/lib/catalogue/ext-id.js +76 -0
  112. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
  113. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  114. package/dist/lib/catalogue/finding-identity.js +183 -0
  115. package/dist/lib/catalogue/findings.js +46 -0
  116. package/dist/lib/catalogue/fk-validator.js +136 -0
  117. package/dist/lib/catalogue/freshness.js +70 -0
  118. package/dist/lib/catalogue/gate-checks.js +338 -0
  119. package/dist/lib/catalogue/graph-projection.js +346 -0
  120. package/dist/lib/catalogue/impact-run.js +228 -0
  121. package/dist/lib/catalogue/manifest-loader.js +435 -0
  122. package/dist/lib/catalogue/platform-scope.js +7 -0
  123. package/dist/lib/catalogue/population-checks.js +87 -0
  124. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  125. package/dist/lib/catalogue/reverse-product-quality.js +99 -0
  126. package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
  127. package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
  128. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  129. package/dist/lib/catalogue/schema-compat.js +52 -0
  130. package/dist/lib/catalogue/schema-loader.js +87 -0
  131. package/dist/lib/catalogue/scores-coverage.js +573 -0
  132. package/dist/lib/catalogue/screen-validation-loader.js +89 -0
  133. package/dist/lib/catalogue/service-architecture-parser.js +193 -0
  134. package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
  135. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  136. package/dist/lib/catalogue/traceability-loader.js +350 -0
  137. package/dist/lib/catalogue/validate.js +200 -0
  138. package/dist/lib/catalogue/version.js +18 -0
  139. package/dist/lib/conformance/binding.js +70 -0
  140. package/dist/lib/conformance/finding.js +69 -0
  141. package/dist/lib/conformance/index.js +578 -0
  142. package/dist/lib/context-pull/index.js +191 -0
  143. package/dist/lib/context-pull/resolve.js +70 -0
  144. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  145. package/dist/lib/contracts-availability/index.js +267 -0
  146. package/dist/lib/generate-briefing/index.js +77 -0
  147. package/dist/lib/list/discover.js +100 -0
  148. package/dist/lib/list/format.js +84 -0
  149. package/dist/lib/list/index.js +16 -0
  150. package/dist/lib/lock-file.js +437 -0
  151. package/dist/lib/pack/log.js +18 -0
  152. package/dist/lib/pack/schema.js +121 -0
  153. package/dist/lib/pack/seed.js +182 -0
  154. package/dist/lib/pack/validate.js +49 -0
  155. package/dist/lib/package-version.js +92 -0
  156. package/dist/lib/plan/index.js +69 -0
  157. package/dist/lib/plan/read-sdd.js +102 -0
  158. package/dist/lib/plan/render-template.js +84 -0
  159. package/dist/lib/repo-kind.js +129 -0
  160. package/dist/lib/scaffold/domain.js +234 -0
  161. package/dist/lib/scaffold/registry.js +136 -0
  162. package/dist/lib/scripts.js +7 -0
  163. package/dist/lib/sdd-index/repo-config.js +245 -0
  164. package/dist/lib/sdd-reader.js +91 -0
  165. package/dist/lib/snapshot/advance.js +454 -0
  166. package/dist/lib/snapshot/diff.js +190 -0
  167. package/dist/lib/snapshot/fold.js +208 -0
  168. package/dist/lib/snapshot/land.js +178 -0
  169. package/dist/lib/snapshot/manifest.js +206 -0
  170. package/dist/lib/snapshot/targets.js +169 -0
  171. package/dist/lib/snapshot/verify.js +91 -0
  172. package/dist/lib/sync/briefing-merge.js +106 -0
  173. package/dist/lib/sync/bundle-cache.js +109 -0
  174. package/dist/lib/sync/yaml-block-merge.js +120 -0
  175. package/dist/lib/validate/aggregate.js +51 -0
  176. package/dist/lib/validate/bundle-coherence.js +74 -0
  177. package/dist/lib/validate/dispatch.js +243 -0
  178. package/dist/lib/validate/index.js +110 -0
  179. package/dist/lib/validate/plan.js +54 -0
  180. package/dist/lib/validate/runner.js +85 -0
  181. package/dist/lib/validate/yaml-parse.js +112 -0
  182. package/dist/maintain-version.json +4 -0
  183. package/dist/verbs/api-surface.js +71 -0
  184. package/dist/verbs/catalogue-check.js +28 -0
  185. package/dist/verbs/catalogue-verify.js +114 -0
  186. package/dist/verbs/conformance.js +34 -0
  187. package/dist/verbs/context.js +39 -0
  188. package/dist/verbs/contracts.js +34 -0
  189. package/dist/verbs/generate-briefing.js +29 -0
  190. package/dist/verbs/list.js +45 -0
  191. package/dist/verbs/pack-validate.js +17 -0
  192. package/dist/verbs/path-option.js +25 -0
  193. package/dist/verbs/plan.js +42 -0
  194. package/dist/verbs/render.js +87 -0
  195. package/dist/verbs/scaffold.js +83 -0
  196. package/dist/verbs/snapshot.js +134 -0
  197. package/dist/verbs/validate.js +48 -0
  198. package/dist/verify-program.js +122 -0
  199. package/dist/verify-version.json +4 -0
  200. package/package.json +32 -4
  201. package/scripts/validate-app-dependencies.sh +664 -0
  202. package/scripts/validate-app-docs.sh +199 -0
  203. package/scripts/validate-integration-docs.sh +505 -0
@@ -0,0 +1,664 @@
1
+ #!/bin/bash
2
+ #
3
+ # validate-app-dependencies.sh - Validate app platform dependency declarations
4
+ #
5
+ # Usage:
6
+ # ./validate-app-dependencies.sh --app-root <path> [--platform-root <path>]
7
+ #
8
+ # --platform-root is OPTIONAL. Without it the checks that need no platform surface
9
+ # still run, and the ones that do are reported as NOT RUN with their denominator —
10
+ # a partial run, never a met gate. See the block above the flag parsing.
11
+ #
12
+ # Checks (for each entry in app-platform-dependencies.yaml), split by whether they
13
+ # need a platform surface at all — LOCAL checks run wherever the app repo is:
14
+ # 1. (local) required_stability is a valid value (provisional | locked | unknown)
15
+ # 2a. (local) service_id is declared and non-empty
16
+ # 2b. (platform) service_id resolves against the platform source of truth:
17
+ # catalogues/services.csv (exact id match) when the catalogue exists,
18
+ # else docs/platform/service-architecture.md (word-anchored match)
19
+ # 3a. (local) a `locked` dependency declares a contract_path at all
20
+ # 3b. (platform) that contract_path resolves to a file under the platform root
21
+ #
22
+ # Outputs a dependency status table.
23
+ # Used as the Stage 6 lock-in gate in the App Exploration Workflow.
24
+ #
25
+ # THE STABILITY VOCABULARY, AND WHY IT IS WHAT IT IS
26
+ # `required_stability` states the CONSUMING app's expectation of contract
27
+ # stability. Three values express that:
28
+ #
29
+ # provisional the contract may still change; the app tolerates it
30
+ # locked the contract is fixed; a contract_path must exist (check 3)
31
+ # unknown the expectation is not determined
32
+ #
33
+ # `unknown` is accepted because the canonical schema's
34
+ # product_platform_dependencies table and the distributed reverse template
35
+ # both offer it, and this gate was the only surface rejecting it — an adopter
36
+ # filling in the method's own template produced a file the method's own gate
37
+ # hard-failed. Reverse-derived declarations legitimately cannot always
38
+ # determine the expectation, and naming that is better than encoding it as
39
+ # silence (M-ADR 0196). It does not require a contract_path.
40
+ #
41
+ # `external` and `legacy` are NOT stability values and are reported as
42
+ # warnings, not failures. They answer different questions — where the target
43
+ # lives, and what lifecycle its implementation is in — on a field that asks
44
+ # how stable the consumer needs the contract to be. A third-party API can be
45
+ # more stable than an internal one, so the two axes are orthogonal. They are
46
+ # warned rather than failed because they encode real information for which no
47
+ # replacement field exists yet: an `external` target is a declaration that the
48
+ # dependency does not resolve against platform services at all, which is the
49
+ # method's external-system concept (M-ADR 0189), not a stability claim. Failing
50
+ # them before that route exists would force an adopter to discard the
51
+ # information to get green. They become failures when the locus field ships.
52
+
53
+ set -euo pipefail
54
+
55
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
56
+ ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
57
+
58
+ RED='\033[0;31m'
59
+ GREEN='\033[0;32m'
60
+ YELLOW='\033[1;33m'
61
+ BLUE='\033[0;34m'
62
+ NC='\033[0m'
63
+
64
+ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
65
+ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
66
+ log_fail() { echo -e "${RED}[FAIL]${NC} $1"; }
67
+ log_pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
68
+ # R1: an empty population is "measured nothing", not a pass — the [N/A] tag
69
+ # must start the line (no leading colour escape) so callers can detect it.
70
+ log_na() { echo -e "[N/A] $1"; }
71
+ log_section() {
72
+ echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}"
73
+ echo -e "${BLUE} $1${NC}"
74
+ echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n"
75
+ }
76
+
77
+ APP_ROOT=""
78
+ PLATFORM_ROOT=""
79
+
80
+ usage() {
81
+ cat <<USAGE
82
+ Usage: $(basename "$0") --app-root <path> [--platform-root <path>]
83
+
84
+ Options:
85
+ --app-root Path to the app repository root (must contain app-platform-dependencies.yaml)
86
+ --platform-root OPTIONAL. Path to the platform repository root (service_id checks use
87
+ catalogues/services.csv when present, else docs/platform/service-architecture.md).
88
+ Omitted: the stability vocabulary and the local halves of the
89
+ service_id/contract_path checks run; the resolutions are reported
90
+ as not run. The gate is not met by a partial run.
91
+ USAGE
92
+ }
93
+
94
+ while [[ $# -gt 0 ]]; do
95
+ case "$1" in
96
+ --app-root)
97
+ APP_ROOT="${2:-}"
98
+ shift 2
99
+ ;;
100
+ --platform-root)
101
+ PLATFORM_ROOT="${2:-}"
102
+ shift 2
103
+ ;;
104
+ -h|--help)
105
+ usage
106
+ exit 0
107
+ ;;
108
+ *)
109
+ log_fail "Unknown argument: $1"
110
+ usage
111
+ exit 1
112
+ ;;
113
+ esac
114
+ done
115
+
116
+ if [[ -z "$APP_ROOT" ]]; then
117
+ log_fail "--app-root is required"
118
+ usage
119
+ exit 1
120
+ fi
121
+
122
+ if [[ ! -d "$APP_ROOT" ]]; then
123
+ log_fail "App root not found: $APP_ROOT"
124
+ exit 1
125
+ fi
126
+
127
+ # --platform-root is OPTIONAL, and this is the whole of why:
128
+ #
129
+ # Only checks 2 and 3 need a platform surface. Check 1 is local to the app repo.
130
+ # While the flag was mandatory, an app repo standing alone could run NONE of the
131
+ # three — the validator was skipped wholesale by the dispatcher, so it had never
132
+ # executed in any of the 70 dependency files across two estates, and the suite
133
+ # reported "All validators passed" over the skip.
134
+ #
135
+ # Without a platform root the checks that CAN run, run, and the checks that
136
+ # cannot are named as not-run with their denominator. Absent evidence is
137
+ # reported as absent, never as a pass (M-ADR 0200 §4.3) — the same treatment
138
+ # validate-gate-readiness.sh gives an empty registry.
139
+ PLATFORM_AVAILABLE=1
140
+ if [[ -z "$PLATFORM_ROOT" ]]; then
141
+ PLATFORM_AVAILABLE=0
142
+ elif [[ ! -d "$PLATFORM_ROOT" ]]; then
143
+ # An explicitly-given root that does not exist is an error, not a downgrade:
144
+ # the caller asserted a platform surface that is not there.
145
+ log_fail "Platform root not found: $PLATFORM_ROOT"
146
+ exit 1
147
+ fi
148
+
149
+ APP_DEPS_FILE="$APP_ROOT/app-platform-dependencies.yaml"
150
+
151
+ # TWO AUTHORING SOURCES, NOT ONE.
152
+ # The catalogue builder does multi-source ingestion under the ADR 0157 parity
153
+ # principle: `app-platform-dependencies.yaml` AND every
154
+ # `reverse-evidence/**/feature-grounding-evidence.yaml`'s platform_dependencies
155
+ # block, merged by primary key with forward winning. They are equal-status
156
+ # authoring surfaces.
157
+ #
158
+ # This gate read only the forward file, so an entire equal-status source went
159
+ # ungated. Measured in one reverse estate: 15 evidence files declaring 65
160
+ # further dependencies, 38 of them `locked` — precisely the lock-in population
161
+ # a Stage 6 lock-in gate exists to check — none of which this gate could see.
162
+ #
163
+ # A source that the catalogue reads and the gate does not is a coverage gap by
164
+ # construction, so the gate now reads both.
165
+ DEP_SOURCES=()
166
+ if [[ -f "$APP_DEPS_FILE" ]]; then
167
+ DEP_SOURCES+=("$APP_DEPS_FILE")
168
+ fi
169
+ if [[ -d "$APP_ROOT/reverse-evidence" ]]; then
170
+ while IFS= read -r evidence_file; do
171
+ [[ -n "$evidence_file" ]] || continue
172
+ DEP_SOURCES+=("$evidence_file")
173
+ done < <(find "$APP_ROOT/reverse-evidence" -name "feature-grounding-evidence.yaml" -type f | sort)
174
+ fi
175
+ PLATFORM_SERVICE_ARCH="$PLATFORM_ROOT/docs/platform/service-architecture.md"
176
+ PLATFORM_SERVICES_CSV="$PLATFORM_ROOT/catalogues/services.csv"
177
+
178
+ # One value per line: this array is the enforcement point, and the correspondence
179
+ # registry reads it line-wise to compare against what the distributed templates
180
+ # offer (instances app-dependency-stability-vocabulary-forward and -reverse).
181
+ VALID_STABILITY_VALUES=(
182
+ "provisional" # contract may still change; the app tolerates it
183
+ "locked" # contract is fixed; check 3 requires a resolvable contract_path
184
+ "unknown" # expectation not determined; no contract_path required
185
+ )
186
+
187
+ # M-ADR 0204: where the dependency's TARGET lives. One value per line — the
188
+ # correspondence registry reads this array line-wise against what the
189
+ # distributed templates offer, exactly as it does VALID_STABILITY_VALUES.
190
+ # Single-quoted deliberately: the correspondence extractor for the STABILITY
191
+ # array captures `^ "value"` lines, and two enums sharing one line shape
192
+ # would pollute each other's claimed sets — measured, not hypothetical.
193
+ VALID_TARGET_LOCUS_VALUES=(
194
+ 'platform' # the default when absent; the row is fully platform-validated
195
+ 'external' # third-party target; platform resolution + realiser join skipped, named
196
+ 'legacy' # off-platform predecessor system; same skips, different lifecycle
197
+ )
198
+
199
+ # The pre-0204 improvisation. These were WARNED while no locus field existed;
200
+ # M-ADR 0204 §4.4 flips them to FAILURES now that it does — the message names
201
+ # the home for what they meant.
202
+ DEPRECATED_STABILITY_VALUES=("external" "legacy")
203
+
204
+ # The M-ADR 0192 async set. A shared-stream subscription has NO single realiser
205
+ # service, so it binds to the stream contract plus the publishing capability
206
+ # instead — the canonical schema makes product_platform_dependencies.
207
+ # target_service_id conditionally nullable for exactly these contract types, and
208
+ # states that a synchronous contract_type still requires it.
209
+ #
210
+ # This gate did not know that. Check 2a hard-failed "service_id is empty or
211
+ # missing" on every async entry, which is the shape both distributed templates
212
+ # instruct an author to write (they say to OMIT service_id). Found the moment the
213
+ # check first ran: 3 of the reverse estate's 48 dependency files, all three async.
214
+ ASYNC_CONTRACT_TYPES=("event" "event-stream" "stream")
215
+
216
+ FAILURES=0
217
+ WARNINGS=0
218
+ TOTAL_DEPS=0
219
+ PASSED_DEPS=0
220
+
221
+ # What could not be checked for want of a platform surface. Counted, not ignored:
222
+ # the summary states these as a denominator so a run without a platform root can
223
+ # never read as a clean bill of health on the resolution checks.
224
+ UNPARSEABLE_SOURCES=0
225
+ SOURCE_DEP_COUNT=0
226
+ UNRESOLVED_SERVICE_IDS=0
227
+ UNRESOLVED_CONTRACT_PATHS=0
228
+
229
+ # ──────────────────────────────────────────────────────────────────────────────
230
+ # R1: with zero declared dependencies the run measures nothing, so the
231
+ # prerequisite checks report as [INFO] rather than [PASS] — a [PASS] line would
232
+ # make the empty run read as a pass. Counted up front, quietly; the real count
233
+ # (with parse-failure reporting) happens in main.
234
+ PRE_DEP_COUNT=0
235
+ log_prereq() {
236
+ if [[ "$PRE_DEP_COUNT" -gt 0 ]]; then log_pass "$1"; else log_info "$1"; fi
237
+ }
238
+
239
+ check_prerequisites() {
240
+ log_section "Prerequisites"
241
+
242
+ if command -v yq &>/dev/null; then
243
+ local _src
244
+ for _src in "${DEP_SOURCES[@]+"${DEP_SOURCES[@]}"}"; do
245
+ PRE_DEP_COUNT=$((PRE_DEP_COUNT + $(quiet_dep_count "$_src")))
246
+ done
247
+ fi
248
+
249
+ if [[ ${#DEP_SOURCES[@]} -eq 0 ]]; then
250
+ log_fail "no dependency source found: neither $APP_DEPS_FILE nor any reverse-evidence/**/feature-grounding-evidence.yaml"
251
+ exit 1
252
+ fi
253
+ for src in "${DEP_SOURCES[@]}"; do
254
+ log_prereq "dependency source: ${src#"$APP_ROOT"/}"
255
+ done
256
+
257
+ if ! command -v yq &>/dev/null; then
258
+ log_fail "yq is required but not installed. Install with: brew install yq"
259
+ exit 1
260
+ fi
261
+ log_prereq "yq available"
262
+
263
+ if [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
264
+ PLATFORM_SERVICES_CSV=""
265
+ PLATFORM_SERVICE_ARCH=""
266
+ log_info "No --platform-root given: check 1 (stability vocabulary) runs; checks 2 and 3 do not."
267
+ return 0
268
+ fi
269
+
270
+ if [[ -f "$PLATFORM_SERVICES_CSV" ]]; then
271
+ log_prereq "Platform services catalogue found (canonical service_id source)"
272
+ else
273
+ PLATFORM_SERVICES_CSV=""
274
+ fi
275
+
276
+ if [[ ! -f "$PLATFORM_SERVICE_ARCH" ]]; then
277
+ log_warn "Platform service-architecture.md not found: $PLATFORM_SERVICE_ARCH"
278
+ PLATFORM_SERVICE_ARCH=""
279
+ else
280
+ log_prereq "Platform service-architecture.md found"
281
+ fi
282
+
283
+ if [[ -z "$PLATFORM_SERVICES_CSV" && -z "$PLATFORM_SERVICE_ARCH" ]]; then
284
+ log_warn "Neither services catalogue nor service-architecture.md found — service_id validation will be skipped"
285
+ fi
286
+ }
287
+
288
+ # Is this value shaped like a path, as opposed to a sentinel or a prose note?
289
+ # Mirrors isPathShaped() in the CLI's contract-path-resolution.ts, which applies
290
+ # the same test at aggregation tier — one rule, asserted at both tiers.
291
+ is_path_shaped() {
292
+ local v="$1"
293
+ [[ -n "$v" && "$v" != "null" ]] || return 1
294
+ [[ "${v,,}" != n/a* ]] || return 1
295
+ [[ "${v:0:1}" != "[" ]] || return 1
296
+ [[ "$v" == */* ]] || return 1
297
+ # Prose gives itself away with whitespace before the first separator.
298
+ local first="${v%%/*}"
299
+ [[ "$first" != *[[:space:]]* ]] || return 1
300
+ return 0
301
+ }
302
+
303
+ # Word-anchored match: the id must not be a substring of a longer slug token.
304
+ service_id_in_doc() {
305
+ local service_id="$1"
306
+ grep -qE "(^|[^a-zA-Z0-9-])${service_id}([^a-zA-Z0-9-]|$)" "$PLATFORM_SERVICE_ARCH" 2>/dev/null
307
+ }
308
+
309
+ service_id_in_catalogue() {
310
+ local service_id="$1"
311
+ awk -F',' -v id="$service_id" 'NR>1 && $1==id {found=1} END {exit !found}' "$PLATFORM_SERVICES_CSV" 2>/dev/null
312
+ }
313
+
314
+ # ──────────────────────────────────────────────────────────────────────────────
315
+ # How many platform_dependencies entries does this source declare? Zero for a
316
+ # source that has no such block, and zero — with a reported finding — for one that
317
+ # cannot be parsed. An unparseable evidence file is NOT silently treated as empty:
318
+ # measured in one estate, a single unquoted colon made a 206-line authored file
319
+ # invisible, taking its dependency declarations with it.
320
+ # Sets SOURCE_DEP_COUNT, and increments UNPARSEABLE_SOURCES on a broken source.
321
+ #
322
+ # Deliberately NOT a function that echoes its answer: a command substitution runs
323
+ # in a subshell, so the counter increment would be discarded and the log line
324
+ # would be captured as part of the count. Both happened; both are silent in bash.
325
+ read_source_dep_count() {
326
+ local source_file="$1"
327
+ if ! SOURCE_DEP_COUNT="$(yq e ".platform_dependencies | length" "$source_file" 2>/dev/null)"; then
328
+ log_fail "${source_file#"$APP_ROOT"/}: cannot be parsed as YAML — every dependency it declares is invisible to this gate and to the catalogue"
329
+ UNPARSEABLE_SOURCES=$((UNPARSEABLE_SOURCES + 1))
330
+ SOURCE_DEP_COUNT=0
331
+ return
332
+ fi
333
+ [[ "$SOURCE_DEP_COUNT" =~ ^[0-9]+$ ]] || SOURCE_DEP_COUNT=0
334
+ }
335
+
336
+ # Quiet variant for the display pass — the failure was already reported when the
337
+ # sources were counted; reporting it twice would double-count nothing useful.
338
+ quiet_dep_count() {
339
+ local source_file="$1" count
340
+ count="$(yq e ".platform_dependencies | length" "$source_file" 2>/dev/null || echo 0)"
341
+ [[ "$count" =~ ^[0-9]+$ ]] || count=0
342
+ echo "$count"
343
+ }
344
+
345
+ validate_dependency() {
346
+ local source_file="$1"
347
+ local index="$2"
348
+ local dep_errors=0
349
+
350
+ local capability_id service_id contract_type required_stability contract_path
351
+
352
+ capability_id="$(yq e ".platform_dependencies[$index].capability_id // \"\"" "$source_file")"
353
+ service_id="$(yq e ".platform_dependencies[$index].service_id // \"\"" "$source_file")"
354
+ contract_type="$(yq e ".platform_dependencies[$index].contract_type // \"\"" "$source_file")"
355
+ required_stability="$(yq e ".platform_dependencies[$index].required_stability // \"\"" "$source_file")"
356
+ contract_path="$(yq e ".platform_dependencies[$index].contract_path // \"\"" "$source_file")"
357
+
358
+ local dep_label="${service_id:-dep[$index]}"
359
+ if [[ -n "$capability_id" && "$capability_id" != "null" ]]; then
360
+ dep_label="$capability_id / $service_id"
361
+ fi
362
+
363
+ # M-ADR 0204: target locus. Absent = platform. Invalid FAILS — this field is
364
+ # new, has no legacy corpus, and a tolerated typo would silently exempt the
365
+ # row from every platform check.
366
+ local target_locus
367
+ target_locus="$(yq e ".platform_dependencies[$index].target_locus // \"\"" "$source_file")"
368
+ [[ "$target_locus" == "null" ]] && target_locus=""
369
+ if [[ -z "$target_locus" ]]; then
370
+ target_locus="platform"
371
+ elif [[ " ${VALID_TARGET_LOCUS_VALUES[*]} " != *" $target_locus "* ]]; then
372
+ log_fail "$dep_label: invalid target_locus '$target_locus' (allowed: ${VALID_TARGET_LOCUS_VALUES[*]})"
373
+ ((dep_errors++))
374
+ target_locus="platform"
375
+ fi
376
+
377
+ # Check 1: required_stability is a valid value. Needs NO platform surface —
378
+ # it is the only check that can run in an app repo standing alone.
379
+ local stability_valid=0
380
+ for v in "${VALID_STABILITY_VALUES[@]}"; do
381
+ if [[ "$required_stability" == "$v" ]]; then
382
+ stability_valid=1
383
+ break
384
+ fi
385
+ done
386
+
387
+ if [[ "$stability_valid" -eq 0 && ( -z "$required_stability" || "$required_stability" == "null" ) ]]; then
388
+ # ABSENT is not INVALID. The canonical schema declares
389
+ # required_stability nullable on both tables that carry it, so an entry
390
+ # without one is schema-legal and the gate must not contradict its own
391
+ # schema — that is exactly the defect the `unknown` alignment fixed.
392
+ # Reported all the same: M-ADR 0196 prefers a named gap to a silent one,
393
+ # and `unknown` now exists to name this one explicitly.
394
+ log_warn "$dep_label: required_stability is absent — schema-legal (nullable), but state it explicitly as 'unknown' so the gap is named rather than inferred"
395
+ ((WARNINGS++))
396
+ elif [[ "$stability_valid" -eq 0 ]]; then
397
+ local stability_deprecated=0
398
+ for v in "${DEPRECATED_STABILITY_VALUES[@]}"; do
399
+ if [[ "$required_stability" == "$v" ]]; then
400
+ stability_deprecated=1
401
+ break
402
+ fi
403
+ done
404
+
405
+ if [[ "$stability_deprecated" -eq 1 ]]; then
406
+ log_fail "$dep_label: required_stability '$required_stability' is not a stability value (M-ADR 0204 §4.4). Declare target_locus: '$required_stability' on the entry and give required_stability a real value (${VALID_STABILITY_VALUES[*]}) — a third-party contract can be locked."
407
+ ((dep_errors++))
408
+ else
409
+ log_fail "$dep_label: invalid required_stability '$required_stability' (allowed: ${VALID_STABILITY_VALUES[*]})"
410
+ ((dep_errors++))
411
+ fi
412
+ fi
413
+
414
+ # M-ADR 0204: an off-platform row skips platform resolution and the realiser
415
+ # join — NAMED, never silent. It must still bind to something (an
416
+ # external-system id in service_id, or a contract_path), and a capability_id
417
+ # on it is reported per §4.3.
418
+ if [[ "$target_locus" != "platform" ]]; then
419
+ if [[ -n "$capability_id" && "$capability_id" != "null" ]]; then
420
+ log_warn "$dep_label: capability_id on a target_locus=$target_locus row — an off-platform target has no platform capability (M-ADR 0204 §4.3); move the intent to notes"
421
+ ((WARNINGS++))
422
+ fi
423
+ if [[ ( -z "$service_id" || "$service_id" == "null" ) && ( -z "$contract_path" || "$contract_path" == "null" ) ]]; then
424
+ log_fail "$dep_label: target_locus=$target_locus entry names neither an external-system id (service_id) nor a contract_path — it binds to nothing"
425
+ ((dep_errors++))
426
+ else
427
+ log_info "$dep_label: target_locus=$target_locus — platform service/contract resolution NOT APPLICABLE (skipped, M-ADR 0204 §4.1); corroboration against external systems happens at aggregation"
428
+ fi
429
+ if [[ "$dep_errors" -eq 0 ]]; then
430
+ log_pass "$dep_label: locus=$target_locus stability=$required_stability type=$contract_type"
431
+ ((PASSED_DEPS++))
432
+ return 0
433
+ fi
434
+ FAILURES=$((FAILURES + dep_errors))
435
+ return 1
436
+ fi
437
+
438
+ # Check 2: service_id is declared (2a, local) and resolves against the platform
439
+ # source of truth (2b, needs the platform). The services catalogue (when present)
440
+ # is canonical — the same source the aggregator FK-validates dependency edges
441
+ # against. The prose service-architecture.md is the fallback for pre-catalogue
442
+ # platforms.
443
+ #
444
+ # 2a and 2b are separated because only 2b needs a platform surface: whether a
445
+ # service_id was declared at all is answerable in the app repo alone, and it is
446
+ # the more basic defect of the two.
447
+ local is_async=0
448
+ if [[ " ${ASYNC_CONTRACT_TYPES[*]} " == *" $contract_type "* ]]; then
449
+ is_async=1
450
+ fi
451
+
452
+ if [[ -z "$service_id" || "$service_id" == "null" ]]; then
453
+ if [[ "$is_async" -eq 1 ]]; then
454
+ # No realiser service by design (M-ADR 0192). It must still name what it
455
+ # DOES bind to, or nothing identifies the dependency at all — the
456
+ # aggregator drops such a row, so an unchecked hole here is a silent drop.
457
+ if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
458
+ log_fail "$dep_label: async dependency (contract_type '$contract_type') has neither service_id nor contract_path — it binds to nothing and cannot be recorded"
459
+ ((dep_errors++))
460
+ fi
461
+ else
462
+ log_fail "$dep_label: service_id is empty or missing"
463
+ ((dep_errors++))
464
+ fi
465
+ elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
466
+ ((UNRESOLVED_SERVICE_IDS++))
467
+ elif [[ -n "$PLATFORM_SERVICES_CSV" ]]; then
468
+ if ! service_id_in_catalogue "$service_id"; then
469
+ log_fail "$dep_label: service_id '$service_id' not found in platform services catalogue (catalogues/services.csv)"
470
+ ((dep_errors++))
471
+ elif [[ -n "$PLATFORM_SERVICE_ARCH" ]] && ! service_id_in_doc "$service_id"; then
472
+ log_warn "$dep_label: service_id '$service_id' is in the services catalogue but not mentioned in service-architecture.md (doc drift)"
473
+ ((WARNINGS++))
474
+ fi
475
+ elif [[ -n "$PLATFORM_SERVICE_ARCH" ]]; then
476
+ if ! service_id_in_doc "$service_id"; then
477
+ log_fail "$dep_label: service_id '$service_id' not found in platform service-architecture.md"
478
+ ((dep_errors++))
479
+ fi
480
+ fi
481
+
482
+ # Check 3c (local): a DECLARED contract_path must actually be a path. The field
483
+ # was long typed "<platform-relative-path-or-unknown>", which invited sentinels
484
+ # (`n/a`) and prose ("Kafka (<vendor> bus): topic-a") into a field every consumer
485
+ # resolves as a path. Reported where it is authored; the aggregator reports the
486
+ # same class as `convention` when it tries to resolve them. Warned, not failed:
487
+ # the corpus predates the rule and the explanation belongs in `notes` (0200 §4.6).
488
+ if [[ -n "$contract_path" && "$contract_path" != "null" ]] && ! is_path_shaped "$contract_path"; then
489
+ log_warn "$dep_label: contract_path \"$contract_path\" is not a path — nothing can resolve it; omit the field and put the explanation in notes"
490
+ ((WARNINGS++))
491
+ fi
492
+
493
+ # Check 3: a locked dependency declares a contract_path (3a, local) and that path
494
+ # resolves to a file (3b, needs the platform). Same split, same reason: "locked
495
+ # with no path at all" is an app-repo defect that needs nothing external to catch.
496
+ if [[ "$required_stability" == "locked" ]]; then
497
+ if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
498
+ log_fail "$dep_label: required_stability is 'locked' but contract_path is empty"
499
+ ((dep_errors++))
500
+ elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
501
+ ((UNRESOLVED_CONTRACT_PATHS++))
502
+ else
503
+ local full_contract_path="$PLATFORM_ROOT/$contract_path"
504
+ if [[ ! -f "$full_contract_path" ]]; then
505
+ log_fail "$dep_label: locked contract_path '$contract_path' does not exist in platform repo"
506
+ ((dep_errors++))
507
+ fi
508
+ fi
509
+ elif [[ "$required_stability" == "provisional" || "$required_stability" == "unknown" ]]; then
510
+ # Neither requires a contract_path. If one is declared it is still worth
511
+ # reporting when it does not resolve — silently accepting a path that
512
+ # points at nothing is how a locked dependency later inherits a dead path.
513
+ if [[ -n "$contract_path" && "$contract_path" != "null" && "$PLATFORM_AVAILABLE" -eq 1 ]]; then
514
+ local full_contract_path="$PLATFORM_ROOT/$contract_path"
515
+ if [[ ! -f "$full_contract_path" ]]; then
516
+ log_warn "$dep_label: contract_path '$contract_path' declared but not yet present ($required_stability — acceptable)"
517
+ ((WARNINGS++))
518
+ fi
519
+ fi
520
+ fi
521
+
522
+ if [[ "$dep_errors" -eq 0 ]]; then
523
+ log_pass "$dep_label: stability=$required_stability type=$contract_type"
524
+ ((PASSED_DEPS++))
525
+ return 0
526
+ fi
527
+
528
+ FAILURES=$((FAILURES + dep_errors))
529
+ return 1
530
+ }
531
+
532
+ # ──────────────────────────────────────────────────────────────────────────────
533
+ print_status_table() {
534
+ log_section "Dependency Status Table"
535
+
536
+ local app_id
537
+ app_id="$(yq e ".app_id // \"unknown\"" "$APP_DEPS_FILE" 2>/dev/null || echo unknown)"
538
+ log_info "App: $app_id"
539
+
540
+ printf "\n %-30s %-15s %-12s %s\n" "Service ID" "Stability" "Contract" "Status"
541
+ printf " %-30s %-15s %-12s %s\n" "──────────────────────────────" "───────────────" "────────────" "──────────"
542
+
543
+ local source_file count
544
+ for source_file in "${DEP_SOURCES[@]}"; do
545
+ count="$(quiet_dep_count "$source_file")"
546
+ [[ "$count" -gt 0 ]] || continue
547
+ if [[ ${#DEP_SOURCES[@]} -gt 1 ]]; then
548
+ printf " %s\n" "── ${source_file#"$APP_ROOT"/}"
549
+ fi
550
+
551
+ for ((i = 0; i < count; i++)); do
552
+ local service_id required_stability contract_type contract_path dep_status
553
+ service_id="$(yq e ".platform_dependencies[$i].service_id // \"(missing)\"" "$source_file")"
554
+ required_stability="$(yq e ".platform_dependencies[$i].required_stability // \"(missing)\"" "$source_file")"
555
+ contract_type="$(yq e ".platform_dependencies[$i].contract_type // \"(missing)\"" "$source_file")"
556
+ contract_path="$(yq e ".platform_dependencies[$i].contract_path // \"\"" "$source_file")"
557
+
558
+ # Determine display status
559
+ if [[ "$required_stability" == "locked" ]]; then
560
+ if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
561
+ dep_status="${RED}LOCKED UNMET${NC}"
562
+ elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
563
+ # A declared path that was never resolved is not "OK" and not "UNMET".
564
+ # Reporting either would be a claim the run cannot support.
565
+ dep_status="${YELLOW}LOCKED UNRESOLVED${NC}"
566
+ elif [[ -f "$PLATFORM_ROOT/$contract_path" ]]; then
567
+ dep_status="${GREEN}LOCKED OK${NC}"
568
+ else
569
+ dep_status="${RED}LOCKED UNMET${NC}"
570
+ fi
571
+ elif [[ "$required_stability" == "provisional" ]]; then
572
+ dep_status="${YELLOW}PROVISIONAL${NC}"
573
+ elif [[ "$required_stability" == "unknown" ]]; then
574
+ dep_status="${YELLOW}UNKNOWN${NC}"
575
+ elif [[ " ${DEPRECATED_STABILITY_VALUES[*]} " == *" $required_stability "* ]]; then
576
+ dep_status="${YELLOW}NOT A STABILITY VALUE${NC}"
577
+ else
578
+ dep_status="${RED}INVALID${NC}"
579
+ fi
580
+
581
+ printf " %-30s %-15s %-12s " "$service_id" "$required_stability" "$contract_type"
582
+ echo -e "$dep_status"
583
+ done
584
+ done
585
+ echo ""
586
+ }
587
+
588
+ # ──────────────────────────────────────────────────────────────────────────────
589
+ main() {
590
+ log_section "App Dependency Validation"
591
+ log_info "App root: $APP_ROOT"
592
+ log_info "Platform root: $PLATFORM_ROOT"
593
+
594
+ check_prerequisites
595
+
596
+ log_section "Validating Dependencies"
597
+
598
+ local source_file dep_count
599
+ TOTAL_DEPS=0
600
+ for source_file in "${DEP_SOURCES[@]}"; do
601
+ read_source_dep_count "$source_file"
602
+ dep_count="$SOURCE_DEP_COUNT"
603
+ [[ "$dep_count" -gt 0 ]] || continue
604
+ if [[ ${#DEP_SOURCES[@]} -gt 1 ]]; then
605
+ log_info "${source_file#"$APP_ROOT"/}: $dep_count dependency(ies)"
606
+ fi
607
+ TOTAL_DEPS=$((TOTAL_DEPS + dep_count))
608
+ for ((i = 0; i < dep_count; i++)); do
609
+ validate_dependency "$source_file" "$i" || true
610
+ done
611
+ done
612
+
613
+ if [[ "$TOTAL_DEPS" -eq 0 ]]; then
614
+ if [[ "$UNPARSEABLE_SOURCES" -gt 0 ]]; then
615
+ # Zero dependencies BECAUSE the source could not be read is not the
616
+ # same result as zero dependencies declared, and must not exit alike.
617
+ log_fail "$UNPARSEABLE_SOURCES dependency source(s) could not be parsed — 0 dependencies checked, which is an absence of evidence, not a pass."
618
+ exit 1
619
+ fi
620
+ log_warn "No platform_dependencies entries found in ${#DEP_SOURCES[@]} source(s)"
621
+ log_info "If this app has no platform deps, this is acceptable."
622
+ log_na "App dependencies: 0 platform_dependencies entries found — nothing to check"
623
+ exit 0
624
+ fi
625
+
626
+ print_status_table
627
+
628
+ log_section "Summary"
629
+ log_info "Total dependencies: $TOTAL_DEPS"
630
+ log_info "Passed: $PASSED_DEPS"
631
+ log_info "Warnings: $WARNINGS"
632
+ log_info "Failures: $FAILURES"
633
+
634
+ if [[ "$UNPARSEABLE_SOURCES" -gt 0 ]]; then
635
+ log_fail "$UNPARSEABLE_SOURCES dependency source(s) could not be parsed. Whatever they declare was checked by nothing, so no verdict here covers them."
636
+ exit 1
637
+ fi
638
+
639
+ if [[ "$FAILURES" -gt 0 ]]; then
640
+ log_fail "$FAILURES dependency check(s) failed. Stage 6 lock-in gate NOT met."
641
+ exit 1
642
+ fi
643
+
644
+ # Without a platform surface the gate cannot claim to have been met, however
645
+ # clean the checks it DID run. Naming the un-run checks and their denominator
646
+ # is the whole point: "no failures" and "the gate is met" are different claims,
647
+ # and conflating them is how this validator came to report green for months
648
+ # while never executing (M-ADR 0200 §4.3).
649
+ if [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
650
+ log_warn "Ran check 1 (stability vocabulary) and the local halves of checks 2-3 over $TOTAL_DEPS dependency(ies): $FAILURES failure(s), $WARNINGS warning(s)."
651
+ log_warn "NOT RUN, for want of --platform-root: $UNRESOLVED_SERVICE_IDS service_id resolution(s) and $UNRESOLVED_CONTRACT_PATHS locked contract_path resolution(s)."
652
+ log_info "This is a partial run, not a met gate — pass --platform-root to complete it."
653
+ exit 0
654
+ fi
655
+
656
+ if [[ "$WARNINGS" -gt 0 ]]; then
657
+ log_warn "All required checks passed with $WARNINGS warning(s)."
658
+ else
659
+ log_pass "All dependency checks passed. Stage 6 lock-in gate met."
660
+ fi
661
+ exit 0
662
+ }
663
+
664
+ main "$@"