mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 20:26:33 +00:00
feat(docs): report a code span that names a linkable directory
`roles/` set as inline code reads as a literal path, yet the directory holds an index page the build resolves. Writing it as a link costs nothing and gives the reader the page; leaving it as code hides it, which is how this branch degraded six working references in one pass. The lint strips markdown links from a line before scanning it, or it would report the code span inside a finished link, and it skips fenced blocks. It reads the index filenames from the same place the link lint does. Two false-positive classes came out of the first run and shaped the rule. A span of nothing but dots and slashes is a path word, not a path: '/' names a filesystem in a table of mount points and '../' names the traversal an argument must not perform. An absolute span is skipped because this repository writes Keycloak group paths that way, and /roles/web-app-bluesky is a group, not a folder. 35 spans across 12 files become links, and the documentation build resolves every one of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d8caa45420
commit
b291cb7620
14 files changed
+154
-30
No files matched your search
@@ -3,9 +3,9 @@
|
||||
## Priority and Scope 🎯
|
||||
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) is the single source of truth for contributor workflow, coding standards, testing, and review. You MUST read it.
|
||||
- You MUST read every file under `docs/contributing/` (full directory walk, including subdirectories) for the full contributor guidance.
|
||||
- You MUST read every file under `docs/agents/` (full directory walk, including subdirectories) for the agent execution flow, EXCEPT `docs/agents/action/`.
|
||||
- `docs/agents/action/` holds one runbook per action and is read on demand, not at session start: read a runbook when the matching `i8-` skill routes you to it, or when you are about to perform that action without one. It is the bulk of the agent documentation, and every runbook in it is inert until its action is the task.
|
||||
- You MUST read every file under [`docs/contributing/`](docs/contributing/) (full directory walk, including subdirectories) for the full contributor guidance.
|
||||
- You MUST read every file under [`docs/agents/`](docs/agents/) (full directory walk, including subdirectories) for the agent execution flow, EXCEPT [`docs/agents/action/`](docs/agents/action/).
|
||||
- [`docs/agents/action/`](docs/agents/action/) holds one runbook per action and is read on demand, not at session start: read a runbook when the matching `i8-` skill routes you to it, or when you are about to perform that action without one. It is the bulk of the agent documentation, and every runbook in it is inert until its action is the task.
|
||||
- This file extends CONTRIBUTING.md with agent-specific instructions; on conflict between CONTRIBUTING.md and this file, this file wins.
|
||||
- This file is runtime-agnostic and binding for every agent (Claude Code, Codex, Gemini CLI, other). There are no tool-specific extension files.
|
||||
|
||||
@@ -106,7 +106,7 @@ When a credential must be compared, compute and compare digests inside the conta
|
||||
|
||||
## Container-Owned Filesystem Entries 🐳
|
||||
|
||||
Files produced by the containerized runner (e.g. `__pycache__/*.pyc` under `tests/`, build artefacts) are often owned by `nobody` or another in-container UID and cannot be removed from the host. When a host-level `rm`/`chmod`/edit fails with `Permission denied` on such paths, agents MUST run the cleanup via `make compose-exec` (see [compose.yml](compose.yml); the repo is mounted at `/opt/src/infinito`) and MUST NOT ask the operator which path to take.
|
||||
Files produced by the containerized runner (e.g. `__pycache__/*.pyc` under [`tests/`](tests/), build artefacts) are often owned by `nobody` or another in-container UID and cannot be removed from the host. When a host-level `rm`/`chmod`/edit fails with `Permission denied` on such paths, agents MUST run the cleanup via `make compose-exec` (see [compose.yml](compose.yml); the repo is mounted at `/opt/src/infinito`) and MUST NOT ask the operator which path to take.
|
||||
|
||||
## Commit-Time Context Compaction 📦
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ Follow these steps to make your first contribution.
|
||||
|
||||
## Requirements 📐
|
||||
|
||||
Functional requirements live under `docs/requirements/` and follow a fixed format with a User Story and checkable Acceptance Criteria.
|
||||
Functional requirements live under [`docs/requirements/`](docs/requirements/) and follow a fixed format with a User Story and checkable Acceptance Criteria.
|
||||
See [requirements.md](docs/contributing/requirements.md) for the authoring guide.
|
||||
|
||||
## Repository Files 📋
|
||||
|
||||
@@ -11,4 +11,4 @@ For the canonical Make target index that invokes these helpers, see [make.md](..
|
||||
|---|---|---|
|
||||
| `apps.sh` | App-keyed orchestrator that maps `$apps` to compose entities, drives `entity/{db,compose,dir}.sh`, and wipes the matching token-store entries via [tokens.py](../../../utils/cleanup/tokens.py). | The host-side wrapper [entity.sh](../../tests/deploy/local/purge/entity.sh) `docker exec`s this script. |
|
||||
| `web.sh` | Removes nginx and self-signed CA state inside the container. | Called by the local purge wrapper. |
|
||||
| `entity/` | Entity-keyed primitives. | See [entity/README.md](entity/README.md) for the exact scripts. |
|
||||
| [`entity/`](entity/) | Entity-keyed primitives. | See [entity/README.md](entity/README.md) for the exact scripts. |
|
||||
@@ -1,6 +1,6 @@
|
||||
# Local Deploy Test Scripts 🧪
|
||||
|
||||
This file describes the local helpers outside `deploy/`.
|
||||
This file describes the local helpers outside [`deploy/`](deploy/).
|
||||
For executable local deploy flows, use [deploy/README.md](deploy/README.md).
|
||||
For inventory reset helpers, use [reset/README.md](reset/README.md).
|
||||
For cleanup helpers, use [purge/README.md](purge/README.md).
|
||||
|
||||
@@ -14,16 +14,16 @@ For the canonical Make target index that invokes these helpers, see [make.md](..
|
||||
|
||||
## Naming
|
||||
|
||||
The `apps/` subtree is split by verb (the user intent) and scope:
|
||||
The [`apps/`](apps/) subtree is split by verb (the user intent) and scope:
|
||||
|
||||
- `initialize/` creates or refreshes the inventory; existing entity state is kept.
|
||||
- `reinstall/` recreates the inventory and purges existing entities first.
|
||||
- `update/` reuses the already-initialized inventory.
|
||||
- `all.sh` covers every discovered application; `selection.sh` covers one or more apps passed via `apps` (or a positional argument).
|
||||
|
||||
The `bundles/` subtree mirrors the same axes for inventory bundles (`fresh.sh` ≈ initialize, `update.sh` ≈ update).
|
||||
The [`bundles/`](bundles/) subtree mirrors the same axes for inventory bundles (`fresh.sh` ≈ initialize, `update.sh` ≈ update).
|
||||
|
||||
The `container/` subtree holds the in-container halves invoked by the host wrappers and follows the same verb/scope layout; see [container/](container/README.md).
|
||||
The [`container/`](container/) subtree holds the in-container halves invoked by the host wrappers and follows the same verb/scope layout; see [container/](container/README.md).
|
||||
|
||||
## Entry Points
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ The subtree splits by user intent (the verb) and scope:
|
||||
|
||||
| Verb folder | Inventory | Stack cycle | Entity purge |
|
||||
|---|---|---|---|
|
||||
| `initialize/` | fresh | no | no |
|
||||
| `reinstall/` | fresh | yes (down + up) | yes (shared entities) |
|
||||
| `update/` | reuse | no | no |
|
||||
| [`initialize/`](initialize/) | fresh | no | no |
|
||||
| [`reinstall/`](reinstall/) | fresh | yes (down + up) | yes (shared entities) |
|
||||
| [`update/`](update/) | reuse | no | no |
|
||||
|
||||
Each verb folder MUST contain `all.sh` for every discovered application and MAY contain `selection.sh` for one or more apps passed via `apps` (or as a positional argument where supported).
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ through the provisioner's host_vars merge.
|
||||
|
||||
## Scripts
|
||||
|
||||
The sequenced flow lives in `routine/`, the naming SPOT in `utils/topology/`,
|
||||
The sequenced flow lives in [`routine/`](routine/), the naming SPOT in `utils/topology/`,
|
||||
shared helpers in `utils/`, and
|
||||
the cluster declaration (image, containers, network, DNS play) in
|
||||
`compose/swarm/` + `compose/swarm/compose.yml`.
|
||||
@@ -154,7 +154,7 @@ the cluster declaration (image, containers, network, DNS play) in
|
||||
| deploy | `routine/02_provision_inventory.sh` | provision the per-round inventory |
|
||||
| deploy | `routine/03_wait_converge.sh` | wait for every stack service to converge |
|
||||
| deploy | `routine/04_verify_reachable.sh` | probe the app is reachable in-cluster |
|
||||
| deploy | `routine/backup/base.sh` (+ per-host routines in `routine/backup/`) | backup+restore DR drill between rounds |
|
||||
| deploy | `routine/backup/base.sh` (+ per-host routines in [`routine/backup/`](routine/backup/)) | backup+restore DR drill between rounds |
|
||||
| deploy | `utils/clean/purge_stacks.sh` | remove prior-round stacks between rounds |
|
||||
| chaos | `routine/05_seed_content.sh` | seed a marker on the NFS volume |
|
||||
| chaos | `routine/06_drain_worker.sh` | drain the app's worker + force reschedule |
|
||||
|
||||
+6
-6
@@ -4,11 +4,11 @@ All automated tests for Infinito.Nexus live here.
|
||||
|
||||
| Directory | Type | Guide |
|
||||
|---|---|---|
|
||||
| `unit/` | Unit tests (isolated, no I/O) | [unit.md](../docs/contributing/actions/testing/unit.md) |
|
||||
| `integration/` | Integration tests (real component boundaries) | [integration.md](../docs/contributing/actions/testing/integration.md) |
|
||||
| `external/` | Opt-in live third-party checks, excluded from `make test` | [external.md](../docs/contributing/actions/testing/external.md) |
|
||||
| `e2e/` | End-to-end Playwright tests | [playwright.md](../docs/contributing/actions/testing/playwright.md) |
|
||||
| `regression/` | Regression guard rules | [regression.md](../docs/contributing/actions/testing/regression.md) |
|
||||
| `lint/` | Structural lint checks | [lint.md](../docs/contributing/actions/testing/lint.md) |
|
||||
| [`unit/`](unit/) | Unit tests (isolated, no I/O) | [unit.md](../docs/contributing/actions/testing/unit.md) |
|
||||
| [`integration/`](integration/) | Integration tests (real component boundaries) | [integration.md](../docs/contributing/actions/testing/integration.md) |
|
||||
| [`external/`](external/) | Opt-in live third-party checks, excluded from `make test` | [external.md](../docs/contributing/actions/testing/external.md) |
|
||||
| [`e2e/`](e2e/) | End-to-end Playwright tests | [playwright.md](../docs/contributing/actions/testing/playwright.md) |
|
||||
| [`regression/`](regression/) | Regression guard rules | [regression.md](../docs/contributing/actions/testing/regression.md) |
|
||||
| [`lint/`](lint/) | Structural lint checks | [lint.md](../docs/contributing/actions/testing/lint.md) |
|
||||
|
||||
Run each suite via the `make` targets documented in the linked guides.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Ansible Integration Tests 🧩
|
||||
|
||||
Integration tests for Ansible-native artefacts: action plugins, filter plugins, lookup plugins, handlers, group_vars, inventory, Jinja templates, playbooks, and role-local `vars/` / `defaults/`.
|
||||
Integration tests for Ansible-native artefacts: action plugins, filter plugins, lookup plugins, handlers, group_vars, inventory, Jinja templates, playbooks, and role-local [`vars/`](vars/) / `defaults/`.
|
||||
|
||||
Each child directory pins one Ansible surface; see the per-directory README for scope. Topical concerns that are not Ansible-specific (compose/docker/networks, OAuth2/OIDC, service registry, role-meta layout) live under [`infrastructure/`](../infrastructure/), [`iam/`](../iam/), [`meta/`](../meta/), or [`roles/`](../roles/).
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Lint tests that enforce correct Jinja2 filter and lookup composition in Ansible templates and task definitions.
|
||||
|
||||
Tests in this directory MUST only cover Jinja2 expression composition rules.
|
||||
Rules that validate where dotted-path targets are declared live in the sibling `../variables/` directory.
|
||||
Rules that validate where dotted-path targets are declared live in the sibling [`../variables/`](../variables/) directory.
|
||||
|
||||
- `test_no_lookup_config_jinja_default.py` forbids `lookup('config', application_id, 'path') | default(X)`. The two-term `config` lookup raises on a missing key, so the Jinja `default(...)` filter never fires; the fallback MUST be passed as the lookup's third argument instead.
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ Tests in this directory MUST only cover Ansible module / task usage rules.
|
||||
- `test_no_redundant_default_on_module_io.py` forbids `| default(...)` on a task's own `register:` output or input arguments inside its own conditionals, where the value is provably set.
|
||||
- `test_run_once_tags.py` requires that every `set_fact` marked `run_once: true` carries the canonical run-once tags so re-runs and `--tags` filters keep the fact populated.
|
||||
|
||||
Shell-pipeline rules and SQL formatting rules live in sibling directories; see `../shell/` and `../sql/`.
|
||||
Shell-pipeline rules and SQL formatting rules live in sibling directories; see [`../shell/`](../shell/) and [`../sql/`](../sql/).
|
||||
|
||||
For framework, directory layout, and `make test-lint` usage see [lint.md](../../../../docs/contributing/actions/testing/lint.md).
|
||||
@@ -0,0 +1,124 @@
|
||||
"""A code span naming a linkable directory must be a link.
|
||||
|
||||
``roles/`` set as inline code reads as a literal path, yet the directory holds
|
||||
an index page that the documentation build resolves. Writing it as a link costs
|
||||
nothing and gives the reader the page; leaving it as code hides it, which is how
|
||||
six working references were degraded in one pass through this repository.
|
||||
|
||||
Only a directory backed by an index page is reported. A path that does not
|
||||
exist, a directory without one, and a file are all left alone: there is nothing
|
||||
for the link to open.
|
||||
|
||||
Per-line opt-out: ``# nocheck: markdown-code-span-linkable``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
from utils.annotations.suppress import is_suppressed_at
|
||||
from utils.cache.files import read_text
|
||||
|
||||
from . import PROJECT_ROOT, index_page
|
||||
|
||||
_RULE = "markdown-code-span-linkable"
|
||||
|
||||
_FENCE = re.compile(r"^\s*(`{3,}|~{3,})")
|
||||
_LINK = re.compile(r"!?\[[^\]]*\]\([^)]*\)")
|
||||
_CODE_SPAN = re.compile(r"(?<!`)(`+)(?!`)(.+?)(?<!`)\1(?!`)")
|
||||
_PLACEHOLDER = frozenset("<>{}*$|")
|
||||
|
||||
|
||||
class Linkable(NamedTuple):
|
||||
file: Path
|
||||
line: int
|
||||
span: str
|
||||
target: Path
|
||||
|
||||
|
||||
def _tracked_markdown(root: Path) -> list[Path]:
|
||||
out = subprocess.check_output(["git", "-C", str(root), "ls-files", "-z", "*.md"])
|
||||
return [root / rel for rel in out.decode("utf-8").split("\0") if rel]
|
||||
|
||||
|
||||
def _candidate(span: str) -> bool:
|
||||
"""Whether a code span can name a directory relative to its own file.
|
||||
|
||||
A span of nothing but dots and slashes is a path word rather than a path:
|
||||
``/`` names the filesystem in a table of mount points and ``../`` names the
|
||||
traversal an argument must not perform. Both resolve only because the
|
||||
repository root carries an index page.
|
||||
|
||||
An absolute span is skipped because this repository writes Keycloak group
|
||||
paths that way, and ``/roles/web-app-bluesky`` is a group, not a folder.
|
||||
"""
|
||||
text = span.strip()
|
||||
if "/" not in text or _PLACEHOLDER & set(text) or " " in text:
|
||||
return False
|
||||
if text.startswith("/"):
|
||||
return False
|
||||
return text.strip("./") != ""
|
||||
|
||||
|
||||
def _resolve(span: str, file: Path, root: Path) -> Path:
|
||||
return (file.parent / span.strip().rstrip("/")).resolve()
|
||||
|
||||
|
||||
def _findings(file: Path, root: Path) -> list[Linkable]:
|
||||
try:
|
||||
lines = read_text(str(file)).splitlines()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return []
|
||||
|
||||
found: list[Linkable] = []
|
||||
fence: str | None = None
|
||||
for number, line in enumerate(lines, start=1):
|
||||
opener = _FENCE.match(line)
|
||||
if opener:
|
||||
marker = opener.group(1)[0]
|
||||
fence = None if fence == marker else (fence or marker)
|
||||
continue
|
||||
if fence:
|
||||
continue
|
||||
for match in _CODE_SPAN.finditer(_LINK.sub("", line)):
|
||||
span = match.group(2)
|
||||
if not _candidate(span):
|
||||
continue
|
||||
target = _resolve(span, file, root)
|
||||
if not target.is_dir() or index_page(target) is None:
|
||||
continue
|
||||
if is_suppressed_at(lines, number, _RULE, mode="same-or-above"):
|
||||
continue
|
||||
found.append(Linkable(file, number, span, target))
|
||||
return found
|
||||
|
||||
|
||||
class TestMarkdownCodeSpanLinks(unittest.TestCase):
|
||||
def test_a_linkable_directory_is_written_as_a_link(self) -> None:
|
||||
findings: list[Linkable] = []
|
||||
for file in sorted(_tracked_markdown(PROJECT_ROOT)):
|
||||
findings.extend(_findings(file, PROJECT_ROOT))
|
||||
|
||||
if not findings:
|
||||
return
|
||||
|
||||
listed = "\n".join(
|
||||
f" {item.file.relative_to(PROJECT_ROOT).as_posix()}:{item.line}: "
|
||||
f"`{item.span}` -> {index_page(item.target).name}"
|
||||
for item in findings
|
||||
)
|
||||
self.fail(
|
||||
"These code spans name a directory the documentation build can "
|
||||
"open, because it holds an index page. Write them as a link so the "
|
||||
"reader reaches it:\n\n"
|
||||
" [`path/`](path/)\n\n"
|
||||
f"Offending spans:\n{listed}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Vendored
+3
-3
@@ -17,7 +17,7 @@ Every module here belongs to one of four roles: parse, resolve, orchestrate, wri
|
||||
| `runtime.py` | Resolve host-context lookups (disk, RAM, hostname, GHA/Act flags, `/proc/version`, helper-script invocation). |
|
||||
| `builder.py` | Thin orchestrator that defines `EnvBuilder`, `BuildContext`, and `build_env()`. Walks the handler registry. |
|
||||
| `writer.py` | Serialise an `EnvBuilder` into a docker-compose-compatible `.env` on disk. |
|
||||
| `handlers/` | One module per dynamically-computed variable. See [handlers README](handlers/README.md). |
|
||||
| [`handlers/`](handlers/) | One module per dynamically-computed variable. See [handlers README](handlers/README.md). |
|
||||
|
||||
The CLI entry point lives at [__main__.py](../../cli/meta/env/__main__.py) under `cli/meta/env/`.
|
||||
Shell consumers source [load.sh](../../scripts/meta/env/load.sh) under `scripts/meta/env/`.
|
||||
@@ -26,7 +26,7 @@ Shell consumers source [load.sh](../../scripts/meta/env/load.sh) under `scripts/
|
||||
|
||||
- Module names MUST stay lowercase snake_case.
|
||||
- Pure passthrough or simple parser/writer helpers MUST live directly under `utils/env/`.
|
||||
- Per-variable computation MUST live under `handlers/` (see [handlers README](handlers/README.md) for the naming convention used there).
|
||||
- Per-variable computation MUST live under [`handlers/`](handlers/) (see [handlers README](handlers/README.md) for the naming convention used there).
|
||||
- Test files live in `tests/unit/python/utils/env/` and mirror the module name with a `test_` prefix.
|
||||
|
||||
## Machine Facts vs Run Facts 🧭
|
||||
@@ -44,6 +44,6 @@ A key that some environment declares per run therefore MUST be written by a dedi
|
||||
|
||||
## Import Rules 🔗
|
||||
|
||||
- `parser.py`, `runtime.py`, and `writer.py` MUST NOT import from `builder.py` or `handlers/`.
|
||||
- `parser.py`, `runtime.py`, and `writer.py` MUST NOT import from `builder.py` or [`handlers/`](handlers/).
|
||||
- `builder.py` MUST NOT import individual handler modules; it MUST go through the `ORDERED_HANDLERS` list exposed by `handlers/__init__.py`.
|
||||
- Handler modules MUST NOT import each other (see [handlers README](handlers/README.md)).
|
||||
@@ -17,7 +17,7 @@ Modules at this level are channel-agnostic primitives; per-tool policy lives one
|
||||
| `npm.py` | Global / local npm install with per-user `--prefix` fallback. |
|
||||
| `github_release.py` | Latest-tag resolver and asset downloader for GitHub releases. |
|
||||
| `system_pkg.py` | Per-manager dispatch for `pacman` / `apt-get` / `dnf` / `yum` / `brew`. |
|
||||
| `lint/` | One module per tool, each exposing `ensure()`. See [lint README](lint/README.md). |
|
||||
| [`lint/`](lint/) | One module per tool, each exposing `ensure()`. See [lint README](lint/README.md). |
|
||||
|
||||
The CLI entry point lives at [__main__.py](lint/__main__.py), invoked from [lint.sh](../../scripts/install/lint.sh) as `python3 -m utils.install.lint`.
|
||||
The matching dispatcher [wrapper.sh](../../scripts/install/wrapper.sh) routes `make install-lint` host vs container based on `INFINITO_LINT_RUNNER`.
|
||||
@@ -26,7 +26,7 @@ The matching dispatcher [wrapper.sh](../../scripts/install/wrapper.sh) routes `m
|
||||
|
||||
- Module names MUST stay lowercase snake_case.
|
||||
- Channel-agnostic helpers MUST live directly under `utils/install/`.
|
||||
- Per-tool policy (one tool per file) MUST live under `lint/` (see [lint README](lint/README.md)).
|
||||
- Per-tool policy (one tool per file) MUST live under [`lint/`](lint/) (see [lint README](lint/README.md)).
|
||||
- Test files live in `tests/unit/python/utils/install/` and mirror the module name with a `test_` prefix.
|
||||
|
||||
## Import Rules
|
||||
@@ -34,4 +34,4 @@ The matching dispatcher [wrapper.sh](../../scripts/install/wrapper.sh) routes `m
|
||||
- `primitives.py` MUST NOT import from any other module in this package.
|
||||
- `pip.py`, `npm.py`, `github_release.py`, and `system_pkg.py` MAY import from `primitives.py`.
|
||||
- `npm.py` MAY import from `system_pkg.py` to bootstrap npm itself; no other cross-imports between these four are allowed.
|
||||
- Per-tool modules under `lint/` MUST NOT import each other (see [lint README](lint/README.md)).
|
||||
- Per-tool modules under [`lint/`](lint/) MUST NOT import each other (see [lint README](lint/README.md)).
|
||||
Reference in new issue
Block a user