Skip to content
Closed
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
20 changes: 17 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.12"]
python: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: pip install pyyaml pytest
- run: pip install . pytest==9.1.1
# A run that collects nothing must not pass: -p no:cacheprovider keeps
# it hermetic, and the count check fails an empty suite.
- run: python -m pytest -q -p no:cacheprovider
Expand All @@ -35,5 +35,19 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install ruff
- run: pip install ruff==0.16.10
- run: ruff check .

wheel:
name: Installed wheel proof
runs-on: ubuntu-latest
env:
SOURCE_DATE_EPOCH: "946684800"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.10"
- run: python -m pip install uv==0.12.0
- run: uv build --wheel --out-dir dist
- run: python scripts/smoke_wheel.py dist/chaski-*.whl
25 changes: 25 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Documentation
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
book:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Install checksummed mdBook
env:
MDBOOK_SHA256: 3f28de05dafca9d0f2eab99c662116b0e37b89b1d96a08f8f430b9eeae958cd7
run: |
set -euo pipefail
asset=mdbook-v0.5.4-x86_64-unknown-linux-gnu.tar.gz
curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset" "https://github.com/rust-lang/mdBook/releases/download/v0.5.4/$asset"
echo "$MDBOOK_SHA256 $RUNNER_TEMP/$asset" | sha256sum -c -
mkdir "$RUNNER_TEMP/mdbook"
tar -xzf "$RUNNER_TEMP/$asset" -C "$RUNNER_TEMP/mdbook"
echo "$RUNNER_TEMP/mdbook" >> "$GITHUB_PATH"
- run: mdbook build
38 changes: 38 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Publish documentation
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: github-pages
cancel-in-progress: true
jobs:
pages:
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Install checksummed mdBook
env:
MDBOOK_SHA256: 3f28de05dafca9d0f2eab99c662116b0e37b89b1d96a08f8f430b9eeae958cd7
run: |
set -euo pipefail
asset=mdbook-v0.5.4-x86_64-unknown-linux-gnu.tar.gz
curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset" "https://github.com/rust-lang/mdBook/releases/download/v0.5.4/$asset"
echo "$MDBOOK_SHA256 $RUNNER_TEMP/$asset" | sha256sum -c -
mkdir "$RUNNER_TEMP/mdbook"
tar -xzf "$RUNNER_TEMP/$asset" -C "$RUNNER_TEMP/mdbook"
echo "$RUNNER_TEMP/mdbook" >> "$GITHUB_PATH"
- run: mdbook build
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa
with:
path: book
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
id: deployment
41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
env:
SOURCE_DATE_EPOCH: "946684800"
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- run: python -m pip install uv==0.12.0
- name: Require a reviewed main commit and matching tag
run: |
set -euo pipefail
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
expected="v$(python -c 'from chaski_version import __version__; print(__version__)')"
test "$GITHUB_REF_NAME" = "$expected"
- run: uv run --frozen pytest -q -p no:cacheprovider
- run: uv run --frozen ruff check .
- run: uv build --out-dir dist
- run: python scripts/smoke_wheel.py dist/chaski-*.whl
- name: Checksum the artifacts
run: |
cd dist
sha256sum chaski-*.whl chaski-*.tar.gz > checksums.txt
- name: Publish the verified artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" --verify-tag \
--repo "$GITHUB_REPOSITORY" --title "chaski $GITHUB_REF_NAME" \
--notes-file CHANGELOG.md dist/chaski-*.whl dist/chaski-*.tar.gz dist/checksums.txt
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
__pycache__/
*.pyc

.venv/
build/
dist/
*.egg-info/
/book/
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Chaski contributor instructions

Chaski supports Python 3.10 and newer. Keep the service argument contract and
flat runtime modules compatible with the installed reactor.

Run the full pytest suite and Ruff before proposing a change. Build a wheel
with `uv build`, then run `python scripts/smoke_wheel.py dist/*.whl` outside an
active service installation. The smoke command creates and removes its own
temporary environment; it never talks to a live graph.

Runtime state includes event cursors, verdicts, deadlines and the outbox. Do
not delete or reset it to make a check pass. Unknown adapter results must
preserve the previous verdict. A successful delivery requires receiver proof.

