@sdd-method/sdd-cli 1.0.1 → 1.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/dist/lib/catalogue/aggregate.d.ts.map +1 -1
- package/dist/lib/catalogue/aggregate.js +111 -4
- package/dist/lib/catalogue/aggregate.js.map +1 -1
- package/dist/lib/catalogue/builders/external-system-exemption.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/external-system-exemption.js +32 -3
- package/dist/lib/catalogue/builders/external-system-exemption.js.map +1 -1
- package/dist/lib/catalogue/builders/product-edges.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/product-edges.js +9 -0
- package/dist/lib/catalogue/builders/product-edges.js.map +1 -1
- package/dist/lib/catalogue/builders/product-platform-dependencies.d.ts +12 -0
- package/dist/lib/catalogue/builders/product-platform-dependencies.d.ts.map +1 -1
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +99 -0
- package/dist/lib/catalogue/builders/product-platform-dependencies.js.map +1 -1
- package/dist/lib/catalogue/canonical-schema.yaml +8 -6
- package/dist/lib/catalogue/contract-path-resolution.d.ts +55 -0
- package/dist/lib/catalogue/contract-path-resolution.d.ts.map +1 -0
- package/dist/lib/catalogue/contract-path-resolution.js +149 -0
- package/dist/lib/catalogue/contract-path-resolution.js.map +1 -0
- package/dist/lib/sync/apply.d.ts +29 -1
- package/dist/lib/sync/apply.d.ts.map +1 -1
- package/dist/lib/sync/apply.js +77 -2
- package/dist/lib/sync/apply.js.map +1 -1
- package/dist/lib/sync/index.d.ts.map +1 -1
- package/dist/lib/sync/index.js +23 -1
- package/dist/lib/sync/index.js.map +1 -1
- package/dist/lib/sync/settings-merge.d.ts +29 -0
- package/dist/lib/sync/settings-merge.d.ts.map +1 -1
- package/dist/lib/sync/settings-merge.js +31 -0
- package/dist/lib/sync/settings-merge.js.map +1 -1
- package/dist/lib/validate/dispatch.d.ts.map +1 -1
- package/dist/lib/validate/dispatch.js +19 -17
- package/dist/lib/validate/dispatch.js.map +1 -1
- package/dist/lib/validate/index.d.ts.map +1 -1
- package/dist/lib/validate/index.js +23 -4
- package/dist/lib/validate/index.js.map +1 -1
- package/dist/verbs/conformance.d.ts.map +1 -1
- package/dist/verbs/conformance.js +3 -2
- package/dist/verbs/conformance.js.map +1 -1
- package/dist/verbs/contracts.d.ts.map +1 -1
- package/dist/verbs/contracts.js +3 -2
- package/dist/verbs/contracts.js.map +1 -1
- package/dist/verbs/generate-briefing.d.ts.map +1 -1
- package/dist/verbs/generate-briefing.js +2 -1
- package/dist/verbs/generate-briefing.js.map +1 -1
- package/dist/verbs/init.d.ts.map +1 -1
- package/dist/verbs/init.js +3 -2
- package/dist/verbs/init.js.map +1 -1
- package/dist/verbs/pack.d.ts.map +1 -1
- package/dist/verbs/pack.js +2 -1
- package/dist/verbs/pack.js.map +1 -1
- package/dist/verbs/path-option.d.ts +22 -0
- package/dist/verbs/path-option.d.ts.map +1 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/path-option.js.map +1 -0
- package/dist/verbs/plan.d.ts.map +1 -1
- package/dist/verbs/plan.js +2 -1
- package/dist/verbs/plan.js.map +1 -1
- package/dist/verbs/scaffold.d.ts.map +1 -1
- package/dist/verbs/scaffold.js +2 -1
- package/dist/verbs/scaffold.js.map +1 -1
- package/dist/verbs/sync.d.ts.map +1 -1
- package/dist/verbs/sync.js +2 -1
- package/dist/verbs/sync.js.map +1 -1
- package/dist/verbs/validate.d.ts.map +1 -1
- package/dist/verbs/validate.js +3 -2
- package/dist/verbs/validate.js.map +1 -1
- package/package.json +1 -1
- package/scripts/validate-app-dependencies.sh +378 -58
|
@@ -3,17 +3,52 @@
|
|
|
3
3
|
# validate-app-dependencies.sh - Validate app platform dependency declarations
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# ./validate-app-dependencies.sh --app-root <path> --platform-root <path>
|
|
6
|
+
# ./validate-app-dependencies.sh --app-root <path> [--platform-root <path>]
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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
|
|
14
21
|
#
|
|
15
22
|
# Outputs a dependency status table.
|
|
16
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.
|
|
17
52
|
|
|
18
53
|
set -euo pipefail
|
|
19
54
|
|
|
@@ -41,12 +76,15 @@ PLATFORM_ROOT=""
|
|
|
41
76
|
|
|
42
77
|
usage() {
|
|
43
78
|
cat <<USAGE
|
|
44
|
-
Usage: $(basename "$0") --app-root <path> --platform-root <path>
|
|
79
|
+
Usage: $(basename "$0") --app-root <path> [--platform-root <path>]
|
|
45
80
|
|
|
46
81
|
Options:
|
|
47
82
|
--app-root Path to the app repository root (must contain app-platform-dependencies.yaml)
|
|
48
|
-
--platform-root Path to the platform repository root (service_id checks use
|
|
49
|
-
catalogues/services.csv when present, else docs/platform/service-architecture.md)
|
|
83
|
+
--platform-root OPTIONAL. Path to the platform repository root (service_id checks use
|
|
84
|
+
catalogues/services.csv when present, else docs/platform/service-architecture.md).
|
|
85
|
+
Omitted: the stability vocabulary and the local halves of the
|
|
86
|
+
service_id/contract_path checks run; the resolutions are reported
|
|
87
|
+
as not run. The gate is not met by a partial run.
|
|
50
88
|
USAGE
|
|
51
89
|
}
|
|
52
90
|
|
|
@@ -78,42 +116,124 @@ if [[ -z "$APP_ROOT" ]]; then
|
|
|
78
116
|
exit 1
|
|
79
117
|
fi
|
|
80
118
|
|
|
81
|
-
if [[ -z "$PLATFORM_ROOT" ]]; then
|
|
82
|
-
log_fail "--platform-root is required"
|
|
83
|
-
usage
|
|
84
|
-
exit 1
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
119
|
if [[ ! -d "$APP_ROOT" ]]; then
|
|
88
120
|
log_fail "App root not found: $APP_ROOT"
|
|
89
121
|
exit 1
|
|
90
122
|
fi
|
|
91
123
|
|
|
92
|
-
|
|
124
|
+
# --platform-root is OPTIONAL, and this is the whole of why:
|
|
125
|
+
#
|
|
126
|
+
# Only checks 2 and 3 need a platform surface. Check 1 is local to the app repo.
|
|
127
|
+
# While the flag was mandatory, an app repo standing alone could run NONE of the
|
|
128
|
+
# three — the validator was skipped wholesale by the dispatcher, so it had never
|
|
129
|
+
# executed in any of the 70 dependency files across two estates, and the suite
|
|
130
|
+
# reported "All validators passed" over the skip.
|
|
131
|
+
#
|
|
132
|
+
# Without a platform root the checks that CAN run, run, and the checks that
|
|
133
|
+
# cannot are named as not-run with their denominator. Absent evidence is
|
|
134
|
+
# reported as absent, never as a pass (M-ADR 0200 §4.3) — the same treatment
|
|
135
|
+
# validate-gate-readiness.sh gives an empty registry.
|
|
136
|
+
PLATFORM_AVAILABLE=1
|
|
137
|
+
if [[ -z "$PLATFORM_ROOT" ]]; then
|
|
138
|
+
PLATFORM_AVAILABLE=0
|
|
139
|
+
elif [[ ! -d "$PLATFORM_ROOT" ]]; then
|
|
140
|
+
# An explicitly-given root that does not exist is an error, not a downgrade:
|
|
141
|
+
# the caller asserted a platform surface that is not there.
|
|
93
142
|
log_fail "Platform root not found: $PLATFORM_ROOT"
|
|
94
143
|
exit 1
|
|
95
144
|
fi
|
|
96
145
|
|
|
97
146
|
APP_DEPS_FILE="$APP_ROOT/app-platform-dependencies.yaml"
|
|
147
|
+
|
|
148
|
+
# TWO AUTHORING SOURCES, NOT ONE.
|
|
149
|
+
# The catalogue builder does multi-source ingestion under the ADR 0157 parity
|
|
150
|
+
# principle: `app-platform-dependencies.yaml` AND every
|
|
151
|
+
# `reverse-evidence/**/feature-grounding-evidence.yaml`'s platform_dependencies
|
|
152
|
+
# block, merged by primary key with forward winning. They are equal-status
|
|
153
|
+
# authoring surfaces.
|
|
154
|
+
#
|
|
155
|
+
# This gate read only the forward file, so an entire equal-status source went
|
|
156
|
+
# ungated. Measured in one reverse estate: 15 evidence files declaring 65
|
|
157
|
+
# further dependencies, 38 of them `locked` — precisely the lock-in population
|
|
158
|
+
# a Stage 6 lock-in gate exists to check — none of which this gate could see.
|
|
159
|
+
#
|
|
160
|
+
# A source that the catalogue reads and the gate does not is a coverage gap by
|
|
161
|
+
# construction, so the gate now reads both.
|
|
162
|
+
DEP_SOURCES=()
|
|
163
|
+
if [[ -f "$APP_DEPS_FILE" ]]; then
|
|
164
|
+
DEP_SOURCES+=("$APP_DEPS_FILE")
|
|
165
|
+
fi
|
|
166
|
+
if [[ -d "$APP_ROOT/reverse-evidence" ]]; then
|
|
167
|
+
while IFS= read -r evidence_file; do
|
|
168
|
+
[[ -n "$evidence_file" ]] || continue
|
|
169
|
+
DEP_SOURCES+=("$evidence_file")
|
|
170
|
+
done < <(find "$APP_ROOT/reverse-evidence" -name "feature-grounding-evidence.yaml" -type f | sort)
|
|
171
|
+
fi
|
|
98
172
|
PLATFORM_SERVICE_ARCH="$PLATFORM_ROOT/docs/platform/service-architecture.md"
|
|
99
173
|
PLATFORM_SERVICES_CSV="$PLATFORM_ROOT/catalogues/services.csv"
|
|
100
174
|
|
|
101
|
-
|
|
175
|
+
# One value per line: this array is the enforcement point, and the correspondence
|
|
176
|
+
# registry reads it line-wise to compare against what the distributed templates
|
|
177
|
+
# offer (instances app-dependency-stability-vocabulary-forward and -reverse).
|
|
178
|
+
VALID_STABILITY_VALUES=(
|
|
179
|
+
"provisional" # contract may still change; the app tolerates it
|
|
180
|
+
"locked" # contract is fixed; check 3 requires a resolvable contract_path
|
|
181
|
+
"unknown" # expectation not determined; no contract_path required
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# M-ADR 0204: where the dependency's TARGET lives. One value per line — the
|
|
185
|
+
# correspondence registry reads this array line-wise against what the
|
|
186
|
+
# distributed templates offer, exactly as it does VALID_STABILITY_VALUES.
|
|
187
|
+
# Single-quoted deliberately: the correspondence extractor for the STABILITY
|
|
188
|
+
# array captures `^ "value"` lines, and two enums sharing one line shape
|
|
189
|
+
# would pollute each other's claimed sets — measured, not hypothetical.
|
|
190
|
+
VALID_TARGET_LOCUS_VALUES=(
|
|
191
|
+
'platform' # the default when absent; the row is fully platform-validated
|
|
192
|
+
'external' # third-party target; platform resolution + realiser join skipped, named
|
|
193
|
+
'legacy' # off-platform predecessor system; same skips, different lifecycle
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# The pre-0204 improvisation. These were WARNED while no locus field existed;
|
|
197
|
+
# M-ADR 0204 §4.4 flips them to FAILURES now that it does — the message names
|
|
198
|
+
# the home for what they meant.
|
|
199
|
+
DEPRECATED_STABILITY_VALUES=("external" "legacy")
|
|
200
|
+
|
|
201
|
+
# The M-ADR 0192 async set. A shared-stream subscription has NO single realiser
|
|
202
|
+
# service, so it binds to the stream contract plus the publishing capability
|
|
203
|
+
# instead — the canonical schema makes product_platform_dependencies.
|
|
204
|
+
# target_service_id conditionally nullable for exactly these contract types, and
|
|
205
|
+
# states that a synchronous contract_type still requires it.
|
|
206
|
+
#
|
|
207
|
+
# This gate did not know that. Check 2a hard-failed "service_id is empty or
|
|
208
|
+
# missing" on every async entry, which is the shape both distributed templates
|
|
209
|
+
# instruct an author to write (they say to OMIT service_id). Found the moment the
|
|
210
|
+
# check first ran: 3 of the reverse estate's 48 dependency files, all three async.
|
|
211
|
+
ASYNC_CONTRACT_TYPES=("event" "event-stream" "stream")
|
|
102
212
|
|
|
103
213
|
FAILURES=0
|
|
104
214
|
WARNINGS=0
|
|
105
215
|
TOTAL_DEPS=0
|
|
106
216
|
PASSED_DEPS=0
|
|
107
217
|
|
|
218
|
+
# What could not be checked for want of a platform surface. Counted, not ignored:
|
|
219
|
+
# the summary states these as a denominator so a run without a platform root can
|
|
220
|
+
# never read as a clean bill of health on the resolution checks.
|
|
221
|
+
UNPARSEABLE_SOURCES=0
|
|
222
|
+
SOURCE_DEP_COUNT=0
|
|
223
|
+
UNRESOLVED_SERVICE_IDS=0
|
|
224
|
+
UNRESOLVED_CONTRACT_PATHS=0
|
|
225
|
+
|
|
108
226
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
109
227
|
check_prerequisites() {
|
|
110
228
|
log_section "Prerequisites"
|
|
111
229
|
|
|
112
|
-
if [[
|
|
113
|
-
log_fail "
|
|
230
|
+
if [[ ${#DEP_SOURCES[@]} -eq 0 ]]; then
|
|
231
|
+
log_fail "no dependency source found: neither $APP_DEPS_FILE nor any reverse-evidence/**/feature-grounding-evidence.yaml"
|
|
114
232
|
exit 1
|
|
115
233
|
fi
|
|
116
|
-
|
|
234
|
+
for src in "${DEP_SOURCES[@]}"; do
|
|
235
|
+
log_pass "dependency source: ${src#"$APP_ROOT"/}"
|
|
236
|
+
done
|
|
117
237
|
|
|
118
238
|
if ! command -v yq &>/dev/null; then
|
|
119
239
|
log_fail "yq is required but not installed. Install with: brew install yq"
|
|
@@ -121,6 +241,13 @@ check_prerequisites() {
|
|
|
121
241
|
fi
|
|
122
242
|
log_pass "yq available"
|
|
123
243
|
|
|
244
|
+
if [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
245
|
+
PLATFORM_SERVICES_CSV=""
|
|
246
|
+
PLATFORM_SERVICE_ARCH=""
|
|
247
|
+
log_info "No --platform-root given: check 1 (stability vocabulary) runs; checks 2 and 3 do not."
|
|
248
|
+
return 0
|
|
249
|
+
fi
|
|
250
|
+
|
|
124
251
|
if [[ -f "$PLATFORM_SERVICES_CSV" ]]; then
|
|
125
252
|
log_pass "Platform services catalogue found (canonical service_id source)"
|
|
126
253
|
else
|
|
@@ -139,6 +266,21 @@ check_prerequisites() {
|
|
|
139
266
|
fi
|
|
140
267
|
}
|
|
141
268
|
|
|
269
|
+
# Is this value shaped like a path, as opposed to a sentinel or a prose note?
|
|
270
|
+
# Mirrors isPathShaped() in the CLI's contract-path-resolution.ts, which applies
|
|
271
|
+
# the same test at aggregation tier — one rule, asserted at both tiers.
|
|
272
|
+
is_path_shaped() {
|
|
273
|
+
local v="$1"
|
|
274
|
+
[[ -n "$v" && "$v" != "null" ]] || return 1
|
|
275
|
+
[[ "${v,,}" != n/a* ]] || return 1
|
|
276
|
+
[[ "${v:0:1}" != "[" ]] || return 1
|
|
277
|
+
[[ "$v" == */* ]] || return 1
|
|
278
|
+
# Prose gives itself away with whitespace before the first separator.
|
|
279
|
+
local first="${v%%/*}"
|
|
280
|
+
[[ "$first" != *[[:space:]]* ]] || return 1
|
|
281
|
+
return 0
|
|
282
|
+
}
|
|
283
|
+
|
|
142
284
|
# Word-anchored match: the id must not be a substring of a longer slug token.
|
|
143
285
|
service_id_in_doc() {
|
|
144
286
|
local service_id="$1"
|
|
@@ -151,24 +293,70 @@ service_id_in_catalogue() {
|
|
|
151
293
|
}
|
|
152
294
|
|
|
153
295
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
296
|
+
# How many platform_dependencies entries does this source declare? Zero for a
|
|
297
|
+
# source that has no such block, and zero — with a reported finding — for one that
|
|
298
|
+
# cannot be parsed. An unparseable evidence file is NOT silently treated as empty:
|
|
299
|
+
# measured in one estate, a single unquoted colon made a 206-line authored file
|
|
300
|
+
# invisible, taking its dependency declarations with it.
|
|
301
|
+
# Sets SOURCE_DEP_COUNT, and increments UNPARSEABLE_SOURCES on a broken source.
|
|
302
|
+
#
|
|
303
|
+
# Deliberately NOT a function that echoes its answer: a command substitution runs
|
|
304
|
+
# in a subshell, so the counter increment would be discarded and the log line
|
|
305
|
+
# would be captured as part of the count. Both happened; both are silent in bash.
|
|
306
|
+
read_source_dep_count() {
|
|
307
|
+
local source_file="$1"
|
|
308
|
+
if ! SOURCE_DEP_COUNT="$(yq e ".platform_dependencies | length" "$source_file" 2>/dev/null)"; then
|
|
309
|
+
log_fail "${source_file#"$APP_ROOT"/}: cannot be parsed as YAML — every dependency it declares is invisible to this gate and to the catalogue"
|
|
310
|
+
UNPARSEABLE_SOURCES=$((UNPARSEABLE_SOURCES + 1))
|
|
311
|
+
SOURCE_DEP_COUNT=0
|
|
312
|
+
return
|
|
313
|
+
fi
|
|
314
|
+
[[ "$SOURCE_DEP_COUNT" =~ ^[0-9]+$ ]] || SOURCE_DEP_COUNT=0
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Quiet variant for the display pass — the failure was already reported when the
|
|
318
|
+
# sources were counted; reporting it twice would double-count nothing useful.
|
|
319
|
+
quiet_dep_count() {
|
|
320
|
+
local source_file="$1" count
|
|
321
|
+
count="$(yq e ".platform_dependencies | length" "$source_file" 2>/dev/null || echo 0)"
|
|
322
|
+
[[ "$count" =~ ^[0-9]+$ ]] || count=0
|
|
323
|
+
echo "$count"
|
|
324
|
+
}
|
|
325
|
+
|
|
154
326
|
validate_dependency() {
|
|
155
|
-
local
|
|
327
|
+
local source_file="$1"
|
|
328
|
+
local index="$2"
|
|
156
329
|
local dep_errors=0
|
|
157
330
|
|
|
158
331
|
local capability_id service_id contract_type required_stability contract_path
|
|
159
332
|
|
|
160
|
-
capability_id="$(yq e ".platform_dependencies[$index].capability_id // \"\"" "$
|
|
161
|
-
service_id="$(yq e ".platform_dependencies[$index].service_id // \"\"" "$
|
|
162
|
-
contract_type="$(yq e ".platform_dependencies[$index].contract_type // \"\"" "$
|
|
163
|
-
required_stability="$(yq e ".platform_dependencies[$index].required_stability // \"\"" "$
|
|
164
|
-
contract_path="$(yq e ".platform_dependencies[$index].contract_path // \"\"" "$
|
|
333
|
+
capability_id="$(yq e ".platform_dependencies[$index].capability_id // \"\"" "$source_file")"
|
|
334
|
+
service_id="$(yq e ".platform_dependencies[$index].service_id // \"\"" "$source_file")"
|
|
335
|
+
contract_type="$(yq e ".platform_dependencies[$index].contract_type // \"\"" "$source_file")"
|
|
336
|
+
required_stability="$(yq e ".platform_dependencies[$index].required_stability // \"\"" "$source_file")"
|
|
337
|
+
contract_path="$(yq e ".platform_dependencies[$index].contract_path // \"\"" "$source_file")"
|
|
165
338
|
|
|
166
339
|
local dep_label="${service_id:-dep[$index]}"
|
|
167
340
|
if [[ -n "$capability_id" && "$capability_id" != "null" ]]; then
|
|
168
341
|
dep_label="$capability_id / $service_id"
|
|
169
342
|
fi
|
|
170
343
|
|
|
171
|
-
#
|
|
344
|
+
# M-ADR 0204: target locus. Absent = platform. Invalid FAILS — this field is
|
|
345
|
+
# new, has no legacy corpus, and a tolerated typo would silently exempt the
|
|
346
|
+
# row from every platform check.
|
|
347
|
+
local target_locus
|
|
348
|
+
target_locus="$(yq e ".platform_dependencies[$index].target_locus // \"\"" "$source_file")"
|
|
349
|
+
[[ "$target_locus" == "null" ]] && target_locus=""
|
|
350
|
+
if [[ -z "$target_locus" ]]; then
|
|
351
|
+
target_locus="platform"
|
|
352
|
+
elif [[ " ${VALID_TARGET_LOCUS_VALUES[*]} " != *" $target_locus "* ]]; then
|
|
353
|
+
log_fail "$dep_label: invalid target_locus '$target_locus' (allowed: ${VALID_TARGET_LOCUS_VALUES[*]})"
|
|
354
|
+
((dep_errors++))
|
|
355
|
+
target_locus="platform"
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Check 1: required_stability is a valid value. Needs NO platform surface —
|
|
359
|
+
# it is the only check that can run in an app repo standing alone.
|
|
172
360
|
local stability_valid=0
|
|
173
361
|
for v in "${VALID_STABILITY_VALUES[@]}"; do
|
|
174
362
|
if [[ "$required_stability" == "$v" ]]; then
|
|
@@ -177,18 +365,86 @@ validate_dependency() {
|
|
|
177
365
|
fi
|
|
178
366
|
done
|
|
179
367
|
|
|
180
|
-
if [[ "$stability_valid" -eq 0 ]]; then
|
|
181
|
-
|
|
182
|
-
|
|
368
|
+
if [[ "$stability_valid" -eq 0 && ( -z "$required_stability" || "$required_stability" == "null" ) ]]; then
|
|
369
|
+
# ABSENT is not INVALID. The canonical schema declares
|
|
370
|
+
# required_stability nullable on both tables that carry it, so an entry
|
|
371
|
+
# without one is schema-legal and the gate must not contradict its own
|
|
372
|
+
# schema — that is exactly the defect the `unknown` alignment fixed.
|
|
373
|
+
# Reported all the same: M-ADR 0196 prefers a named gap to a silent one,
|
|
374
|
+
# and `unknown` now exists to name this one explicitly.
|
|
375
|
+
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"
|
|
376
|
+
((WARNINGS++))
|
|
377
|
+
elif [[ "$stability_valid" -eq 0 ]]; then
|
|
378
|
+
local stability_deprecated=0
|
|
379
|
+
for v in "${DEPRECATED_STABILITY_VALUES[@]}"; do
|
|
380
|
+
if [[ "$required_stability" == "$v" ]]; then
|
|
381
|
+
stability_deprecated=1
|
|
382
|
+
break
|
|
383
|
+
fi
|
|
384
|
+
done
|
|
385
|
+
|
|
386
|
+
if [[ "$stability_deprecated" -eq 1 ]]; then
|
|
387
|
+
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."
|
|
388
|
+
((dep_errors++))
|
|
389
|
+
else
|
|
390
|
+
log_fail "$dep_label: invalid required_stability '$required_stability' (allowed: ${VALID_STABILITY_VALUES[*]})"
|
|
391
|
+
((dep_errors++))
|
|
392
|
+
fi
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# M-ADR 0204: an off-platform row skips platform resolution and the realiser
|
|
396
|
+
# join — NAMED, never silent. It must still bind to something (an
|
|
397
|
+
# external-system id in service_id, or a contract_path), and a capability_id
|
|
398
|
+
# on it is reported per §4.3.
|
|
399
|
+
if [[ "$target_locus" != "platform" ]]; then
|
|
400
|
+
if [[ -n "$capability_id" && "$capability_id" != "null" ]]; then
|
|
401
|
+
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"
|
|
402
|
+
((WARNINGS++))
|
|
403
|
+
fi
|
|
404
|
+
if [[ ( -z "$service_id" || "$service_id" == "null" ) && ( -z "$contract_path" || "$contract_path" == "null" ) ]]; then
|
|
405
|
+
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"
|
|
406
|
+
((dep_errors++))
|
|
407
|
+
else
|
|
408
|
+
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"
|
|
409
|
+
fi
|
|
410
|
+
if [[ "$dep_errors" -eq 0 ]]; then
|
|
411
|
+
log_pass "$dep_label: locus=$target_locus stability=$required_stability type=$contract_type"
|
|
412
|
+
((PASSED_DEPS++))
|
|
413
|
+
return 0
|
|
414
|
+
fi
|
|
415
|
+
FAILURES=$((FAILURES + dep_errors))
|
|
416
|
+
return 1
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
# Check 2: service_id is declared (2a, local) and resolves against the platform
|
|
420
|
+
# source of truth (2b, needs the platform). The services catalogue (when present)
|
|
421
|
+
# is canonical — the same source the aggregator FK-validates dependency edges
|
|
422
|
+
# against. The prose service-architecture.md is the fallback for pre-catalogue
|
|
423
|
+
# platforms.
|
|
424
|
+
#
|
|
425
|
+
# 2a and 2b are separated because only 2b needs a platform surface: whether a
|
|
426
|
+
# service_id was declared at all is answerable in the app repo alone, and it is
|
|
427
|
+
# the more basic defect of the two.
|
|
428
|
+
local is_async=0
|
|
429
|
+
if [[ " ${ASYNC_CONTRACT_TYPES[*]} " == *" $contract_type "* ]]; then
|
|
430
|
+
is_async=1
|
|
183
431
|
fi
|
|
184
432
|
|
|
185
|
-
# Check 2: service_id resolves against the platform source of truth.
|
|
186
|
-
# The services catalogue (when present) is canonical — it is the same source
|
|
187
|
-
# the aggregator FK-validates dependency edges against. The prose
|
|
188
|
-
# service-architecture.md is the fallback for pre-catalogue platforms.
|
|
189
433
|
if [[ -z "$service_id" || "$service_id" == "null" ]]; then
|
|
190
|
-
|
|
191
|
-
|
|
434
|
+
if [[ "$is_async" -eq 1 ]]; then
|
|
435
|
+
# No realiser service by design (M-ADR 0192). It must still name what it
|
|
436
|
+
# DOES bind to, or nothing identifies the dependency at all — the
|
|
437
|
+
# aggregator drops such a row, so an unchecked hole here is a silent drop.
|
|
438
|
+
if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
|
|
439
|
+
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"
|
|
440
|
+
((dep_errors++))
|
|
441
|
+
fi
|
|
442
|
+
else
|
|
443
|
+
log_fail "$dep_label: service_id is empty or missing"
|
|
444
|
+
((dep_errors++))
|
|
445
|
+
fi
|
|
446
|
+
elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
447
|
+
((UNRESOLVED_SERVICE_IDS++))
|
|
192
448
|
elif [[ -n "$PLATFORM_SERVICES_CSV" ]]; then
|
|
193
449
|
if ! service_id_in_catalogue "$service_id"; then
|
|
194
450
|
log_fail "$dep_label: service_id '$service_id' not found in platform services catalogue (catalogues/services.csv)"
|
|
@@ -204,11 +460,26 @@ validate_dependency() {
|
|
|
204
460
|
fi
|
|
205
461
|
fi
|
|
206
462
|
|
|
207
|
-
# Check
|
|
463
|
+
# Check 3c (local): a DECLARED contract_path must actually be a path. The field
|
|
464
|
+
# was long typed "<platform-relative-path-or-unknown>", which invited sentinels
|
|
465
|
+
# (`n/a`) and prose ("Kafka (<vendor> bus): topic-a") into a field every consumer
|
|
466
|
+
# resolves as a path. Reported where it is authored; the aggregator reports the
|
|
467
|
+
# same class as `convention` when it tries to resolve them. Warned, not failed:
|
|
468
|
+
# the corpus predates the rule and the explanation belongs in `notes` (0200 §4.6).
|
|
469
|
+
if [[ -n "$contract_path" && "$contract_path" != "null" ]] && ! is_path_shaped "$contract_path"; then
|
|
470
|
+
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"
|
|
471
|
+
((WARNINGS++))
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
# Check 3: a locked dependency declares a contract_path (3a, local) and that path
|
|
475
|
+
# resolves to a file (3b, needs the platform). Same split, same reason: "locked
|
|
476
|
+
# with no path at all" is an app-repo defect that needs nothing external to catch.
|
|
208
477
|
if [[ "$required_stability" == "locked" ]]; then
|
|
209
478
|
if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
|
|
210
479
|
log_fail "$dep_label: required_stability is 'locked' but contract_path is empty"
|
|
211
480
|
((dep_errors++))
|
|
481
|
+
elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
482
|
+
((UNRESOLVED_CONTRACT_PATHS++))
|
|
212
483
|
else
|
|
213
484
|
local full_contract_path="$PLATFORM_ROOT/$contract_path"
|
|
214
485
|
if [[ ! -f "$full_contract_path" ]]; then
|
|
@@ -216,11 +487,14 @@ validate_dependency() {
|
|
|
216
487
|
((dep_errors++))
|
|
217
488
|
fi
|
|
218
489
|
fi
|
|
219
|
-
elif [[ "$required_stability" == "provisional" ]]; then
|
|
220
|
-
|
|
490
|
+
elif [[ "$required_stability" == "provisional" || "$required_stability" == "unknown" ]]; then
|
|
491
|
+
# Neither requires a contract_path. If one is declared it is still worth
|
|
492
|
+
# reporting when it does not resolve — silently accepting a path that
|
|
493
|
+
# points at nothing is how a locked dependency later inherits a dead path.
|
|
494
|
+
if [[ -n "$contract_path" && "$contract_path" != "null" && "$PLATFORM_AVAILABLE" -eq 1 ]]; then
|
|
221
495
|
local full_contract_path="$PLATFORM_ROOT/$contract_path"
|
|
222
496
|
if [[ ! -f "$full_contract_path" ]]; then
|
|
223
|
-
log_warn "$dep_label: contract_path '$contract_path' declared but not yet present (
|
|
497
|
+
log_warn "$dep_label: contract_path '$contract_path' declared but not yet present ($required_stability — acceptable)"
|
|
224
498
|
((WARNINGS++))
|
|
225
499
|
fi
|
|
226
500
|
fi
|
|
@@ -241,31 +515,46 @@ print_status_table() {
|
|
|
241
515
|
log_section "Dependency Status Table"
|
|
242
516
|
|
|
243
517
|
local app_id
|
|
244
|
-
app_id="$(yq e ".app_id // \"unknown\"" "$APP_DEPS_FILE")"
|
|
518
|
+
app_id="$(yq e ".app_id // \"unknown\"" "$APP_DEPS_FILE" 2>/dev/null || echo unknown)"
|
|
245
519
|
log_info "App: $app_id"
|
|
246
520
|
|
|
247
521
|
printf "\n %-30s %-15s %-12s %s\n" "Service ID" "Stability" "Contract" "Status"
|
|
248
522
|
printf " %-30s %-15s %-12s %s\n" "──────────────────────────────" "───────────────" "────────────" "──────────"
|
|
249
523
|
|
|
250
|
-
local count
|
|
251
|
-
|
|
524
|
+
local source_file count
|
|
525
|
+
for source_file in "${DEP_SOURCES[@]}"; do
|
|
526
|
+
count="$(quiet_dep_count "$source_file")"
|
|
527
|
+
[[ "$count" -gt 0 ]] || continue
|
|
528
|
+
if [[ ${#DEP_SOURCES[@]} -gt 1 ]]; then
|
|
529
|
+
printf " %s\n" "── ${source_file#"$APP_ROOT"/}"
|
|
530
|
+
fi
|
|
252
531
|
|
|
253
532
|
for ((i = 0; i < count; i++)); do
|
|
254
533
|
local service_id required_stability contract_type contract_path dep_status
|
|
255
|
-
service_id="$(yq e ".platform_dependencies[$i].service_id // \"(missing)\"" "$
|
|
256
|
-
required_stability="$(yq e ".platform_dependencies[$i].required_stability // \"(missing)\"" "$
|
|
257
|
-
contract_type="$(yq e ".platform_dependencies[$i].contract_type // \"(missing)\"" "$
|
|
258
|
-
contract_path="$(yq e ".platform_dependencies[$i].contract_path // \"\"" "$
|
|
534
|
+
service_id="$(yq e ".platform_dependencies[$i].service_id // \"(missing)\"" "$source_file")"
|
|
535
|
+
required_stability="$(yq e ".platform_dependencies[$i].required_stability // \"(missing)\"" "$source_file")"
|
|
536
|
+
contract_type="$(yq e ".platform_dependencies[$i].contract_type // \"(missing)\"" "$source_file")"
|
|
537
|
+
contract_path="$(yq e ".platform_dependencies[$i].contract_path // \"\"" "$source_file")"
|
|
259
538
|
|
|
260
539
|
# Determine display status
|
|
261
540
|
if [[ "$required_stability" == "locked" ]]; then
|
|
262
|
-
if [[ -
|
|
541
|
+
if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
|
|
542
|
+
dep_status="${RED}LOCKED UNMET${NC}"
|
|
543
|
+
elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
544
|
+
# A declared path that was never resolved is not "OK" and not "UNMET".
|
|
545
|
+
# Reporting either would be a claim the run cannot support.
|
|
546
|
+
dep_status="${YELLOW}LOCKED UNRESOLVED${NC}"
|
|
547
|
+
elif [[ -f "$PLATFORM_ROOT/$contract_path" ]]; then
|
|
263
548
|
dep_status="${GREEN}LOCKED OK${NC}"
|
|
264
549
|
else
|
|
265
550
|
dep_status="${RED}LOCKED UNMET${NC}"
|
|
266
551
|
fi
|
|
267
552
|
elif [[ "$required_stability" == "provisional" ]]; then
|
|
268
553
|
dep_status="${YELLOW}PROVISIONAL${NC}"
|
|
554
|
+
elif [[ "$required_stability" == "unknown" ]]; then
|
|
555
|
+
dep_status="${YELLOW}UNKNOWN${NC}"
|
|
556
|
+
elif [[ " ${DEPRECATED_STABILITY_VALUES[*]} " == *" $required_stability "* ]]; then
|
|
557
|
+
dep_status="${YELLOW}NOT A STABILITY VALUE${NC}"
|
|
269
558
|
else
|
|
270
559
|
dep_status="${RED}INVALID${NC}"
|
|
271
560
|
fi
|
|
@@ -273,6 +562,7 @@ print_status_table() {
|
|
|
273
562
|
printf " %-30s %-15s %-12s " "$service_id" "$required_stability" "$contract_type"
|
|
274
563
|
echo -e "$dep_status"
|
|
275
564
|
done
|
|
565
|
+
done
|
|
276
566
|
echo ""
|
|
277
567
|
}
|
|
278
568
|
|
|
@@ -286,20 +576,33 @@ main() {
|
|
|
286
576
|
|
|
287
577
|
log_section "Validating Dependencies"
|
|
288
578
|
|
|
289
|
-
local dep_count
|
|
290
|
-
|
|
291
|
-
|
|
579
|
+
local source_file dep_count
|
|
580
|
+
TOTAL_DEPS=0
|
|
581
|
+
for source_file in "${DEP_SOURCES[@]}"; do
|
|
582
|
+
read_source_dep_count "$source_file"
|
|
583
|
+
dep_count="$SOURCE_DEP_COUNT"
|
|
584
|
+
[[ "$dep_count" -gt 0 ]] || continue
|
|
585
|
+
if [[ ${#DEP_SOURCES[@]} -gt 1 ]]; then
|
|
586
|
+
log_info "${source_file#"$APP_ROOT"/}: $dep_count dependency(ies)"
|
|
587
|
+
fi
|
|
588
|
+
TOTAL_DEPS=$((TOTAL_DEPS + dep_count))
|
|
589
|
+
for ((i = 0; i < dep_count; i++)); do
|
|
590
|
+
validate_dependency "$source_file" "$i" || true
|
|
591
|
+
done
|
|
592
|
+
done
|
|
292
593
|
|
|
293
|
-
if [[ "$
|
|
294
|
-
|
|
594
|
+
if [[ "$TOTAL_DEPS" -eq 0 ]]; then
|
|
595
|
+
if [[ "$UNPARSEABLE_SOURCES" -gt 0 ]]; then
|
|
596
|
+
# Zero dependencies BECAUSE the source could not be read is not the
|
|
597
|
+
# same result as zero dependencies declared, and must not exit alike.
|
|
598
|
+
log_fail "$UNPARSEABLE_SOURCES dependency source(s) could not be parsed — 0 dependencies checked, which is an absence of evidence, not a pass."
|
|
599
|
+
exit 1
|
|
600
|
+
fi
|
|
601
|
+
log_warn "No platform_dependencies entries found in ${#DEP_SOURCES[@]} source(s)"
|
|
295
602
|
log_info "If this app has no platform deps, this is acceptable."
|
|
296
603
|
exit 0
|
|
297
604
|
fi
|
|
298
605
|
|
|
299
|
-
for ((i = 0; i < dep_count; i++)); do
|
|
300
|
-
validate_dependency "$i" || true
|
|
301
|
-
done
|
|
302
|
-
|
|
303
606
|
print_status_table
|
|
304
607
|
|
|
305
608
|
log_section "Summary"
|
|
@@ -308,11 +611,28 @@ main() {
|
|
|
308
611
|
log_info "Warnings: $WARNINGS"
|
|
309
612
|
log_info "Failures: $FAILURES"
|
|
310
613
|
|
|
614
|
+
if [[ "$UNPARSEABLE_SOURCES" -gt 0 ]]; then
|
|
615
|
+
log_fail "$UNPARSEABLE_SOURCES dependency source(s) could not be parsed. Whatever they declare was checked by nothing, so no verdict here covers them."
|
|
616
|
+
exit 1
|
|
617
|
+
fi
|
|
618
|
+
|
|
311
619
|
if [[ "$FAILURES" -gt 0 ]]; then
|
|
312
620
|
log_fail "$FAILURES dependency check(s) failed. Stage 6 lock-in gate NOT met."
|
|
313
621
|
exit 1
|
|
314
622
|
fi
|
|
315
623
|
|
|
624
|
+
# Without a platform surface the gate cannot claim to have been met, however
|
|
625
|
+
# clean the checks it DID run. Naming the un-run checks and their denominator
|
|
626
|
+
# is the whole point: "no failures" and "the gate is met" are different claims,
|
|
627
|
+
# and conflating them is how this validator came to report green for months
|
|
628
|
+
# while never executing (M-ADR 0200 §4.3).
|
|
629
|
+
if [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
630
|
+
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)."
|
|
631
|
+
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)."
|
|
632
|
+
log_info "This is a partial run, not a met gate — pass --platform-root to complete it."
|
|
633
|
+
exit 0
|
|
634
|
+
fi
|
|
635
|
+
|
|
316
636
|
if [[ "$WARNINGS" -gt 0 ]]; then
|
|
317
637
|
log_warn "All required checks passed with $WARNINGS warning(s)."
|
|
318
638
|
else
|