-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate_docs.sh
More file actions
executable file
·449 lines (429 loc) · 23.1 KB
/
Copy pathvalidate_docs.sh
File metadata and controls
executable file
·449 lines (429 loc) · 23.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
#!/usr/bin/env bash
# Validates internal consistency of base_docs documentation set.
# Exit 0 if clean, exit 1 if issues found.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
errors=0
echo "=== base_docs validation ==="
echo ""
# 1. Check for unfilled template placeholders in accepted/active files
# (skip files whose names contain "template" — those are expected to have placeholders)
# These are warnings only (non-blocking) since in the template repo some
# files are legitimately Accepted with placeholder dates.
echo "--- Checking for template placeholders in accepted/active files ---"
warnings=0
while IFS= read -r file; do
[[ -z "$file" ]] && continue
[[ "$file" == *template* ]] && continue
if grep -q 'YYYY-MM-DD' "$file"; then
echo " WARN: Unfilled date placeholder in $file"
warnings=$((warnings + 1))
fi
if grep -q '<roles / team>' "$file"; then
echo " WARN: Unfilled deciders placeholder in $file"
warnings=$((warnings + 1))
fi
if grep -q '<ClassName>' "$file"; then
echo " WARN: Unfilled ClassName placeholder in $file"
warnings=$((warnings + 1))
fi
done < <(grep -rl 'Status:.*\(Accepted\|Active\|Deferred\)' --include='*.md' . 2>/dev/null || true)
# `Deferred` added 2026-08-14 (C-09): the filter read Accepted|Active only, so
# ADR-004 -- the one Deferred ADR -- was never scanned for placeholders. It has
# none today, so this changes nothing now and closes the gap the entry named.
scanned=$(grep -rl 'Status:.*\(Accepted\|Active\|Deferred\)' --include='*.md' . 2>/dev/null | wc -l)
if [ "$warnings" -eq 0 ]; then
echo " OK: ${scanned} accepted/active/deferred file(s) scanned, no placeholders"
fi
# 2. The CIC list and the CIC directory must agree — IN BOTH DIRECTIONS.
#
# C-09. This check only ever walked the LIST, and the list says "None yet", so it
# iterated over zero items and had never executed its body. It could not fail, and
# a green run said so in exactly the same words as a run that checked something.
#
# That is C-55's shape — a guard that stands down whenever there is nothing to
# satisfy it — and C-55's remedy applies: an ALWAYS-RUNS COMPANION. Checking the
# reverse direction gives this something to assert at all times, because the
# directory can be enumerated whether or not the list has entries. Today both are
# empty, which passes while genuinely comparing two things.
#
# The reverse direction is also the more likely defect once Phase 1 starts: a CIC
# written and not listed is invisible to anyone reading CICs/README.md as the
# index it advertises itself to be.
echo "--- Checking CIC active contract references ---"
cic_listed=0
cic_files=0
if [ -f "CICs/README.md" ]; then
# forward: everything listed must exist
while IFS= read -r line; do
[[ -z "$line" ]] && continue
contract=$(echo "$line" | sed -n 's/^- `\(.*\.md\)`.*$/\1/p')
if [ -n "$contract" ]; then
cic_listed=$((cic_listed + 1))
if [ ! -f "CICs/$contract" ]; then
echo " ERROR: CIC contract listed but missing: CICs/$contract"
errors=$((errors + 1))
fi
fi
done < <(grep -E '^- `[A-Z].*\.md`' CICs/README.md 2>/dev/null | grep -v '>' || true)
# reverse: everything that exists must be listed
while IFS= read -r f; do
[[ -z "$f" ]] && continue
base=$(basename "$f")
case "$base" in README.md|cic_template.md) continue ;; esac
cic_files=$((cic_files + 1))
if ! grep -qF "\`$base\`" CICs/README.md 2>/dev/null; then
echo " ERROR: CIC contract exists but is not listed in CICs/README.md: $base"
echo " That index is what ADR-006 points readers at; an unlisted contract is invisible."
errors=$((errors + 1))
fi
done < <(find CICs -maxdepth 1 -name '*.md' 2>/dev/null || true)
echo " OK: ${cic_listed} listed, ${cic_files} contract file(s) — the two agree"
else
echo " ERROR: CICs/README.md is missing; ADR-006 requires it as the contract index"
errors=$((errors + 1))
fi
# 3. Cross-ADR reference integrity (constitutional ADRs 000-009 only;
# higher numbers are project-specific and not expected in the template repo)
# 3. Cross-ADR reference integrity.
#
# C-09. This read `ADR-00[0-9]` for two months, so it could not see ADR-010 or
# ADR-011 -- both of which exist and are cited 14 times between them. It reported
# a clean run over a set it could not reach.
#
# WIDENED to ADR-0[0-9][0-9], with three exclusions. All three are DECLARED here
# rather than inferred, and each was measured before being written:
#
# (a) FOREIGN ADRs. `views-postprocessing ADR-017`, `views-faoapi ADR-019` and
# the like belong to other repositories and must not resolve locally. A
# reference is foreign if its line names one of the repos below.
# (b) CANDIDATES. `docs/ADRs/README.md` lists ADR-012..015 as candidates that
# deliberately do not exist yet. Marked `(candidate)` at the point of use.
# (c) THE PLATFORM TIER, and this is the load-bearing one. `ADRs/platform/`
# cites foreign ADRs constantly and mostly WITHOUT naming the repo on the
# same line -- "Their ADR-017 §5", where the owner is established in
# surrounding prose. Measured: 11 such lines across the seam contract and
# the deployment pattern.
#
# Those are not defects to fix. The platform tier is not part of this
# repository's constitutional series -- ADR-011 draws exactly that
# distinction -- and editing a VERSIONED contract to satisfy a local linter
# would force a version bump, a new edition, a tag, and a re-pin decision
# for six consumers (§10), which is the cost C-73 registers. A cosmetic
# citation change is not worth an edition.
#
# So this check governs the constitutional tier and the docs around it. The
# platform tier's own citations are governed by §10's supersession discipline.
FOREIGN_ADR_REPOS='views-models\|views-faoapi\|views-crafdapi\|views-postprocessing\|views-pipeline-core\|views-datafactory'
echo "--- Checking cross-ADR references (constitutional 000-011; platform tier excluded) ---"
adr_refs_checked=0
while IFS= read -r ref; do
[[ -z "$ref" ]] && continue
file=$(echo "$ref" | cut -d: -f1)
adr_num=$(echo "$ref" | grep -oP 'ADR-0\K[0-9]{2}' | head -1)
if [ -n "$adr_num" ]; then
adr_refs_checked=$((adr_refs_checked + 1))
match_count=$(find ADRs -maxdepth 1 -name "0${adr_num}_*.md" 2>/dev/null | wc -l)
if [ "$match_count" -eq 0 ]; then
echo " ERROR: $file references ADR-0${adr_num} but no matching file found"
echo " If it belongs to another repo, name the repo on the same line."
echo " If it does not exist yet, mark it (candidate)."
errors=$((errors + 1))
fi
fi
done < <(grep -rn 'ADR-0[0-9][0-9]' --include='*.md' . 2>/dev/null \
| grep -v '^\./ADRs/platform/' \
| grep -v "$FOREIGN_ADR_REPOS" \
| grep -v '(candidate)' || true)
adr_files=$(find ADRs -maxdepth 1 -name '0[0-9][0-9]_*.md' | wc -l)
echo " OK: ${adr_refs_checked} reference(s) checked against ${adr_files} ADR file(s)"
# 4. Check that referenced protocol files exist
echo "--- Checking protocol file references ---"
proto_refs=0
while IFS= read -r ref; do
[[ -z "$ref" ]] && continue
file=$(echo "$ref" | cut -d: -f1)
proto=$(echo "$ref" | grep -oP 'contributor_protocols/[a-z_]+\.md' | head -1)
if [ -n "$proto" ]; then
proto_refs=$((proto_refs + 1))
if [ ! -f "$proto" ]; then
echo " ERROR: $file references $proto but file does not exist"
errors=$((errors + 1))
fi
fi
done < <(grep -rn 'contributor_protocols/' --include='*.md' . 2>/dev/null || true)
echo " OK: ${proto_refs} protocol reference(s) resolved"
# 5. Report template status markers
echo "--- Checking template status markers ---"
template_count=$(grep -rl '\-\-template\-\-' --include='*.md' . 2>/dev/null | wc -l)
echo " INFO: $template_count files still have --template-- status (expected in template repo)"
# 6. The seam contract resolves, and its retired alias still points somewhere.
# Check 3 above hard-codes ADR-00[0-9] and is structurally blind to the
# platform tier — which is this repo's only live artifact. þing-02 /falsify
# C-32: the one automated check could not see the one thing that ships.
# ADR-011 retired the PLATFORM-NNN scheme; this check follows the rename.
echo "--- Checking seam-contract references ---"
SEAM="ADRs/platform/appwrite_seam_contract.md"
if [ ! -f "$SEAM" ]; then
echo " ERROR: the seam contract is missing from $SEAM"
errors=$((errors + 1))
else
# The retired name must remain resolvable: anything still citing PLATFORM-001
# has to find the alias recorded in the contract, or the rename stranded it.
if grep -rqn 'PLATFORM-001' --include='*.md' . 2>/dev/null; then
if ! grep -q 'Former name' "$SEAM"; then
echo " ERROR: docs still cite PLATFORM-001 but $SEAM records no 'Former name' alias"
errors=$((errors + 1))
fi
fi
# No document may POINT AT the pre-rename path. A markdown link resolves and
# therefore breaks; inline code in prose is a quotation and does not. ADR-011
# quotes the retired filename as its own evidence, which must stay readable —
# so match link syntax, not every occurrence.
# Scanned from the REPO ROOT, not from docs/. The script cd's to docs/ at
# line 8, so every other check here is blind to README.md, reports/ and
# tests/ — and README.md is the likeliest place for a stale contract link.
# Verified by planting one: under the docs/-only scope it was not caught.
# This check widens its own scope only; the rest of the script is unchanged.
while IFS= read -r stale; do
[[ -z "$stale" ]] && continue
echo " ERROR: ${stale%%:*} links to the pre-rename contract path"
errors=$((errors + 1))
done < <(grep -rn '](\([^)]*\)\?PLATFORM-001_identity_secrets_configuration_contract\.md' \
--include='*.md' .. 2>/dev/null || true)
# In TOML and shell there is no prose, so any occurrence is a pointer.
while IFS= read -r stale; do
[[ -z "$stale" ]] && continue
case "${stale%%:*}" in ./validate_docs.sh) continue ;; esac
echo " ERROR: ${stale%%:*} references the pre-rename contract path"
errors=$((errors + 1))
done < <(grep -rn 'PLATFORM-001_identity_secrets_configuration_contract\.md' \
--include='*.toml' --include='*.sh' . 2>/dev/null || true)
_vd_p1_cites=$(grep -rl 'PLATFORM-001' --include='*.md' . 2>/dev/null | wc -l)
echo " OK: seam contract resolves; ${_vd_p1_cites} file(s) still cite PLATFORM-001, alias intact"
fi
# 7. Contract/registry version coherence
# Seam contract §10: "The registry and this contract version together."
# That rule was ratified and held by hand in two files. þing-02 /falsify
# C-33: nothing enforced it, and the v1.2.0 edit touched both.
echo "--- Checking contract/registry version coherence ---"
CONTRACT="$SEAM"
REGISTRY="ADRs/platform/coordinate_registry.toml"
PATTERN="ADRs/platform/consumer_api_deployment_pattern.md"
if [ -f "$CONTRACT" ] && [ -f "$REGISTRY" ]; then
contract_ver=$(grep -P '^\| Version \|' "$CONTRACT" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
registry_ver=$(grep -oP '^version = "\K[0-9.]+' "$REGISTRY" | head -1)
if [ -z "$contract_ver" ]; then
echo " ERROR: cannot parse a version from $CONTRACT (expected a '| Version |' row)"
errors=$((errors + 1))
elif [ -z "$registry_ver" ]; then
echo " ERROR: cannot parse a version from $REGISTRY (expected [meta] version = \"x.y.z\")"
errors=$((errors + 1))
elif [ "$contract_ver" != "$registry_ver" ]; then
echo " ERROR: the seam contract is v${contract_ver} but coordinate_registry.toml is v${registry_ver}"
echo " §10 requires them to version together. Bump both or neither."
errors=$((errors + 1))
else
echo " OK: contract and registry both at v${contract_ver}"
fi
else
echo " ERROR: contract or registry missing from ADRs/platform/"
errors=$((errors + 1))
fi
echo ""
echo "--- Checking that a changed contract/registry bumped its version ---"
# C-53. The check above compares the two versions TO EACH OTHER and to nothing
# else, so "bump neither" satisfies it perfectly. That is how four coordinates
# became canonical on 2026-08-02 while the registry still called itself v1.3.0 --
# green gate, broken rule (§10: "every change bumps the version").
#
# Consumers pin by version, and three of them resolve this file through
# /blob/main/, so an unbumped edit changes what they read with nothing to compare.
# The version is the only handle they have; if it can stand still through a
# content change, it carries no information.
#
# Compares against origin/main, the branch consumers actually resolve.
#
# LOCAL vs CI ARE DELIBERATELY ASYMMETRIC (C-70 / epic #66, story #68).
#
# Locally this SKIPs with a visible note: a developer working in a checkout with
# no `origin/main` should not be blocked by a check about what consumers resolve.
#
# In CI it is an ERROR. `actions/checkout` does not reliably create `origin/main`,
# so the skip path is the DEFAULT there, not the exception -- and it prints a note
# and exits 0. A gating job would go green with this check silently not running:
# the fix for C-53 present, and inert. That is the exact shape this repo keeps
# finding (C-52, C-55, C-68) and the reason the epic exists, so the workflow
# fetches the ref AND this refuses to pass without it. Belt and braces, because
# the workflow step is the thing most likely to be "simplified" later.
_vd_ci="${CI:-}"
if ! command -v git >/dev/null 2>&1 || ! git rev-parse --git-dir >/dev/null 2>&1; then
if [ -n "$_vd_ci" ]; then
echo " ERROR: CI is set but this is not a git checkout, so the version"
echo " check cannot run. A gate that cannot run must not pass."
errors=$((errors + 1))
else
echo " SKIP: not a git checkout — cannot diff against origin/main"
fi
elif ! git rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
if [ -n "$_vd_ci" ]; then
echo " ERROR: CI is set but origin/main is unreachable, so the version"
echo " check cannot run — and this is the DEFAULT state after"
echo " actions/checkout, not an edge case. Fetch it in the workflow:"
echo " git fetch --no-tags --depth=1 origin main:refs/remotes/origin/main"
echo " Passing here would be a green tick with C-53's guard inert."
errors=$((errors + 1))
else
echo " SKIP: no origin/main in this checkout — nothing to compare against"
fi
else
# Compares the version VALUE across the two revisions, not whether the line
# that carries it was touched. Two earlier drafts of this check were wrong in
# opposite directions, and both are worth not repeating:
#
# "any changed line mentioning a semver" -> passes when someone adds prose
# like "byte-identical to v1.3.0" and never touches the declaration.
# "the declaration line appears in the diff" -> passes when the declaration
# line changes for an unrelated reason. Editing the trailing comment on
# `version = "1.3.0"` satisfied it while the version stood still.
#
# Only the value answers the question consumers actually ask: is what I pinned
# still what I would get?
_version_of() { # $1 = git revision or "" for the working tree, $2 = file
if [ -z "$1" ]; then cat "$2" 2>/dev/null; else git show "$1:docs/$2" 2>/dev/null; fi \
| if [ "$2" = "$REGISTRY" ]; then grep -oP '^version = "\K[0-9.]+'
else grep -P '^\| Version \|' | grep -oP '[0-9]+\.[0-9]+\.[0-9]+'; fi \
| head -1
}
# PATTERN is checked here but deliberately NOT in check 7's lockstep: it is
# versioned independently of the seam contract, because they govern different
# things and change for different reasons (its §7). It still may not change
# content without moving its version — that rule is universal.
for f in "$CONTRACT" "$REGISTRY" "$PATTERN"; do
[ -f "$f" ] || continue
git diff --quiet origin/main -- ":/docs/$f" 2>/dev/null && continue
was=$(_version_of origin/main "$f")
now=$(_version_of "" "$f")
if [ -z "$now" ]; then
echo " ERROR: cannot parse a version from $(basename "$f")"
errors=$((errors + 1))
elif [ -z "$was" ] && git cat-file -e "origin/main:docs/$f" 2>/dev/null; then
# The file exists on origin/main but no version parsed out of it. An
# empty `was` would otherwise differ from `now` and report OK -- a
# false pass on exactly the comparison this check exists to make.
echo " ERROR: $(basename "$f") exists on origin/main but no version could be parsed there."
echo " Cannot prove the version moved, so this is not a pass."
errors=$((errors + 1))
elif [ "$was" = "$now" ]; then
echo " ERROR: $(basename "$f") differs from origin/main but its version is still v${now}."
echo " §10: 'every change bumps the version. The registry and this"
echo " contract version together.' Consumers pin by version; an edit"
echo " they cannot detect is the failure mode this rule exists to stop."
errors=$((errors + 1))
else
echo " OK: $(basename "$f") changed and its version moved v${was} -> v${now}"
fi
done
fi
# 9. The README's pinning advice must be followable.
#
# C-76. For seven editions the README said "Pin against this tag -- appwrite-seam-v1.4.4"
# while the current edition was v1.7.1, and all eight checks above passed the whole time.
# They verify cross-references and version coherence; nothing verified whether prose ABOUT
# this repo was true. The front page of a public contract repo told five consumers to pin
# an edition predating the [contract.*] table two of them now bind to.
#
# The fix was to stop naming a tag and point at the FLOOR instead -- §10.1's
# `obliges_consumers_since`, which moves only when an edition actually obliges a consumer.
# This check defends that, three ways:
#
# (a) the README still references the floor mechanism. If someone rewrites the pin
# advice back to "pin the newest tag", this fails -- that is the regression.
# (b) every appwrite-seam-v* tag the README names RESOLVES. A historical mention is
# legitimate (the C-76 note names v1.4.4 on purpose); an invented one is not.
# (c) the floor itself is a fetchable tag. Consumers are told to compare against
# obliges_consumers_since; if no tag exists at that version, the advice is unusable.
#
# LOCAL vs CI ARE ASYMMETRIC, same as check 8 and for the same reason: a developer in a
# checkout with no tags should not be blocked, but in CI a check that cannot run must not
# pass. `actions/checkout` does not fetch tags by default, so the skip path would be the
# DEFAULT there -- which is exactly how C-55, C-53 and the reader guard each switched
# themselves off while reporting green.
echo "--- Checking that the README's pinning advice is followable ---"
_vd_readme="$SCRIPT_DIR/../README.md"
_vd_registry="$SCRIPT_DIR/ADRs/platform/coordinate_registry.toml"
if [ ! -f "$_vd_readme" ]; then
echo " ERROR: README.md not found at $_vd_readme"
errors=$((errors + 1))
else
# (a) the floor mechanism is still what the README points at
if grep -q "obliges_consumers_since" "$_vd_readme"; then
echo " OK: README points at obliges_consumers_since (the §10.1 floor)"
else
echo " ERROR: README.md no longer references \`obliges_consumers_since\`."
echo " C-76: it named a fixed tag for seven editions while every gate stayed"
echo " green. Consumers should compare against the FLOOR, not the newest tag —"
echo " most editions oblige nobody. Restore the §10.1 pinning guidance."
errors=$((errors + 1))
fi
if ! command -v git >/dev/null 2>&1 || ! git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1; then
if [ -n "${CI:-}" ]; then
echo " ERROR: CI is set but this is not a git checkout, so the tag checks"
echo " cannot run. A gate that cannot run must not pass."
errors=$((errors + 1))
else
echo " SKIP: not a git checkout — cannot resolve tags"
fi
elif [ -z "$(git -C "$SCRIPT_DIR" tag -l 'appwrite-seam-v*')" ]; then
if [ -n "${CI:-}" ]; then
echo " ERROR: CI is set but no appwrite-seam-v* tags are present, so the tag"
echo " checks cannot run. actions/checkout does NOT fetch tags by default;"
echo " fetch them in the workflow:"
echo " git fetch --no-tags --depth=1 origin +refs/tags/*:refs/tags/*"
echo " Passing here would leave C-76's guard inert."
errors=$((errors + 1))
else
echo " SKIP: no appwrite-seam-v* tags in this checkout (try: git fetch --tags)"
fi
else
# (b) every tag the README names must exist
_vd_bad=""
for _vd_t in $(grep -o 'appwrite-seam-v[0-9][0-9.]*' "$_vd_readme" | sort -u); do
git -C "$SCRIPT_DIR" rev-parse -q --verify "refs/tags/${_vd_t}" >/dev/null 2>&1 \
|| _vd_bad="$_vd_bad $_vd_t"
done
if [ -n "$_vd_bad" ]; then
echo " ERROR: README.md names tags that do not exist:$_vd_bad"
echo " Every tag named here is something a consumer may pin. §10 forbids"
echo " moving a published tag, so a name that resolves to nothing stays"
echo " broken until someone reads this line."
errors=$((errors + 1))
else
echo " OK: every appwrite-seam-v* tag named in README.md resolves"
fi
# (c) the floor must itself be a fetchable edition
_vd_floor="$(grep -m1 '^obliges_consumers_since' "$_vd_registry" 2>/dev/null \
| sed 's/.*"\(.*\)".*/\1/')"
if [ -z "$_vd_floor" ]; then
echo " ERROR: [meta] obliges_consumers_since is absent from the registry, but the"
echo " README tells consumers to compare against it."
errors=$((errors + 1))
elif git -C "$SCRIPT_DIR" rev-parse -q --verify "refs/tags/appwrite-seam-v${_vd_floor}" >/dev/null 2>&1; then
echo " OK: the obligation floor v${_vd_floor} resolves to a published tag"
else
echo " ERROR: obliges_consumers_since is v${_vd_floor} but no tag"
echo " appwrite-seam-v${_vd_floor} exists. Consumers are told to compare"
echo " their pin against this edition; without a tag they cannot fetch it."
errors=$((errors + 1))
fi
fi
fi
echo ""
if [ "$errors" -gt 0 ]; then
echo "=== FAILED: $errors issue(s) found ==="
exit 1
else
echo "=== PASSED: no issues found ==="
exit 0
fi