Release from a reviewed main commit with a `v<version>` tag matching
`chaski_version.py`. The release workflow tests the built wheel before
publishing its wheel, source distribution and checksums. Keep credentials out
of source, logs and example configuration. Release artifacts do not establish
that any service installed or ran them; verify deployment separately.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

## 0.1.0

- Installable Python wheel and source distribution with the `chaski` command.
- Event and schedule reactions, durable verdict/outbox delivery, change-driven
adapters, graph and JSONL receivers, and configured Alertmanager routing.
- An isolated event verification command with negative, delivery, reader and
replay controls for installer verification.
- Versioned release artifacts, contributor instructions and documentation book.
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include AGENTS.md CHANGELOG.md book.toml uv.lock
recursive-include docs *.md
recursive-include scripts *.py
include test_*.py
48 changes: 31 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,42 @@ alert, so alerts resolve on their own.
- **Emission reuses Prometheus and Alertmanager.** chaski exports metrics; it
does not invent notification plumbing.

## Status
## Install and first event

Stage 1: a read-only skeleton, with the event tail, schedule rules, metrics and
the canary. No writes and no pages yet.
Python 3.10 or newer is required. Download a reviewed wheel and its checksums
from [GitHub releases](https://github.com/scbrown/chaski/releases), verify its
SHA256, then install it with pip or pipx:

Stage 2 (in progress): **emitters** (`emitter.py`). An emitter runs an external
verdict adapter on a schedule and turns its answers into transition events. A
first sighting is a baseline, "unknown" keeps the last verdict, and only a
known blocked-to-unblocked transition emits. Verdict and outbox commit in one
transaction. Delivery is at least once to a receiver that deduplicates on the
adapter's deterministic event id. Configure with `--emitters emitters.yaml`:
```sh
pipx install ./chaski-0.1.0-py3-none-any.whl
chaski --version
chaski --help
chaski events --directory ./proof
chaski verify-event --directory ./proof --marker first-event
chaski events --directory ./proof
```

```yaml
emitters:
- label: workitem-unblocked
command: [python3, /path/to/blocked_by.py]
schedule: PT15M
owner: someone
event: unblocked
sink: {jsonl: /var/lib/chaski/unblocked.jsonl}
The reads return `[]` before and `["first-event"]` after. The proof runs the
actual adapter, verdict/outbox transaction and JSONL receiver in a new scratch
directory, including a receiver replay control. It never contacts a live graph
or starts the service. Reusing a proof directory is refused.

Run the reactor using explicit configuration and preserved state:

```sh
chaski --quipu http://localhost:3030 --rules rules.example.yaml --state state/cursor.json
```

The source includes durable emitters, graph/JSONL receivers, configured
Alertmanager routing, and change-driven adapters. Sink writes are opt-in through
configuration and share a global write budget. Runtime installation and graph
admission are separate from a successful local event proof.

Read the [installation book](docs/book/src/installing.md),
[configuration](docs/book/src/configuration.md), and
[operations](docs/book/src/operations.md). Contributors should read
[AGENTS.md](AGENTS.md) and [CHANGELOG.md](CHANGELOG.md).

## License

Apache-2.0. See [LICENSE](LICENSE).
Expand Down
8 changes: 8 additions & 0 deletions book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[book]
title = "Chaski"
authors = ["Steve Brown"]
language = "en"
src = "docs/book/src"

[output.html]
git-repository-url = "https://github.com/scbrown/chaski"
10 changes: 9 additions & 1 deletion chaski.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import argparse
import sys
import json
import logging
import re
Expand All @@ -30,6 +31,8 @@

import yaml

from chaski_version import __version__

if TYPE_CHECKING:
import emitter

Expand Down Expand Up @@ -397,7 +400,12 @@ def make_sink(e: emitter.Emitter, sink: dict, quipu_url: str):


def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
argv = sys.argv[1:] if argv is None else argv
if argv and argv[0] in {"verify-event", "events"}:
from chaski_verify import main as verification_main
return verification_main(argv)
ap = argparse.ArgumentParser(description="The chaski CLI: durable reactions for Quipu knowledge graphs")
ap.add_argument("--version", action="version", version=f"chaski {__version__}")
ap.add_argument("--quipu", required=True, help="quipu base URL")
ap.add_argument("--rules", required=True, type=Path)
ap.add_argument("--state", required=True, type=Path, help="event cursor file")
Expand Down
63 changes: 63 additions & 0 deletions chaski_verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Local event delivery proof using the production emitter and JSONL receiver."""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import emitter


def verify_event(root: Path, marker: str) -> dict:
root.mkdir(parents=True, exist_ok=True)
work = root / "chaski-event-proof"
work.mkdir() # Never reuse or replace a caller's existing database.
ledger = work / "events.jsonl"
conn = emitter.connect(work / "emitter.db")
sink = emitter.JsonlSink(ledger)
try:
if sink.ids():
raise RuntimeError("negative receiver control failed")
state = work / "verdict.json"
adapter = "import pathlib,sys; print(pathlib.Path(sys.argv[1]).read_text())"
item = emitter.Emitter("package-proof", [sys.executable, "-c", adapter, str(state)], 1, "local")
runner = emitter.Runner(item, conn, sink, emitter.WriteBudget(1))
state.write_text(json.dumps([{"item": marker, "verdict": "BLOCKED"}]))
runner.tick(1000)
if sink.ids() or conn.execute("SELECT COUNT(*) FROM outbox").fetchone()[0]:
raise RuntimeError("baseline unexpectedly emitted")
state.write_text(json.dumps([{"item": marker, "verdict": "UNBLOCKED", "event_id": marker}]))
runner.tick(1002)
row = conn.execute("SELECT payload, delivered_at FROM outbox WHERE event_id=?", (marker,)).fetchone()
if not row or row[1] is None or sink.ids() != {marker}:
raise RuntimeError("event did not reach the receiver")
event = json.loads(row[0])
sink.deliver(event) # Receiver retry must preserve exactly one event.
lines = [json.loads(line) for line in ledger.read_text().splitlines()]
if len(lines) != 1 or lines[0]["event_id"] != marker or runner.runs != {"ok": 2, "unknown": 0}:
raise RuntimeError("event round trip or replay control failed")
return {"verified": True, "receiver": "jsonl", "events": 1, "database": str(work / "emitter.db")}
finally:
conn.close()


def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
proof = sub.add_parser("verify-event", help="emit and receive an isolated local event")
proof.add_argument("--directory", type=Path, required=True)
proof.add_argument("--marker", required=True)
read = sub.add_parser("events", help="read event IDs from the local proof receiver")
read.add_argument("--directory", type=Path, required=True)
args = parser.parse_args(argv)
if args.command == "events":
print(json.dumps(sorted(emitter.JsonlSink(args.directory / "chaski-event-proof" / "events.jsonl").ids())))
return 0
try:
result = verify_event(args.directory, args.marker)
except (OSError, ValueError, RuntimeError) as error:
print(f"verification failed: {error}", file=sys.stderr)
return 1
print(json.dumps(result, sort_keys=True))
return 0
2 changes: 2 additions & 0 deletions chaski_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""The version shared by build metadata and the console entry point."""
__version__ = "0.1.0"
6 changes: 6 additions & 0 deletions docs/book/src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Summary

- [Chaski](introduction.md)
- [Install and verify](installing.md)
- [Configuration and state](configuration.md)
- [Operations and releases](operations.md)
21 changes: 21 additions & 0 deletions docs/book/src/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Configuration and state

Run the reactor with explicit configuration and state paths:

```sh
chaski --quipu http://localhost:3030 --rules rules.yaml --state state/cursor.json
```

Rules use `triggerKind`, `condition`, `severity`, `action`, `owner` and optional
schedule/event filters. The condition returns `?focus`. Unknown evaluations
retain the last firing set rather than resolving it.

Optional emitters run verdict adapters. Each emitter declares its command,
owner, transition and sink. Change-driven adapters receive discovery, routing
and bounded evaluation requests; their daily schedule reconciles catalogue
state. Event polling retains the sixty-second floor and sink writes share one
global budget.

Keep cursor files, emitter databases, verdicts, deadlines and delivery history.
Credentials are read from explicitly configured files; do not put them in
rules, package metadata or command arguments.
Loading
Loading