@sdd-method/verify-cli 0.1.0 → 0.2.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/baseline-check.js +17 -2
- package/dist/lib/catalogue/canonical-schema.yaml +476 -10
- package/dist/lib/catalogue/coverage-render.js +30 -0
- package/dist/lib/catalogue/engine-validator.js +17 -0
- package/dist/lib/catalogue/graph-projection.js +17 -0
- package/dist/lib/catalogue/impact-run.js +2 -2
- package/dist/lib/catalogue/manifest-loader.js +9 -0
- package/dist/lib/catalogue/platform-scope.js +7 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +91 -2
- package/dist/lib/catalogue/scores-coverage.js +38 -0
- package/dist/lib/catalogue/validate.js +33 -9
- package/dist/lib/validate/aggregate.js +31 -15
- package/dist/lib/validate/bundle-coherence.js +7 -2
- package/dist/lib/validate/dispatch.js +25 -0
- package/dist/lib/validate/index.js +32 -10
- package/dist/lib/validate/yaml-parse.js +112 -0
- package/dist/verbs/catalogue-verify.js +1 -1
- package/package.json +2 -1
- package/scripts/validate-app-dependencies.sh +664 -0
- package/scripts/validate-app-docs.sh +199 -0
- package/scripts/validate-integration-docs.sh +505 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# validate-integration-docs.sh - Validate integration repo documentation structure
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./validate-integration-docs.sh [--repo-root <path>]
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
11
|
+
REPO_ROOT="$ROOT_DIR"
|
|
12
|
+
EXPECTED_REPO_KIND="integration-product"
|
|
13
|
+
|
|
14
|
+
RED='\033[0;31m'
|
|
15
|
+
GREEN='\033[0;32m'
|
|
16
|
+
YELLOW='\033[1;33m'
|
|
17
|
+
BLUE='\033[0;34m'
|
|
18
|
+
NC='\033[0m'
|
|
19
|
+
|
|
20
|
+
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
21
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
22
|
+
log_fail() { echo -e "${RED}[FAIL]${NC} $1"; }
|
|
23
|
+
log_pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
|
|
24
|
+
log_section() {
|
|
25
|
+
echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
|
26
|
+
echo -e "${BLUE} $1${NC}"
|
|
27
|
+
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
while [[ $# -gt 0 ]]; do
|
|
31
|
+
case "$1" in
|
|
32
|
+
--repo-root)
|
|
33
|
+
REPO_ROOT="${2:-}"
|
|
34
|
+
shift 2
|
|
35
|
+
;;
|
|
36
|
+
-h|--help)
|
|
37
|
+
echo "Usage: $(basename "$0") [--repo-root <path>]"
|
|
38
|
+
exit 0
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
log_fail "Unknown argument: $1"
|
|
42
|
+
exit 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
if [[ ! -d "$REPO_ROOT" ]]; then
|
|
48
|
+
log_fail "Repo root not found: $REPO_ROOT"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
command -v yq >/dev/null 2>&1 || {
|
|
53
|
+
log_fail "yq is required. Install it before running $(basename "$0")."
|
|
54
|
+
exit 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
FAILURES=0
|
|
58
|
+
|
|
59
|
+
check_repo_kind() {
|
|
60
|
+
log_section "Group 1: Repo Kind"
|
|
61
|
+
|
|
62
|
+
local marker="$REPO_ROOT/.sdd-repo-kind"
|
|
63
|
+
if [[ ! -f "$marker" ]]; then
|
|
64
|
+
log_fail ".sdd-repo-kind not found"
|
|
65
|
+
FAILURES=$((FAILURES + 1))
|
|
66
|
+
return
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
local value
|
|
70
|
+
value="$(tr -d '[:space:]' < "$marker")"
|
|
71
|
+
if [[ "$value" != "$EXPECTED_REPO_KIND" ]]; then
|
|
72
|
+
log_fail "Expected repo kind '$EXPECTED_REPO_KIND', found '$value'"
|
|
73
|
+
FAILURES=$((FAILURES + 1))
|
|
74
|
+
else
|
|
75
|
+
log_pass "Repo kind marker present: $value"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
check_required_files() {
|
|
80
|
+
log_section "Group 2: Required Files"
|
|
81
|
+
|
|
82
|
+
local files=(
|
|
83
|
+
"integration-manifest.yaml"
|
|
84
|
+
"docs/service-architecture.md"
|
|
85
|
+
"docs/method/methods/templates/feature-grounding-integration-template.md"
|
|
86
|
+
"docs/method/methods/workflows/integration-delivery-workflow-guide.md"
|
|
87
|
+
"docs/method/methods/templates/integration-service-architecture-template.md"
|
|
88
|
+
)
|
|
89
|
+
local rel
|
|
90
|
+
|
|
91
|
+
for rel in "${files[@]}"; do
|
|
92
|
+
if [[ -f "$REPO_ROOT/$rel" ]]; then
|
|
93
|
+
log_pass "$rel found"
|
|
94
|
+
else
|
|
95
|
+
log_fail "$rel missing"
|
|
96
|
+
FAILURES=$((FAILURES + 1))
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
check_manifest() {
|
|
102
|
+
log_section "Group 3: Manifest Structure"
|
|
103
|
+
|
|
104
|
+
local manifest="$REPO_ROOT/integration-manifest.yaml"
|
|
105
|
+
if [[ ! -f "$manifest" ]]; then
|
|
106
|
+
log_fail "integration-manifest.yaml missing; cannot validate structure"
|
|
107
|
+
FAILURES=$((FAILURES + 1))
|
|
108
|
+
return
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
local required_scalars=("integrationId" "providerName" "version" "profileType" "sdkVersion" "publishedBy" "bindingModel")
|
|
112
|
+
local field value
|
|
113
|
+
for field in "${required_scalars[@]}"; do
|
|
114
|
+
value="$(yq e ".${field} // \"\"" "$manifest")"
|
|
115
|
+
if [[ -z "$value" || "$value" == "null" || "$value" == "TBD" ]]; then
|
|
116
|
+
log_fail "integration-manifest.yaml: '$field' missing or placeholder"
|
|
117
|
+
FAILURES=$((FAILURES + 1))
|
|
118
|
+
else
|
|
119
|
+
log_pass "integration-manifest.yaml: '$field' set"
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
|
|
123
|
+
local inbound outbound asset_count profile
|
|
124
|
+
inbound="$(yq e '.capabilityFlags.inbound' "$manifest")"
|
|
125
|
+
outbound="$(yq e '.capabilityFlags.outbound' "$manifest")"
|
|
126
|
+
profile="$(yq e '.profileType // ""' "$manifest")"
|
|
127
|
+
asset_count="$(yq e '.supportedAssetTypes | length' "$manifest" 2>/dev/null || echo 0)"
|
|
128
|
+
|
|
129
|
+
if [[ "$inbound" != "true" && "$inbound" != "false" ]]; then
|
|
130
|
+
log_fail "integration-manifest.yaml: capabilityFlags.inbound must be true or false"
|
|
131
|
+
FAILURES=$((FAILURES + 1))
|
|
132
|
+
else
|
|
133
|
+
log_pass "integration-manifest.yaml: capabilityFlags.inbound set"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
if [[ "$outbound" != "true" && "$outbound" != "false" ]]; then
|
|
137
|
+
log_fail "integration-manifest.yaml: capabilityFlags.outbound must be true or false"
|
|
138
|
+
FAILURES=$((FAILURES + 1))
|
|
139
|
+
else
|
|
140
|
+
log_pass "integration-manifest.yaml: capabilityFlags.outbound set"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [[ "$asset_count" -lt 1 ]]; then
|
|
144
|
+
log_fail "integration-manifest.yaml: supportedAssetTypes must contain at least one value"
|
|
145
|
+
FAILURES=$((FAILURES + 1))
|
|
146
|
+
else
|
|
147
|
+
log_pass "integration-manifest.yaml: supportedAssetTypes populated"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
case "$profile" in
|
|
151
|
+
workflow|financial-services|data-hub|data-feed)
|
|
152
|
+
if [[ "$outbound" != "false" ]]; then
|
|
153
|
+
log_fail "integration-manifest.yaml: profileType '$profile' should default outbound capability to false"
|
|
154
|
+
FAILURES=$((FAILURES + 1))
|
|
155
|
+
else
|
|
156
|
+
log_pass "integration-manifest.yaml: inbound-only profile directionality is coherent"
|
|
157
|
+
fi
|
|
158
|
+
;;
|
|
159
|
+
*)
|
|
160
|
+
;;
|
|
161
|
+
esac
|
|
162
|
+
|
|
163
|
+
# Interaction patterns (ADR 0171) — optional fields, validated only when present.
|
|
164
|
+
# Existing manifests predate the field; absence is not a failure. Directionality
|
|
165
|
+
# mismatch (pattern present but the matching capability flag is false) is a warning.
|
|
166
|
+
local ingestion invocation
|
|
167
|
+
ingestion="$(yq e '.ingestionPattern // ""' "$manifest")"
|
|
168
|
+
invocation="$(yq e '.invocationPattern // ""' "$manifest")"
|
|
169
|
+
|
|
170
|
+
if [[ -n "$ingestion" && "$ingestion" != "null" ]]; then
|
|
171
|
+
case "$ingestion" in
|
|
172
|
+
event-stream-consume|webhook|socket-listen|poll)
|
|
173
|
+
log_pass "integration-manifest.yaml: ingestionPattern '$ingestion' valid"
|
|
174
|
+
# if/fi for the same reason as the invocationPattern warn below:
|
|
175
|
+
# safe only because more code follows, which is not a property
|
|
176
|
+
# worth relying on.
|
|
177
|
+
if [[ "$inbound" != "true" ]]; then
|
|
178
|
+
log_warn "integration-manifest.yaml: ingestionPattern set but capabilityFlags.inbound is not true"
|
|
179
|
+
fi
|
|
180
|
+
;;
|
|
181
|
+
*)
|
|
182
|
+
log_fail "integration-manifest.yaml: ingestionPattern '$ingestion' not in {event-stream-consume, webhook, socket-listen, poll} — if this provider is none of the four, pick the pattern matching the MECHANICS and move the distinguishing semantics to patternQualifier (free-text); minting a pattern value is a method change"
|
|
183
|
+
FAILURES=$((FAILURES + 1))
|
|
184
|
+
;;
|
|
185
|
+
esac
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
if [[ -n "$invocation" && "$invocation" != "null" ]]; then
|
|
189
|
+
case "$invocation" in
|
|
190
|
+
request-response|async-request|publish)
|
|
191
|
+
log_pass "integration-manifest.yaml: invocationPattern '$invocation' valid"
|
|
192
|
+
# Explicit if/fi, NOT `[[ ... ]] && log_warn`. As the last
|
|
193
|
+
# statement in this function, a false test made the function
|
|
194
|
+
# return 1, and under `set -e` that aborted the whole run before
|
|
195
|
+
# Group 4 — so the check fired on the CORRECT case (a manifest
|
|
196
|
+
# that declares invocationPattern and properly sets
|
|
197
|
+
# outbound: true), and the better-formed the manifest, the more
|
|
198
|
+
# certainly its grounding evidence went unchecked.
|
|
199
|
+
if [[ "$outbound" != "true" ]]; then
|
|
200
|
+
log_warn "integration-manifest.yaml: invocationPattern set but capabilityFlags.outbound is not true"
|
|
201
|
+
fi
|
|
202
|
+
;;
|
|
203
|
+
*)
|
|
204
|
+
log_fail "integration-manifest.yaml: invocationPattern '$invocation' not in {request-response, async-request, publish}"
|
|
205
|
+
FAILURES=$((FAILURES + 1))
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
fi
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Unedited-scaffold-residue lint (WARN-only).
|
|
213
|
+
#
|
|
214
|
+
# WHAT THIS MEASURES, and what it deliberately does not: residue measures
|
|
215
|
+
# **authoring completeness** — how much of the scaffold is still untouched. It is
|
|
216
|
+
# NOT a signal for whether an SDD models a real provider. Those are independent
|
|
217
|
+
# properties, measured to be so: on one estate 5 SDDs are demonstrations and 11
|
|
218
|
+
# carry residue, because six REAL integrations are simply not authored yet. A
|
|
219
|
+
# residue list is therefore an authoring WORK QUEUE, never a census of
|
|
220
|
+
# demonstrations. Do not re-couple it to the `exemplar` trait.
|
|
221
|
+
#
|
|
222
|
+
# THE TEMPLATE IS THE ORACLE. Residue is defined as template text that survived
|
|
223
|
+
# into the authored document, so the shipped template is read at runtime and the
|
|
224
|
+
# comparison is exact. This matters for two reasons:
|
|
225
|
+
# 1. It is self-maintaining. An earlier attempt at this check hardcoded a list
|
|
226
|
+
# of placeholder strings; the list silently went stale, so tokens from other
|
|
227
|
+
# templates read as real content.
|
|
228
|
+
# 2. It separates residue from a brace that is legitimately authored. A JSON
|
|
229
|
+
# example containing `{` is not residue; the exact string `{provider field}`
|
|
230
|
+
# is, because the template is where it comes from.
|
|
231
|
+
#
|
|
232
|
+
# WHAT IS LEGITIMATELY RETAINED — state this half or the rule flags correct
|
|
233
|
+
# authoring. Template text kept as STRUCTURE is right and must never be reported:
|
|
234
|
+
# - section headings (`^#`) — a cohort of SDDs sharing one standard deliberately
|
|
235
|
+
# keeps the method's section shape while filling it with real content, and that
|
|
236
|
+
# is exactly correct authoring;
|
|
237
|
+
# - table header and separator rows (`^|`);
|
|
238
|
+
# - short shared lines — a bare path, a field label — dropped by a length floor.
|
|
239
|
+
# Residue is template text left IN THE SLOTS, never template text AS STRUCTURE.
|
|
240
|
+
#
|
|
241
|
+
# THREE SHAPES OF RESIDUE, and only two are detectable:
|
|
242
|
+
# 1. Unfilled placeholder tokens — detected (exact token match).
|
|
243
|
+
# 2. Leftover instruction prose — detected (exact line match).
|
|
244
|
+
# 3. Unmodified example table rows — NOT ATTEMPTED, on evidence rather than
|
|
245
|
+
# difficulty: every placeholder-free table row in the shipped templates is a
|
|
246
|
+
# table HEADER row, which an authored document is supposed to retain. Rows
|
|
247
|
+
# that do carry placeholders are already caught by shape 1. So verbatim-row
|
|
248
|
+
# matching would flag precisely the lines that should survive and add no
|
|
249
|
+
# signal that shape 1 misses.
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
# Templates that may reach an integration SDD (forward via integration-baseline,
|
|
253
|
+
# reverse via reverse-extension-baseline). Whichever are present are used.
|
|
254
|
+
GROUNDING_TEMPLATES=(
|
|
255
|
+
"docs/method/methods/templates/feature-grounding-integration-template.md"
|
|
256
|
+
"docs/method/methods/reverse/templates/integration-product/docs/design/integration/feature-grounding.md"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Cap on reported items per class — an untouched scaffold would otherwise bury
|
|
260
|
+
# the rest of the run. The count is always reported in full, per no-silent-caps.
|
|
261
|
+
RESIDUE_REPORT_CAP=5
|
|
262
|
+
|
|
263
|
+
check_grounding_residue() {
|
|
264
|
+
local grounding="$1"
|
|
265
|
+
local rel="docs/design/integration/feature-grounding.md"
|
|
266
|
+
|
|
267
|
+
local -a templates=()
|
|
268
|
+
local t
|
|
269
|
+
for t in "${GROUNDING_TEMPLATES[@]}"; do
|
|
270
|
+
[[ -f "$REPO_ROOT/$t" ]] && templates+=("$REPO_ROOT/$t")
|
|
271
|
+
done
|
|
272
|
+
|
|
273
|
+
# Never pass silently for want of an oracle: "no residue found" and "could
|
|
274
|
+
# not look" must not be the same output.
|
|
275
|
+
if [[ ${#templates[@]} -eq 0 ]]; then
|
|
276
|
+
log_warn "$rel: cannot check scaffold residue — no grounding template present in this repo (expected one of: ${GROUNDING_TEMPLATES[*]})"
|
|
277
|
+
return
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# --- Shape 1: unfilled placeholder tokens -------------------------------
|
|
281
|
+
# `|| true` on every match pipeline below: grep exits 1 on no-match, and
|
|
282
|
+
# under `set -euo pipefail` that aborts the run at the assignment. "Found
|
|
283
|
+
# nothing" is the expected result for a well-authored document, so it must not
|
|
284
|
+
# be fatal — this is the same trap that was silently killing Group 4.
|
|
285
|
+
local tokens
|
|
286
|
+
tokens="$(grep -ohE '\{[^}]{1,80}\}' "${templates[@]}" 2>/dev/null | sort -u || true)"
|
|
287
|
+
|
|
288
|
+
local -a hits=()
|
|
289
|
+
local token
|
|
290
|
+
while IFS= read -r token; do
|
|
291
|
+
[[ -z "$token" ]] && continue
|
|
292
|
+
local lines
|
|
293
|
+
lines="$(grep -Fn -- "$token" "$grounding" 2>/dev/null | cut -d: -f1 | paste -sd, - || true)"
|
|
294
|
+
[[ -n "$lines" ]] && hits+=("line ${lines}: ${token}")
|
|
295
|
+
done <<< "$tokens"
|
|
296
|
+
|
|
297
|
+
if [[ ${#hits[@]} -gt 0 ]]; then
|
|
298
|
+
log_warn "$rel: ${#hits[@]} unfilled placeholder token(s) from the shipped template — authoring incomplete (not a demonstration marker)"
|
|
299
|
+
local i
|
|
300
|
+
for ((i = 0; i < ${#hits[@]} && i < RESIDUE_REPORT_CAP; i++)); do
|
|
301
|
+
echo " - ${hits[$i]}"
|
|
302
|
+
done
|
|
303
|
+
if [[ ${#hits[@]} -gt $RESIDUE_REPORT_CAP ]]; then
|
|
304
|
+
echo " - … and $(( ${#hits[@]} - RESIDUE_REPORT_CAP )) more"
|
|
305
|
+
fi
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
# --- Shape 2: leftover template instruction prose -----------------------
|
|
309
|
+
# Prose only: headings and table rows are structure an authored document
|
|
310
|
+
# legitimately keeps. The length floor drops short shared lines (a bare path
|
|
311
|
+
# or a field label) that carry no instruction and would false-positive.
|
|
312
|
+
local -a prose_hits=()
|
|
313
|
+
local line
|
|
314
|
+
while IFS= read -r line; do
|
|
315
|
+
[[ ${#line} -lt 60 ]] && continue
|
|
316
|
+
if grep -Fxq -- "$line" "$grounding" 2>/dev/null; then
|
|
317
|
+
prose_hits+=("$line")
|
|
318
|
+
fi
|
|
319
|
+
done < <(grep -hvE '^#|^\||^$|^[[:space:]]*$' "${templates[@]}" 2>/dev/null | sort -u || true)
|
|
320
|
+
|
|
321
|
+
if [[ ${#prose_hits[@]} -gt 0 ]]; then
|
|
322
|
+
log_warn "$rel: ${#prose_hits[@]} line(s) of template instruction prose retained verbatim"
|
|
323
|
+
local j
|
|
324
|
+
for ((j = 0; j < ${#prose_hits[@]} && j < RESIDUE_REPORT_CAP; j++)); do
|
|
325
|
+
local shown="${prose_hits[$j]}"
|
|
326
|
+
[[ ${#shown} -gt 90 ]] && shown="${shown:0:90}…"
|
|
327
|
+
echo " - ${shown}"
|
|
328
|
+
done
|
|
329
|
+
if [[ ${#prose_hits[@]} -gt $RESIDUE_REPORT_CAP ]]; then
|
|
330
|
+
echo " - … and $(( ${#prose_hits[@]} - RESIDUE_REPORT_CAP )) more"
|
|
331
|
+
fi
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
if [[ ${#hits[@]} -eq 0 && ${#prose_hits[@]} -eq 0 ]]; then
|
|
335
|
+
log_pass "$rel: no unedited scaffold residue detected"
|
|
336
|
+
fi
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
check_grounding_doc() {
|
|
340
|
+
log_section "Group 4: Grounding Evidence"
|
|
341
|
+
|
|
342
|
+
local grounding="$REPO_ROOT/docs/design/integration/feature-grounding.md"
|
|
343
|
+
if [[ ! -f "$grounding" ]]; then
|
|
344
|
+
log_warn "docs/design/integration/feature-grounding.md not found — create before lock-in"
|
|
345
|
+
return
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
local manifest="$REPO_ROOT/integration-manifest.yaml"
|
|
349
|
+
local outbound
|
|
350
|
+
outbound="$(yq e '.capabilityFlags.outbound' "$manifest" 2>/dev/null || echo "")"
|
|
351
|
+
|
|
352
|
+
local sections=("## Overview" "## Platform Contract Boundary" "## Provider-to-Canonical Field Mapping" "## Identity Resolution and Routing" "## Security and Secrets")
|
|
353
|
+
local section
|
|
354
|
+
local missing=0
|
|
355
|
+
for section in "${sections[@]}"; do
|
|
356
|
+
if ! grep -q "^${section}" "$grounding"; then
|
|
357
|
+
log_fail "docs/design/integration/feature-grounding.md missing section '$section'"
|
|
358
|
+
FAILURES=$((FAILURES + 1))
|
|
359
|
+
missing=1
|
|
360
|
+
fi
|
|
361
|
+
done
|
|
362
|
+
|
|
363
|
+
if [[ "$missing" -eq 0 ]]; then
|
|
364
|
+
log_pass "docs/design/integration/feature-grounding.md contains required sections"
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [[ "$outbound" == "true" ]]; then
|
|
368
|
+
if grep -q "^## Command Mapping" "$grounding"; then
|
|
369
|
+
log_pass "docs/design/integration/feature-grounding.md includes command mapping for bidirectional integration"
|
|
370
|
+
else
|
|
371
|
+
log_fail "docs/design/integration/feature-grounding.md missing section '## Command Mapping' for bidirectional integration"
|
|
372
|
+
FAILURES=$((FAILURES + 1))
|
|
373
|
+
fi
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
# Substance, not just presence. The checks above establish that the required
|
|
377
|
+
# sections EXIST; this one asks whether they were filled in. WARN-only and
|
|
378
|
+
# never incrementing FAILURES: an estate must not turn red for being honest
|
|
379
|
+
# about its authoring state, and an exploration-stage integration is a
|
|
380
|
+
# legitimate thing to have.
|
|
381
|
+
check_grounding_residue "$grounding"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
main() {
|
|
385
|
+
log_section "Integration Documentation Validation"
|
|
386
|
+
log_info "Repo root: $REPO_ROOT"
|
|
387
|
+
|
|
388
|
+
check_repo_kind
|
|
389
|
+
check_required_files
|
|
390
|
+
check_manifest
|
|
391
|
+
check_grounding_doc
|
|
392
|
+
check_components_declaration
|
|
393
|
+
|
|
394
|
+
log_section "Summary"
|
|
395
|
+
|
|
396
|
+
if [[ "$FAILURES" -eq 0 ]]; then
|
|
397
|
+
log_pass "All integration documentation checks passed."
|
|
398
|
+
exit 0
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
log_fail "$FAILURES check(s) failed."
|
|
402
|
+
exit 1
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Components declaration (M-ADR 0212-era coordination: the agreed four-state design).
|
|
406
|
+
#
|
|
407
|
+
# Asks only for the INVENTORY, never for per-component patterns: `id` is the sole
|
|
408
|
+
# required field of a components[] entry, so an inventory-only block is complete and
|
|
409
|
+
# conformant. Measured authoring cost was under a minute per SDD for the inventory and
|
|
410
|
+
# up to twenty-five for the patterns, and a warn cannot make the second cheaper — a
|
|
411
|
+
# warn nobody can afford to clear becomes a warn everybody filters.
|
|
412
|
+
#
|
|
413
|
+
# Counts components from each repo's OWN `profile` in reverse-metadata.yaml, never from
|
|
414
|
+
# catalogue row counts: catalogue rows include support repos, so row counting over-counts
|
|
415
|
+
# every SDD that mirrors a shared library.
|
|
416
|
+
#
|
|
417
|
+
# Zero is three different facts and they are reported separately. Collapsing them is what
|
|
418
|
+
# would bury the only one that is a defect.
|
|
419
|
+
check_components_declaration() {
|
|
420
|
+
local manifest="$REPO_ROOT/integration-manifest.yaml"
|
|
421
|
+
[[ -f "$manifest" ]] || return 0
|
|
422
|
+
|
|
423
|
+
local repos_dir="$REPO_ROOT/repos"
|
|
424
|
+
local components=0 support=0 unreadable=0
|
|
425
|
+
|
|
426
|
+
if [[ -d "$repos_dir" ]]; then
|
|
427
|
+
local d meta profile
|
|
428
|
+
for d in "$repos_dir"/*/; do
|
|
429
|
+
meta="$d/reverse-metadata.yaml"
|
|
430
|
+
[[ -f "$meta" ]] || continue
|
|
431
|
+
# A file that EXISTS and yields no profile is not the same fact as one that
|
|
432
|
+
# is absent, and the fallback treats them identically. Say so — this is the
|
|
433
|
+
# defect that hid a mis-typed repo through an otherwise correct rebuild.
|
|
434
|
+
if ! profile="$(yq e '.profile // ""' "$meta" 2>/dev/null)" || [[ -z "$profile" || "$profile" == "null" ]]; then
|
|
435
|
+
unreadable=$((unreadable + 1))
|
|
436
|
+
log_warn "repos/$(basename "$d")/reverse-metadata.yaml: no readable \`profile\` — this repo is not counted, so the component total below may be low"
|
|
437
|
+
continue
|
|
438
|
+
fi
|
|
439
|
+
case "$profile" in
|
|
440
|
+
integration-product) components=$((components + 1)) ;;
|
|
441
|
+
support-repo) support=$((support + 1)) ;;
|
|
442
|
+
esac
|
|
443
|
+
done
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
local catalogue="$REPO_ROOT/catalogues/repositories.csv"
|
|
447
|
+
local catalogued=-1
|
|
448
|
+
if [[ -f "$catalogue" ]]; then
|
|
449
|
+
catalogued="$(awk -F, 'NR>1 && $5=="integration-product"' "$catalogue" 2>/dev/null | wc -l)"
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
# Fall back to the catalogue only when there is no per-repo evidence at all.
|
|
453
|
+
if [[ "$components" -eq 0 && "$support" -eq 0 && "$unreadable" -eq 0 && "$catalogued" -gt 0 ]]; then
|
|
454
|
+
components="$catalogued"
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
local declared="no"
|
|
458
|
+
yq e 'has("components")' "$manifest" 2>/dev/null | grep -q true && declared="yes"
|
|
459
|
+
|
|
460
|
+
# (c) The two sources disagree in the one direction that is a defect: components
|
|
461
|
+
# exist on disk and the catalogue holds none. Reported whatever the count bucket,
|
|
462
|
+
# and deliberately louder than the declaration warn — it is the only state here
|
|
463
|
+
# that means something is broken rather than merely undeclared.
|
|
464
|
+
if [[ "$components" -gt 0 && "$catalogued" -eq 0 ]]; then
|
|
465
|
+
log_warn "catalogues/repositories.csv holds NO integration-product rows while repos/ holds $components — the catalogue was never built for this SDD. Declaring components will not fix it; rebuild the catalogue."
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
if [[ "$components" -gt 1 ]]; then
|
|
469
|
+
if [[ "$declared" == "yes" ]]; then
|
|
470
|
+
local entries
|
|
471
|
+
entries="$(yq e '.components | length' "$manifest" 2>/dev/null || echo 0)"
|
|
472
|
+
[[ "$entries" =~ ^[0-9]+$ ]] || entries=0
|
|
473
|
+
# Asymmetric on purpose. Declaring MORE than repos/ holds is legitimate —
|
|
474
|
+
# components that are generated or held elsewhere are real and uncloned
|
|
475
|
+
# (the same case as the support-repo-only SDDs below). Declaring FEWER means
|
|
476
|
+
# mirrored components the inventory does not mention, which is a declaration
|
|
477
|
+
# that was true when written and quietly is not now.
|
|
478
|
+
if [[ "$entries" -lt "$components" ]]; then
|
|
479
|
+
log_warn "integration-manifest.yaml: \`components:\` declares $entries entr(ies) but repos/ holds $components component repos — the inventory omits $((components - entries)). A declared inventory that stops being true is worse than an absent one."
|
|
480
|
+
else
|
|
481
|
+
log_pass "integration-manifest.yaml: components declared ($entries entr(ies), $components component repos in repos/)"
|
|
482
|
+
fi
|
|
483
|
+
else
|
|
484
|
+
log_warn "integration-manifest.yaml: $components component repos in repos/, no \`components:\` block — the inventory is undeclared. Only \`id\` is required per entry; patterns are optional."
|
|
485
|
+
fi
|
|
486
|
+
return 0
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
if [[ "$components" -eq 1 ]]; then
|
|
490
|
+
log_pass "integration-manifest.yaml: single-component integration — the manifest's own patterns are the whole truth"
|
|
491
|
+
return 0
|
|
492
|
+
fi
|
|
493
|
+
|
|
494
|
+
# (a) Entries exist, none of them a component: the components live elsewhere.
|
|
495
|
+
if [[ "$support" -gt 0 ]]; then
|
|
496
|
+
log_info "0 component repos; $support support repo(s) — components are not cloned into this SDD (generated, or held elsewhere). Nothing owed."
|
|
497
|
+
return 0
|
|
498
|
+
fi
|
|
499
|
+
|
|
500
|
+
# (b) Nothing mirrored at all: a spec-first SDD being silent about components is
|
|
501
|
+
# a spec-first SDD behaving correctly.
|
|
502
|
+
log_info "0 component repos and nothing mirrored — no inventory to declare yet."
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
main "$@"
|