Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions .claude/skills/write-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ Generate or regenerate an operator-facing document from the documentation spec.
```

Where `<doc-name>` 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

Expand All @@ -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.

Expand All @@ -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`
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/go-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 21 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# AGENTS.mdcontributor & 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.

Expand All @@ -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.

Expand Down Expand Up @@ -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)
Expand All @@ -74,18 +74,29 @@ Every acceptance criterion (`AC-NN`) must be covered by a test carrying a litera
`// @spec <id>` + `// @ac AC-NN` annotation, and Go subtests also use a
`t.Run("<spec-id>/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 `<!-- doc-style: allow -->` 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
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
121 changes: 121 additions & 0 deletions scripts/check-doc-style.py
Original file line number Diff line number Diff line change
@@ -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
`<!-- doc-style: allow -->` is skipped when a maintainer has cleared the term.

Usage:
python3 scripts/check-doc-style.py <file.md> [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"<!--\s*doc-style:\s*allow\s*-->")


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 `<!-- doc-style: allow -->` 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())
Loading