diff --git a/.claude/skills/write-doc.md b/.claude/skills/write-doc.md index b17d27bf..0078a004 100644 --- a/.claude/skills/write-doc.md +++ b/.claude/skills/write-doc.md @@ -9,14 +9,14 @@ Generate or regenerate an operator-facing document from the documentation spec. ``` Where `` is one of: -- `docs-readme` — docs/README.md (documentation index) -- `intro` — docs/INTRODUCTION.md -- `installation` — docs/guides/INSTALLATION.md -- `quickstart` — docs/guides/QUICKSTART.md -- `user-roles` — docs/guides/USER_ROLES.md -- `api-guide` — docs/guides/API_GUIDE.md -- `scanning` — docs/guides/SCANNING_AND_COMPLIANCE.md -- `hosts-remediation` — docs/guides/HOSTS_AND_REMEDIATION.md +- `docs-readme`: docs/README.md (documentation index) +- `intro`: docs/INTRODUCTION.md +- `installation`: docs/guides/INSTALLATION.md +- `quickstart`: docs/guides/QUICKSTART.md +- `user-roles`: docs/guides/USER_ROLES.md +- `api-guide`: docs/guides/API_GUIDE.md +- `scanning`: docs/guides/SCANNING_AND_COMPLIANCE.md +- `hosts-remediation`: docs/guides/HOSTS_AND_REMEDIATION.md ## Instructions @@ -26,12 +26,12 @@ When this skill is invoked: Read the authoritative engineering docs and behavioral specs to ground the content: -- `docs/engineering/BACKEND_FUNCTIONALITY.md` — backend behavior and service topology -- `docs/engineering/rbac_registry.md` and `specs/system/rbac.spec.yaml` — RBAC roles and permissions -- `specs/system/http-server.spec.yaml` — the single Go binary that serves the REST API and the embedded React UI over HTTPS on port `8443` -- `specs/system/kensa-executor.spec.yaml` — the Kensa compliance engine (Go, SSH-based, native YAML rules) -- `specs/system/job-queue.spec.yaml` — the PostgreSQL-native background job queue (`SKIP LOCKED`) -- `api/openapi.yaml` — the API contract source of truth (routes, request/response schemas) +- `docs/engineering/ARCHITECTURE.md`: per-package backend map and service topology +- `docs/engineering/rbac_registry.md` and `specs/system/rbac.spec.yaml`: RBAC roles and permissions +- `specs/system/http-server.spec.yaml`: the single Go binary that serves the REST API and the embedded React UI over HTTPS on port `8443` +- `specs/system/kensa-executor.spec.yaml`: the Kensa compliance engine (Go, SSH-based, native YAML rules) +- `specs/system/job-queue.spec.yaml`: the PostgreSQL-native background job queue (`SKIP LOCKED`) +- `api/openapi.yaml`: the API contract source of truth (routes, request/response schemas) OpenWatch is a single Go module at the repo root. There is no `app/` or `backend/` directory, no Docker Compose, and no Redis, Celery, or MongoDB. Data lives in PostgreSQL only. @@ -58,7 +58,8 @@ Follow these rules from the spec constraints: - **Tone**: Procedural and direct. Write for operators who deploy and maintain, not developers who extend. - **Accuracy**: All file paths, API endpoints, environment variables, and role names MUST be verified against the codebase before inclusion. Never state a feature exists without verifying the route/service/model exists in code. - **Scope**: Daily/weekly/monthly tasks only. No edge cases, no developer internals. -- **Format**: GitHub-flavored Markdown. No emojis. Tables for structured data. Code blocks for commands. +- **Format**: GitHub-flavored Markdown. Tables for structured data. Code blocks for commands. +- **Style guide**: Generated prose MUST satisfy the Hanalyx developer documentation style guide (Context Plane `dev/DEVELOPER_DOCUMENTATION_STYLE_GUIDE`). The three CI-enforced gates are no em dashes, no emojis, and no AI speak (hype adjectives, filler openers, and padding verbs such as `leverage`, `utilize`, `facilitate`, `harness`). Also: sentence-case headings, active voice, present tense, second person, ISO 8601 dates, versions written `v0.6.0`, and no `simply`, `just`, `easy`, or `please`. Run `make docs-style` on the generated file before finishing; CI's "Doc Style" job fails the build otherwise. - **Cross-references**: Link between docs using relative paths. Every doc should link to at least 2 others. - **Role names**: `admin`, `security_admin`, `ops_lead`, `auditor`, `viewer` (built-in roles from `internal/users/roles.go` / `internal/auth/roles.gen.go`; admins may also create custom roles) - **Framework data**: Use framework IDs and rule counts verified against `internal/kensa/` and `specs/system/kensa-executor.spec.yaml` diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 3e1b85ee..b129e7dd 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -355,3 +355,21 @@ jobs: /tmp/go-test.json /tmp/vitest-junit.xml retention-days: 7 + + # Hanalyx documentation style gate. Runs as its own job on purpose: the + # "gates" job above short-circuits to success on doc-only changes, so a + # step inside it would be skipped exactly when documentation is what + # changed. Enforces the prohibited list (em dashes, emojis, AI speak) + # from the Context Plane at dev/DEVELOPER_DOCUMENTATION_STYLE_GUIDE. + # Shared implementation: dev/tools/doc-style-check. + doc-style: + name: Doc Style + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Check changed Markdown against the style guide + run: | + git fetch origin main --depth=1 2>/dev/null || true + python3 scripts/check-doc-style.py --changed diff --git a/AGENTS.md b/AGENTS.md index 182dfae8..be728f98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,8 @@ -# AGENTS.md — contributor & AI-assistant guide +# AGENTS.md: contributor and AI-assistant guide This is the **tracked, reviewed** orientation for anyone (human or AI) working in this repo. It is intentionally short and points at authoritative, tracked -sources rather than restating them — restated facts rot. For live counts +sources rather than restating them. Restated facts rot. For live counts (version, package count, spec count) run **`scripts/repo-facts.sh`** instead of trusting a number written here. @@ -17,7 +17,7 @@ trusting a number written here. A single Go binary (`github.com/Hanalyx/openwatch`, Go 1.26) that serves a REST API plus an embedded React 19 SPA over HTTPS, backed by **PostgreSQL only** (no MongoDB, Redis, or Celery). Compliance scanning is the **Kensa** engine -(SSH-based, native YAML rules), integrated as a Go dependency — OpenSCAP/`oscap` +(SSH-based, native YAML rules), integrated as a Go dependency. OpenSCAP/`oscap` is not used. See [README.md](README.md) and [docs/guides/](docs/guides/) for the product-level picture. @@ -64,7 +64,7 @@ full workflow and the pre-commit / pre-push hooks. `internal/server/api/server.gen.go` and `frontend/src/api/schema.d.ts`. CI fails if they drift (the "Verify generated … in sync" steps). The generator (oapi-codegen) is **pinned** in the Makefile and the embedded-spec blob is -**off** (see `api/oapi-codegen.yaml`) so regeneration is byte-deterministic — +**off** (see `api/oapi-codegen.yaml`) so regeneration is byte-deterministic, so don't turn embedded-spec back on without reading the comment there. ## Spec-driven development (Specter) @@ -74,18 +74,29 @@ Every acceptance criterion (`AC-NN`) must be covered by a test carrying a litera `// @spec ` + `// @ac AC-NN` annotation, and Go subtests also use a `t.Run("/AC-NN", …)` token. CI enforces annotation hygiene (`specter check --test`), 100% **structural** coverage -(`specter coverage --strictness annotation` — the literal `// @ac` is required, +(`specter coverage --strictness annotation`; the literal `// @ac` is required, the `t.Run` token alone is not enough), and 100% **outcome** coverage -(`specter sync` — the annotated test must pass). Run `make spec-check` locally +(`specter sync`; the annotated test must pass). Run `make spec-check` locally first. If the spec and code disagree, the (human-approved) spec wins. ## Standards that trip people up -- **`gofmt -s` before committing** — CI lint is strict; `go vet`/`build` pass on +- **`gofmt -s` before committing.** CI lint is strict; `go vet`/`build` pass on unformatted code, so run `gofmt -l` on touched files yourself. -- **No emojis anywhere in code/config/specs** (YAML/encoding hazards). -- **No em-dashes in user-facing UI copy** — restructure with periods/colons/ - parentheses. (Docs and commit messages are unaffected.) +- **No emojis anywhere in code, config, specs, or docs** (YAML and encoding + hazards; CI fails the build on emojis in Markdown). +- **No em dashes in docs or user-facing UI copy.** Restructure with periods, + colons, or parentheses. Commit messages are unaffected. +- **Developer docs follow the Hanalyx style guide**, and CI enforces its hard + rules. `make docs-style` runs the same check locally that the "Doc Style" job + runs on changed Markdown. The three gates are: no em dashes, no emojis, no AI + speak (hype adjectives, filler openers, padding verbs such as `leverage` and + `utilize`). Write "we" only for real team actions, never for the product, and + state capabilities as team facts with numbers. The guide is the source of + truth and lives in the Hanalyx Context Plane at + `dev/DEVELOPER_DOCUMENTATION_STYLE_GUIDE`; the shared checker lives at + `dev/tools/doc-style-check`. Fix a finding rather than suppress it. A cleared + term can carry `` on its line, with a reason. - **Security is not optional**: parameterized SQL only, argument-list exec (never a shell), RBAC + license gates on handlers, audit auth/authz events, secrets from env/files only. `.golangci.yml` forbidigo encodes several of these diff --git a/Makefile b/Makefile index 4f1fe74e..313debb3 100644 --- a/Makefile +++ b/Makefile @@ -206,12 +206,20 @@ spec-check: specter check --test specter coverage --strictness annotation +# docs-style: the Hanalyx documentation style gate (em dashes, emojis, AI +# speak). Mirrors CI's "Doc Style" job. Single-file python3 script, no +# dependencies. Canonical rules: Context Plane dev/DEVELOPER_DOCUMENTATION_STYLE_GUIDE. +# Use DOC_STYLE_ARGS=--all to sweep the whole tree instead of changed files. +.PHONY: docs-style +docs-style: + python3 scripts/check-doc-style.py $(or $(DOC_STYLE_ARGS),--changed) + # ci-local: run locally what CI's "Quality + security gates" job runs, so a # failure is caught before the ~9-minute push round-trip. `make check` alone # omits the generated-code, spec, and frontend gates — this target is the # full mirror. .PHONY: ci-local -ci-local: check-generated vet lint vuln spec-check test-race +ci-local: check-generated vet lint vuln spec-check test-race docs-style cd frontend && { [ -d node_modules ] || npm ci --no-audit --no-fund; } && npx vitest run @echo "ci-local: all gates passed — safe to push" diff --git a/scripts/check-doc-style.py b/scripts/check-doc-style.py new file mode 100755 index 00000000..f1cdd64d --- /dev/null +++ b/scripts/check-doc-style.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Hanalyx documentation style check (language-neutral: runs anywhere python3 is present). + +Enforces the prohibited list from the developer documentation style guide (canonical copy: +Context Plane dev/DEVELOPER_DOCUMENTATION_STYLE_GUIDE): + + 1. Em dashes (the U+2014 character). + 2. Emojis / decorative pictographs. + 3. AI-speak filler and hype words and phrases. + +Fenced code blocks and inline `code` spans are exempt (code is not prose). A line containing +`` is skipped when a maintainer has cleared the term. + +Usage: + python3 scripts/check-doc-style.py [more.md ...] # check specific files + python3 scripts/check-doc-style.py --changed # markdown changed vs origin/main + python3 scripts/check-doc-style.py --all # all tracked *.md +""" +import re +import subprocess +import sys + +AI_SPEAK = [ + "seamless", "robust", "powerful", "cutting-edge", "best-in-class", "world-class", + "state-of-the-art", "revolutionary", "game-changing", "game-changer", "next-generation", + "enterprise-grade", "blazing-fast", "blazing fast", + "leverage", "utilize", "facilitate", "empower", "unlock", "elevate", "delve", "embark", + "foster", "harness", "supercharge", + "it's important to note", "it is important to note", "it's worth mentioning", + "needless to say", "at the end of the day", "in today's fast-paced world", + "in the ever-evolving", "rest assured", "peace of mind", "let's dive in", "dive in", + "in conclusion", "as an ai", "game changer", +] +# Single tokens match on word boundaries; multiword phrases match as substrings. +AI_RE = [ + (t, re.compile((r"\b" + re.escape(t) + r"\b") if " " not in t else re.escape(t), re.I)) + for t in AI_SPEAK +] +EM_DASH = re.compile("—") +# python3's re has no \p{Extended_Pictographic}; approximate with the common emoji blocks. +EMOJI = re.compile( + "[\U0001F000-\U0001FAFF\U00002600-\U000027BF\U00002B00-\U00002BFF" + "\U0001F1E6-\U0001F1FF\U0000FE00-\U0000FE0F]" +) +INLINE_CODE = re.compile(r"`[^`]*`") +FENCE = re.compile(r"^\s*```") +ALLOW = re.compile(r"") + + +def git(cmd): + try: + return subprocess.run(cmd, capture_output=True, text=True, check=False).stdout.strip() + except Exception: + return "" + + +def resolve_files(argv): + flags = {a for a in argv if a.startswith("--")} + explicit = [a for a in argv if not a.startswith("--") and a.endswith(".md")] + if explicit: + return explicit + if "--all" in flags: + return [f for f in git(["git", "ls-files", "*.md"]).splitlines() if f] + base = git(["git", "merge-base", "origin/main", "HEAD"]) or "origin/main" + changed = git(["git", "diff", "--name-only", "--diff-filter=ACMR", f"{base}...HEAD", "--", "*.md"]) + if not changed: + changed = git(["git", "diff", "--name-only", "--cached", "--", "*.md"]) + return [f for f in changed.splitlines() if f] + + +def main(): + files = resolve_files(sys.argv[1:]) + if not files: + print("doc-style: no markdown to check") + return 0 + + findings = 0 + for path in files: + try: + with open(path, encoding="utf-8") as fh: + lines = fh.read().split("\n") + except OSError: + continue + in_fence = False + for n, raw in enumerate(lines, 1): + if FENCE.match(raw): + in_fence = not in_fence + continue + if in_fence or ALLOW.search(raw): + continue + line = INLINE_CODE.sub("", raw) + + def hit(label, token): + nonlocal findings + findings += 1 + sys.stderr.write(f" {path}:{n} {label}: {token!r}\n") + + if EM_DASH.search(line): + hit("em-dash", "—") + m = EMOJI.search(line) + if m: + hit("emoji", m.group(0)) + for term, rx in AI_RE: + mm = rx.search(line) + if mm: + hit("ai-speak", mm.group(0)) + break + + if findings: + sys.stderr.write( + f"\ndoc-style FAILED: {findings} finding(s). " + "See dev/DEVELOPER_DOCUMENTATION_STYLE_GUIDE.\n" + "Fix the prose, or add `` to a line a maintainer has cleared.\n" + ) + return 1 + print(f"doc-style: {len(files)} file(s) clean") + return 0 + + +if __name__ == "__main__": + sys.exit(main())