blockwatch 0.5.3

Language agnostic linter that keeps your code and documentation in sync and valid
Documentation
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
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# CLI Reference

For command-line flag documentation directly in your terminal, run `blockwatch --help`.

## Quick Options Reference

[//]: # (<block name="cli-docs">)

- **Read a Diff**: `git diff --patch | blockwatch --diff` marks which blocks the diff changed.
- **Only Changed Blocks**: `git diff --patch | blockwatch --diff --only-changed` narrows the run to them, instead of
  every block in the repository.
- **List Blocks**: `blockwatch list` outputs a JSON report of all discovered blocks.
- **Custom Extensions**: Map custom file extensions: `blockwatch -E cxx=cpp`
- **Disable Validators**: `blockwatch -d check-ai`
- **Enable Validators**: `blockwatch -e keep-sorted`
- **Ignore Files**: `blockwatch --ignore "**/generated/**"`
- **Report What Ran**: `blockwatch --verbosity summary` (or `full` for JSON on stdout)
- **Suppress Violations**: `blockwatch --suppress FILE[:BLOCK[:VALIDATOR[:HASH]]]` reports them but stops them failing
  the run
- **Violation Format**: `blockwatch --format sarif` writes a SARIF log instead of the JSON diagnostics

[//]: # (</block>)

## Selecting Files

By default, `blockwatch` scans every file in the repository, respecting `.gitignore`. Paths are reported relative to the
repository root, whichever directory you run from. Linked worktrees and submodules are supported too: a run inside one
checks that repository only.

VCS directories like `.git`, `.hg`, `.jj` and `.svn` are skipped.

```shell
# Check everything in the repository
blockwatch

# Restrict checks to specific glob patterns
blockwatch "src/**/*.rs" "**/*.md"

# Exclude specific paths
blockwatch "**/*.rs" --ignore "**/generated/**"
```

Note: Quote glob patterns to prevent shell expansion before passing arguments to `blockwatch`.

Globs **intersect** with whatever the run mode selected, in every mode. They only ever narrow a run: passing
`"src/**/*.rs"` alongside a diff checks the changed blocks under `src/`, and never adds an unchanged file back.

Globs choose which blocks are **validated**, not which files a rule may **resolve a reference against**. A rule such as
[`affects`](validators/affects.md) still finds its target in a file the globs left out, so narrowing a run to one
language does not turn every cross-language rule into a failure. An excluded file is read to answer the reference and
for nothing else: it is never validated, and never appears in a run report.

## Run Modes

Which files are parsed and which blocks are validated are two separate decisions, and each has its own flag.

| Invocation                         | Files parsed               | Blocks validated                                     |
|------------------------------------|----------------------------|------------------------------------------------------|
| `blockwatch`                       | Every file in scope        | Every block found; none counts as changed            |
| `blockwatch --diff`                | Every file in scope        | Every block found; the diff marks which ones changed |
| `blockwatch --diff --only-changed` | Only the files in the diff | Only the blocks the diff touched                     |

```shell
# Check every block in the repository
blockwatch

# Check every block, and enforce the rules that need a diff
git diff --patch | blockwatch --diff

# Check only the blocks the diff changed
git diff --patch --unified=0 | blockwatch --diff --only-changed

# The same, for staged changes
git diff --cached --patch --unified=0 | blockwatch --diff --only-changed

# Changed blocks under specific globs only
git diff --patch | blockwatch --diff --only-changed "src/**/*.rs" "**/*.md"
```

A block counts as changed when the diff overlaps its line range or its start tag. To inspect which blocks a diff
touches, use `blockwatch list --diff`.

`--only-changed` is what keeps pre-commit hooks and per-pull-request CI runs fast (see [CI Integration](ci.md)).
`--diff` on its own audits the whole repository while still enforcing the rules that fire only on changed content.

### Rules of the Modes

- **stdin is never read without `--diff`.** A diff piped to a bare `blockwatch` is ignored and the whole tree is
  scanned.
- **`--only-changed` requires `--diff`.** Without a diff there is nothing to narrow the run down to.
- **`--diff` with a terminal on stdin is an error.** No diff is coming, and quietly scanning the tree instead would hide
  that.
- **Under `--diff`, stdin that cannot be a diff is an error.** Empty input, input carrying ANSI color escapes (produce
  the diff with `--color=never`), and input with no unified-diff header are each reported by name rather than treated as
  "nothing changed".
- **A diff that resolves to nothing is an error.** Under `--only-changed` the diff is the scope, so any path in it that
  BlockWatch would parse but cannot find fails the run. Under `--diff` alone such a path is passed over — unless *none*
  resolves, which means the diff was taken against a different root (`diff.relative=true`, a wrong `-p` level) and no
  rule that needs a diff could fire.
- **[`affects`](validators/affects.md) needs `--diff`.** It compares blocks that a diff has touched, so it reports
  nothing at all without one. `--verbosity summary` says how many blocks carry a rule that needs a diff — see
  [Run Reports](#run-reports).

### Supported Diff Input

[//]: # (<block name="diff-input" affects="src/repo_path.rs:diff-target-resolution">)

Each diff header points at a file, and `blockwatch` resolves that path against the repository:

- **Path prefixes are required.** Git writes each diff path behind a one-component prefix — `a/`
  and `b/` by default, or `i/`, `w/`, `c/` and `o/` under `diff.mnemonicPrefix`. Exactly one is removed, so a repository
  directory that happens to share a prefix's name (a top-level `b/`, for example) is preserved.
- **Quoted paths are decoded.** Git's default `core.quotePath` writes a name containing non-ASCII characters, tabs,
  quotes or backslashes in an escaped form such as `"b/caf\303\251.py"`; the real name is recovered from it.

Diffs written *without* prefixes — `git diff --no-prefix`, `diff.noprefix=true` — or with a custom
`diff.srcPrefix` / `diff.dstPrefix` are rejected. They cannot be distinguished from prefixed paths whose repository
directory shares the prefix's name, and guessing would risk validating a file the diff never mentioned while the changed
one went unchecked. `diff.relative=true` is likewise unsupported: it writes paths relative to the current directory, and
nothing in the diff records that it did.

Two details make the detection reliable. Git draws the two prefixes from opposite sides of the comparison — `a/` against
`b/`, or `i/`, `c/` and `o/` against `w/` — so a header repeating one prefix on both sides is recognised as unprefixed
rather than stripped. An added file is the exception: its source is `/dev/null`, which says nothing either way, so both
readings are checked against the working tree and a target where both name a real file is reported as ambiguous.

A diff with a file that does not exist in the repository is an error too — but only for files BlockWatch would parse.
Entries whose extension maps to no language, such as binary assets or lockfiles, contribute no blocks and are passed
over, so a diff carrying them alongside source changes still validates normally.

[//]: # (</block>)

In each case `blockwatch` stops rather than check the wrong file, reporting the diff target at fault. The messages
describe the input rather than the command that produced it, since a diff need not come from Git:

```console
$ git diff --no-prefix | blockwatch --diff
Error: diff target "rules.py" has no recognized path prefix (a/, b/, i/, w/, c/, o/).
```

With Git, that means `--no-prefix`, `diff.noprefix`, or a custom `diff.srcPrefix` / `diff.dstPrefix`. To produce
configuration-independent output in a repository that sets any of these globally:

```shell
git diff --patch --default-prefix --no-relative | blockwatch --diff
```

`--default-prefix` requires Git 2.41 or newer. On older versions, use
`git -c diff.mnemonicPrefix=false -c diff.noprefix=false diff --patch`.

## Custom File Extension Mappings

Language detection relies on file extensions. Use `-E` to map unrecognized or custom extensions to a supported grammar:

```shell
blockwatch -E cxx=cpp -E c++=cpp
```

Files with extensions that do not map to any supported grammar are ignored.

## Enabling and Disabling Validators

Control which validators run using `-e` (enable only) or `-d` (disable):

```shell
# Run all validators except check-ai
blockwatch -d check-ai

# Run only keep-sorted and keep-unique
blockwatch -e keep-sorted -e keep-unique
```

Note: `-e` and `-d` cannot be combined in a single invocation.

## Suppressing a Violation

`--suppress` takes the address of a violation and stops it failing the run. The violation is still reported, at its
declared severity, marked `"suppressed": true`; only the exit code changes.

```shell
blockwatch --suppress docs/cli.md:cli-docs:keep-sorted
```

Use it when a rule is wrong at one particular site and you do not want to edit the source or turn the validator off
everywhere with `-d`. Repeat the flag to suppress several violations.

### The Address of a Violation

    FILE[:BLOCK_NAME[:VALIDATOR[:HASH]]]

Every violation of a named block carries its full address in the diagnostics, so the usual way to write a `--suppress`
flag is to copy one from the output.

- `FILE:BLOCK_NAME` is the same `file:name` grammar [`affects`]validators/affects.md and
  [`same-as`]validators/same-as.md use, and identifies exactly one block. As there, a file path containing a `:`
  cannot be addressed.
- `VALIDATOR` is the rule that reported the violation, spelled as in `-d` and `-e`.
- `HASH` is an opaque hex string telling one violation of a block from its siblings, emitted by the five validators that
  can report several: `keep-sorted`, `keep-unique`, `line-pattern`, `affects` and `same-as`.

**Every length is valid, and the shorter it is, the more it covers.** Only `FILE` is required:

| Address                          | Suppresses                                          |
|----------------------------------|-----------------------------------------------------|
| `FILE`                           | every violation in that file, named blocks or not   |
| `FILE:BLOCK_NAME`                | every violation of that block                       |
| `FILE:BLOCK_NAME:VALIDATOR`      | every violation that validator reports on the block |
| `FILE:BLOCK_NAME:VALIDATOR:HASH` | exactly one violation                               |

**A block with no `name` can only be suppressed file-wide.** Its violations carry no `address` in the diagnostics,
because there is nothing narrower to point at, but a `FILE` address covers the whole file and so covers them too.

An invalid address that covers nothing is ignored.

## SARIF Output

`--format sarif` writes the violations as a [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
log instead of the JSON diagnostics. It goes to the same place they do, **stderr**, so the run report keeps stdout to
itself:

```shell
blockwatch --format sarif 2> blockwatch.sarif
```

Nothing else about the run changes: the same violations are found, and the exit code is decided the same way.

```json
{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "blockwatch",
          "version": "0.5.2",
          "semanticVersion": "0.5.2",
          "informationUri": "https://github.com/mennanov/blockwatch",
          "rules": [
            {
              "id": "keep-sorted",
              "name": "keep-sorted",
              "shortDescription": { "text": "Requires the lines of a block to stay in order." },
              "helpUri": "https://github.com/mennanov/blockwatch/blob/v0.5.2/docs/validators/keep-sorted.md"
            }
          ]
        }
      },
      "columnKind": "unicodeCodePoints",
      "results": [
        {
          "ruleId": "keep-sorted",
          "ruleIndex": 0,
          "level": "error",
          "message": { "text": "Block fruits.py:fruits defined at line 2 has an out-of-order line 4 (asc)" },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": { "uri": "fruits.py" },
                "region": { "startLine": 4, "startColumn": 5, "endLine": 4, "endColumn": 12 }
              }
            }
          ],
          "partialFingerprints": { "blockwatchAddress/v1": "fruits.py:fruits:keep-sorted:bbd61689" },
          "properties": {
            "address": "fruits.py:fruits:keep-sorted:bbd61689",
            "data": { "order_by": "asc" }
          }
        }
      ]
    }
  ]
}
```

What to expect from the log:

- **A clean run still writes one**, with an empty `results` array. A service that reads SARIF treats a missing log as a
  run that never happened, rather than as a run that found nothing.
- **Only the rules that fired are described.** Each carries a `helpUri` to its documentation, pinned to the version that
  produced the log.
- **`level` follows [severity]validators/README.md#severity**: `error` and `warning` keep their names, and both `info`
  and `hint` become `note`, the weakest level SARIF consumers display.
- **A [suppressed]#suppressing-a-violation violation is reported like any other**, with
  `"suppressions": [{"kind": "external"}]` alongside it — SARIF's own way of saying a reviewer accepted it. The
  justification is left out: it lives wherever the suppression itself is recorded.
- **`partialFingerprints` carries the violation's [address]#the-address-of-a-violation**, so a service can match a
  violation against the same one in an earlier run. It is absent for a violation on an unnamed block, which has no
  address. The address is repeated in `properties`, where it is easier for a person to find and copy into a `--suppress`
  flag.
- **Paths are repository-relative**, the same paths the JSON diagnostics are keyed by, percent-encoded where a character
  would otherwise change how the path reads as a URI (a `#` or a space, say).
- **Lines and columns are 1-based, with an exclusive end column.** A column counts characters, which the run declares as
  `"columnKind": "unicodeCodePoints"` so that a consumer does not count UTF-16 code units instead.

`--format` cannot be combined with the `list` subcommand, which reports blocks rather than violations. To upload a log
to GitHub code scanning, see [CI Integration](ci.md#github-code-scanning).

## The `list` Command

The `list` command outputs details on all discovered blocks in JSON format without running validation. It mirrors the
[run modes](#run-modes) exactly, so `list` and the default command always agree on which blocks exist.

```shell
# List all blocks under the current directory
blockwatch list

# Restrict block listing to specific globs
blockwatch list "src/**/*.rs" "**/*.md"

# List all blocks, marking the ones the diff changed
git diff --patch | blockwatch list --diff

# List only the blocks the diff changed
git diff --patch | blockwatch list --diff --only-changed
```

Like the default command, `blockwatch list` reads stdin only when `--diff` is explicitly provided, preventing blocking
during non-interactive scripts or pipeline commands (e.g. `blockwatch list "src/**/*.ts" | jq`).

Each block entry carries an `is_content_modified` boolean field. Without a diff nothing marks a block as changed, so it
is `false` throughout; under `--diff` it identifies the blocks the diff touched.

Lines and columns are 1-based, and a column counts characters rather than bytes, so a multi-byte character such as `é`
or an emoji advances it by one. The `range` of a violation follows the same convention.

### Output Example

[//]: # (<block name="list-output-example">)

```json
{
  "src/lib.rs": [
    {
      "attributes": {
        "affects": "README.md:supported-langs",
        "name": "languages-code"
      },
      "column": 4,
      "is_content_modified": false,
      "line": 12,
      "name": "languages-code"
    },
    {
      "attributes": {
        "keep-sorted": "asc"
      },
      "column": 8,
      "is_content_modified": false,
      "line": 40,
      "name": "(unnamed)"
    }
  ]
}
```

[//]: # (</block>)

A block with no `name` attribute is reported as `(unnamed)`. Within a file, blocks appear in source order and each
block's attributes are sorted by name; the files themselves are not ordered, and two runs over an unchanged tree may
emit them differently. Sort by key downstream if you need to diff one run against another — or use [
`--verbosity full`](#full-reports), which does order its files.

## Run Reports

By default `blockwatch` prints nothing when a run succeeds. That makes a check that passed look exactly like a check
that never ran. Use `--verbosity` to see what was actually checked.

| Level            | Output                                                           |
|------------------|------------------------------------------------------------------|
| `none` (default) | Nothing.                                                         |
| `summary`        | One line of counts.                                              |
| `full`           | A JSON report of every block and the validators that checked it. |

The report goes to **stdout**. Violations go to **stderr**. A run can print both, and each one can be piped and parsed
on its own.

```shell
blockwatch --verbosity summary
blockwatch: mode=all, 34/240 files, 61 blocks (3 unchecked, 2 needs --diff), 73 checks, 0 violations
```

Reading that line:

- `mode=all` — which [run mode]#run-modes this was: `all`, `all+diff`, or `only-changed`. Each name is a single token,
  so the line stays tokenizable on whitespace.
- `34/240 files` — 240 files were read, and 34 of them contain blocks.
- `61 blocks (3 unchecked, 2 needs --diff)` — 61 blocks were in scope, no validator checked 3 of them, and 2 carry a
  rule that cannot fire at all without a diff. Fixing that means supplying one, so the figure is a prompt to change how
  you invoked `blockwatch`.
- `73 checks` — validators ran 73 times in total, once per block they applied to.
- `0 violations` — nothing failed.

**`needs --diff` appears only under `mode=all`.** Every other field is present in every mode. Once a diff is supplied
those rules *can* fire, so the question the figure answers no longer arises — and the obvious substitute, counting the
blocks the diff did not happen to reach, would just measure the size of your change. On a repository with fifty
`affects` blocks, a one-line commit would report forty-nine, every time, with nothing wrong. So the clause is left out
rather than reported as zero or as noise.

A parser should therefore read `mode=` first and expect the clause only for `all`; the remaining fields keep a fixed
shape in every mode.

A block goes unchecked for one of three reasons:

- **It is only a reference target.** A block that carries nothing but a `name` exists so that other blocks can point at
  it with `affects` or `same-as`. It declares no rule of its own, so nothing checks it. This is normal and needs no
  fixing.
- **The validator does not apply to this run.** `affects` only compares blocks that a diff has touched, so it checks
  nothing without `--diff`. These are the blocks the `needs --diff` figure counts. Under a diff the same block goes
  unchecked whenever the diff did not reach it, which is normal for an incremental run and is not counted.
- **The attributes do not add up to a rule.** A modifier such as `keep-sorted-pattern` only refines the validator it
  belongs to; on a block with no `keep-sorted`, it has nothing to modify and no validator claims the block. A `full`
  report lists every attribute as it was written, which is usually enough to see what is missing.

### Reports Under a Diff

Under `--diff --only-changed` the diff scopes the report exactly as it scopes the run: only the blocks the diff touched
are described. A block the diff never reached is *absent* from the report rather than listed with an empty `checks`
array, so `blocks_unchecked` counts only blocks that were in scope and that nothing checked. This is the same rule
`blockwatch list --diff --only-changed` follows, so the two commands always agree on which blocks exist. Under `--diff`
alone the report covers the whole tree, exactly as a run without a diff does.

Reference targets are reported by the run's scope, even though they are not resolved by it. When a block declares
`affects` or `same-as`, its target is read from disk and compared wherever it lives — but under `--only-changed` the
target appears in the report only if the diff touched it as well. A diff that changes the source alone therefore reports
a single file, even though two were involved:

```shell
git diff --patch | blockwatch --diff --only-changed --verbosity summary
blockwatch: mode=only-changed, 1/1 files, 1 blocks (0 unchecked), 1 checks, 1 violations
```

Nothing about the failure is hidden by this. Violations are printed to stderr as JSON, keyed by the file the violating
block lives in, and the message names both sides:

```json
{
  "fileA.py": [
    {
      "address": "fileA.py:a:affects:1d7a4c02",
      "code": "affects",
      "data": {
        "affected_block_file_path": "fileB.py",
        "affected_block_name": "b"
      },
      "message": "Block fileA.py:a at line 1 is modified, but fileB.py:b is not",
      "range": {
        "end": {
          "character": 39,
          "line": 1
        },
        "start": {
          "character": 3,
          "line": 1
        }
      },
      "severity": 1
    }
  ]
}
```

`severity` follows the [LSP numbering](validators/README.md#severity): `1` error, `2` warning, `3`
info, `4` hint. For a code-scanning service, ask for the same violations as SARIF instead — see
[SARIF Output](#sarif-output).

`address` is what a `--suppress` flag points at — see [Suppressing a Violation](#suppressing-a-violation). It is absent
when the block has no `name`, which leaves a file-wide address as the only way to suppress the violation. A suppressed
violation carries `"suppressed": true` alongside it; the key is absent otherwise. **A consumer that decides on
`severity` alone has to skip the suppressed violations**, which keep the severity their author declared.

The division of labour is deliberate — the report describes what the run examined, and the violation explains what went
wrong. Once the diff touches the target as well, it appears like any other block, with an empty `checks` array because a
block that carries nothing but a `name` declares no rule of its own:

```shell
git diff --patch | blockwatch --diff --only-changed --verbosity summary
blockwatch: mode=only-changed, 2/2 files, 2 blocks (1 unchecked), 1 checks, 0 violations
```

### Full Reports

`--verbosity full` describes every block the same way `blockwatch list` does, and adds a `checks` array naming the
validators that ran on it. A block checked by several validators lists all of them. The `summary` object carries the
same counts as the one-line report and follows the same rule: `blocks_needing_diff` is present only under `mode=all`,
and is absent — not zero — in the other two modes.

```json
{
  "summary": {
    "mode": "all",
    "files_scanned": 240,
    "files_with_blocks": 34,
    "files_skipped": 179,
    "blocks": 61,
    "blocks_unchecked": 3,
    "blocks_needing_diff": 2,
    "checks": 73,
    "violations": 0,
    "validators": {
      "affects": 41,
      "check-lua": 12,
      "keep-sorted": 20
    }
  },
  "files": {
    "src/validators/check_ai.rs": [
      {
        "attributes": {
          "affects": "docs/validators/check-ai.md:check-ai-env-vars",
          "name": "check-ai-env-vars",
          "same-as": "docs/validators/check-ai.md:check-ai-env-vars",
          "same-as-pattern": "BLOCKWATCH_AI_[A-Z_]+"
        },
        "checks": [
          "affects",
          "same-as"
        ],
        "column": 4,
        "is_content_modified": true,
        "line": 31,
        "name": "check-ai-env-vars"
      }
    ]
  }
}
```

Files are sorted by path, and each block's checks by validator name, so two runs over an unchanged tree print the same
bytes.

The report says which validators looked at a block, not what each one concluded. Violations are not repeated here; they
stay on stderr, under the same file paths and line numbers.

`--verbosity` cannot be combined with the `list` subcommand, because `list` already prints its own JSON to stdout.

## Exit Codes

| Code | Description                                                                                                                                                          |
|------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `0`  | Success. No violations found, or every reported violation is either non-`error` [severity]validators/README.md#severity or [suppressed]#suppressing-a-violation. |
| `1`  | Failure. At least one unsuppressed `error`-severity violation was detected.                                                                                          |

---

[← Return to README](../README.md)