feat(watch): weekly OKF upstream watch that can prove it found nothing
The operator asked for a job that checks at least weekly whether Google OKF has moved, and messages the right repo immediately when it has. It belongs here rather than in `.claude` because knowing what a meaningful spec change IS requires owning the pin, the runbook and the always-latest policy. `tools/okf_watch.py`, stdlib only, driving git against the local read-only mirror. It lives outside `src/` so it never enters a wheel; a new packaging test holds that as a promise rather than an accident of the build config. Three properties carry the design, and each closes a failure this repo has actually met: 1. A failed call is never an empty result. Every git invocation raises on a non-zero exit and carries stderr, so a caller reading "" knows the query ran. The precedent is `grep ... | head; echo $?` reporting head's exit status - a broken query read as a quiet upstream. 2. It proves it can find, every run. Before believing any zero it re-runs the full detect-and-classify path over `ad30107^1..ad30107`, a range known to have changed SPEC.md. An empty known-positive aborts loudly rather than reporting a clean sweep. Network failure likewise raises; it never degrades to "no change". 3. It reports on change, not on state. A pin-keyed state file records what has been announced; moving the pin resets it, because a pin move means everything behind it was absorbed. Quiet is the enumerated list, not signal. Enumerating what counts as normative can only match what upstream has already invented, so anything new would fall outside it and the watch would go silent - failing in the direction nobody notices. A small measured quiet list, everything else reports. README.md is deliberately not quiet: the repository move was announced in a README commit. Sixteen tests build their own git repository in tmp_path rather than skipping when the mirror is absent - a skipped test preserves nothing on the machine where the dependency exists. All four load-bearing behaviours were mutation- tested red before this landed. Two more tests exist because building this fired a real false alarm: running with `--pin` and without `--dry-run` delivered two live coord messages. The override now implies dry-run, enforced in argument parsing rather than remembered, and `.claude` has the correction. The runbook gains a section stating what the watch CANNOT do, because that is the part a future session will otherwise assume away: it sees commits, not meaning. It would have fired on the 2026-08 tightening because SPEC.md changed, but no commit list says a value that conformed last month no longer does, and none says is_stale reversed. Its output is "run the runbook", never "here is your exposure".
This commit is contained in:
parent
e286b5a173
commit
3233b19b30
4 changed files with 719 additions and 3 deletions
|
|
@ -42,6 +42,10 @@ notice. So the re-check is an item on the release checklist — run it at every
|
|||
release of this library, and record the result **even when unchanged**, because an
|
||||
unrecorded check is indistinguishable from a skipped one.
|
||||
|
||||
**Since 2026-08-23 the trigger also fires without a release**, weekly, from
|
||||
`tools/okf_watch.py`. See § The weekly watch below. The watch decides *whether*
|
||||
this procedure runs; it never substitutes for it.
|
||||
|
||||
Check `GoogleCloudPlatform/open-knowledge-format`. **That is the canonical home of
|
||||
the spec, the reference agent and the sample bundles as of 2026-08-21.** A version
|
||||
bump appears as a commit against `SPEC.md` §12 and, in the v0.2 round, as an
|
||||
|
|
@ -54,9 +58,14 @@ will drift out of date"), and this repo was pinned to it until the 2026-08-23 ro
|
|||
Two consequences, both measured that round and neither hypothetical:
|
||||
|
||||
- **The two trees have already diverged**, and not only in the direction you would
|
||||
expect: the frozen copy carries a fix (`38c713f`, eight `tags:` values written as
|
||||
sequences rather than as one plain scalar) that the canonical repo does not. The
|
||||
canonical tree is authoritative for the *spec*; it is not automatically a superset.
|
||||
expect: the frozen repository's *head* carries a fix (`38c713f`, eight `tags:`
|
||||
values written as sequences rather than as one plain scalar) that the canonical
|
||||
repo does not. The canonical tree is authoritative for the *spec*; it is not
|
||||
automatically a superset. Note the pin-level precision, measured 2026-08-23:
|
||||
`38c713f` is **not** an ancestor of the old pin `3fcbb9f` either, so moving the
|
||||
pin lost nothing — canonical simply ships a form its own frozen predecessor has
|
||||
already repaired. Enumerated in full in
|
||||
`docs/plan/okf-2026-08-timestamp-tightening.md` § Known divergence.
|
||||
- **A round run against the frozen tree reports "no change" truthfully and
|
||||
uselessly** — the exact shape of a negative result that is not a measurement.
|
||||
|
||||
|
|
@ -271,6 +280,84 @@ survived verification is only known to have survived if the check is recorded, a
|
|||
claim we withdrew is only safely withdrawn if the withdrawal is written where the
|
||||
claim was.
|
||||
|
||||
## The weekly watch — `tools/okf_watch.py`
|
||||
|
||||
Answers one question on a schedule: *has canonical moved past our pin, and does
|
||||
the move touch anything that bears the contract?* On a hit it sends a coord
|
||||
message to this repo and, as FYI, to `.claude`. On a miss it prints one line and
|
||||
exits 0.
|
||||
|
||||
It is deliberately **not** part of the package: it lives in `tools/`, outside
|
||||
`src/`, so it never enters a wheel and a consumer's install surface is unchanged.
|
||||
`tests/test_packaging.py` holds that as a promise rather than an accident.
|
||||
|
||||
**Run it:**
|
||||
|
||||
python3 tools/okf_watch.py # the real weekly run
|
||||
python3 tools/okf_watch.py --dry-run # print the messages, send nothing
|
||||
python3 tools/okf_watch.py --pin <sha> # demonstrate the hit path (implies --dry-run)
|
||||
|
||||
**Cadence: weekly is the floor.** It costs one `git fetch` against a
|
||||
`blob:none` mirror, so running it daily is not meaningfully more expensive.
|
||||
|
||||
### Three properties, and why each is load-bearing
|
||||
|
||||
1. **A failed call is never an empty result.** Every `git` invocation raises on a
|
||||
non-zero exit and carries stderr. The failure mode this closes is specific and
|
||||
has been met before: `grep … | head; echo $?` reports the exit status of
|
||||
`head`, and a query that failed then reads as a query that found nothing.
|
||||
2. **It proves it can find, on every run.** Before believing any zero, the watch
|
||||
re-runs its full detect-and-classify path over `ad30107^1..ad30107` — a range
|
||||
known to have changed `SPEC.md`. If that comes back empty the query is broken,
|
||||
and the run aborts loudly instead of reporting a clean sweep. This is
|
||||
Verification-law face 4 made executable rather than remembered.
|
||||
3. **It reports on change, not on state.** A JSON state file records which
|
||||
commits have already been announced, keyed on the pin. Moving the pin resets
|
||||
it, because a pin move means everything behind it was absorbed.
|
||||
|
||||
### Quiet is the enumerated list; signal is not
|
||||
|
||||
`QUIET_PREFIXES` names the paths measured *not* to bear the contract
|
||||
(`.github/`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `LICENSE.md`, the HTML
|
||||
viewer, generated `viz.html`). **Everything else reports.**
|
||||
|
||||
The inverse design — enumerate what counts as normative — can only match what
|
||||
upstream has already invented, so anything new falls outside the list and the
|
||||
watch goes quiet about it. That fails in the direction nobody notices.
|
||||
Over-firing is visible and fixable by widening the quiet list; under-firing is
|
||||
neither. **If the watch becomes noisy, widen `QUIET_PREFIXES`. Do not narrow the
|
||||
signal.**
|
||||
|
||||
`README.md` is deliberately not quiet: upstream announced the repository move in
|
||||
a README commit, and that move is the change with the longest reach this library
|
||||
has seen.
|
||||
|
||||
### What the watch cannot do — state this when reporting it
|
||||
|
||||
It sees commits. It cannot see meaning.
|
||||
|
||||
When upstream tightened v0.2 in place on 2026-08-21, the watch would have fired
|
||||
correctly, because `SPEC.md` changed. But **no commit list says "a value that
|
||||
conformed last month does not conform now"**, and none says `is_stale` has
|
||||
reversed for date-only inputs. Those were found by reading the diff and running
|
||||
both readers against the same input. So the watch's output is always *run the
|
||||
runbook*, never *here is your exposure* — and the message it sends says so in as
|
||||
many words.
|
||||
|
||||
Two further blind spots, named rather than left to be discovered:
|
||||
|
||||
- **A silent relocation.** The last move was caught only because upstream
|
||||
committed a notice to `README.md`. A move announced anywhere other than this
|
||||
git history is invisible here.
|
||||
- **A tightening with no commit at all** — a spec whose meaning is changed by an
|
||||
external document, an errata page, a changed reference implementation shipped
|
||||
under a different repository. Nothing local can see that. The release-checklist
|
||||
trigger, which reads rather than diffs, is the only cover.
|
||||
|
||||
The watch narrows the window between an upstream change and our noticing it. It
|
||||
does not close it, and a session that treats a quiet watch as proof that upstream
|
||||
is unchanged has made exactly the mistake the watch was built to prevent.
|
||||
|
||||
## Invariants this procedure protects
|
||||
|
||||
- No profile hard-codes an upstream version.
|
||||
|
|
|
|||
223
tests/test_okf_watch.py
Normal file
223
tests/test_okf_watch.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
"""The upstream OKF watch: it must be able to prove it can find.
|
||||
|
||||
This watch reports a NEGATIVE result almost every time it runs. That is the
|
||||
whole hazard: a run that never reached the network, or whose query silently
|
||||
matched nothing, produces the same "no change" as a run that genuinely found
|
||||
nothing. Trust in an absence that was never measured is worse than no watch,
|
||||
so every test here is about keeping those two outcomes distinguishable.
|
||||
|
||||
The git-backed tests build their own repository in tmp_path. Skipping when
|
||||
`~/repos/_okf-canonical` is missing would preserve nothing on the machine
|
||||
where it exists, and pointing them at the real mirror would make the suite
|
||||
depend on upstream's history.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
||||
|
||||
import okf_watch # noqa: E402
|
||||
|
||||
|
||||
# --- the exit-status / empty-result seam -----------------------------------
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", *args], cwd=repo, capture_output=True, text=True, check=True
|
||||
).stdout
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path: Path) -> Path:
|
||||
"""A repository with one quiet commit and one normative commit."""
|
||||
r = tmp_path / "upstream"
|
||||
r.mkdir()
|
||||
_git(r, "init", "-q", "-b", "main")
|
||||
_git(r, "config", "user.email", "t@example.invalid")
|
||||
_git(r, "config", "user.name", "t")
|
||||
(r / "SPEC.md").write_text("Version 0.2\n", encoding="utf-8")
|
||||
(r / "CONTRIBUTING.md").write_text("hello\n", encoding="utf-8")
|
||||
_git(r, "add", "-A")
|
||||
_git(r, "commit", "-q", "-m", "base")
|
||||
(r / "CONTRIBUTING.md").write_text("hello again\n", encoding="utf-8")
|
||||
_git(r, "add", "-A")
|
||||
_git(r, "commit", "-q", "-m", "docs: contributing")
|
||||
(r / "SPEC.md").write_text("Version 0.2 tightened\n", encoding="utf-8")
|
||||
_git(r, "add", "-A")
|
||||
_git(r, "commit", "-q", "-m", "spec: tighten timestamps")
|
||||
return r
|
||||
|
||||
|
||||
def test_a_failing_git_call_raises_instead_of_returning_empty(repo: Path) -> None:
|
||||
"""The defect this watch exists to avoid, at its smallest.
|
||||
|
||||
`git log <nonexistent>` writes to stderr and exits non-zero. A helper that
|
||||
returned its empty stdout would hand the caller "no commits" — a negative
|
||||
manufactured by a broken query.
|
||||
"""
|
||||
with pytest.raises(okf_watch.WatchError) as exc:
|
||||
okf_watch.git(repo, "log", "--format=%H", "no-such-ref..HEAD")
|
||||
assert "exit" in str(exc.value)
|
||||
|
||||
|
||||
def test_an_empty_result_from_a_successful_call_is_not_an_error(repo: Path) -> None:
|
||||
"""The other half of the seam: a real zero must still be readable as zero."""
|
||||
head = okf_watch.git(repo, "rev-parse", "HEAD")
|
||||
assert okf_watch.git(repo, "log", "--format=%H", f"{head}..{head}") == ""
|
||||
|
||||
|
||||
# --- classification --------------------------------------------------------
|
||||
|
||||
|
||||
def test_a_spec_change_is_normative() -> None:
|
||||
normative, quiet = okf_watch.classify(["SPEC.md", "CONTRIBUTING.md"])
|
||||
assert normative == ["SPEC.md"]
|
||||
assert quiet == ["CONTRIBUTING.md"]
|
||||
|
||||
|
||||
def test_the_reference_reader_is_normative_because_is_stale_reversed_there() -> None:
|
||||
normative, _ = okf_watch.classify(["src/reference_agent/bundle/document.py"])
|
||||
assert normative == ["src/reference_agent/bundle/document.py"]
|
||||
|
||||
|
||||
def test_the_viewer_is_quiet_but_the_readme_is_not() -> None:
|
||||
"""README carries the relocation notice. That is how the move was announced."""
|
||||
normative, quiet = okf_watch.classify(
|
||||
["src/reference_agent/viewer/static/viz.css", "bundles/acme_retail/viz.html", "README.md"]
|
||||
)
|
||||
assert normative == ["README.md"]
|
||||
assert quiet == ["src/reference_agent/viewer/static/viz.css", "bundles/acme_retail/viz.html"]
|
||||
|
||||
|
||||
def test_an_unknown_path_defaults_to_normative() -> None:
|
||||
"""Quiet is an enumerated list; signal is not.
|
||||
|
||||
A watch that enumerates what matters cannot see a change upstream has not
|
||||
invented yet, and fails in the silent direction. Over-firing is visible;
|
||||
under-firing is not.
|
||||
"""
|
||||
normative, quiet = okf_watch.classify(["some/new/thing-upstream-adds-later.md"])
|
||||
assert normative == ["some/new/thing-upstream-adds-later.md"]
|
||||
assert quiet == []
|
||||
|
||||
|
||||
# --- proving it can find ---------------------------------------------------
|
||||
|
||||
|
||||
def test_the_self_check_passes_on_a_range_that_really_changed_the_spec(repo: Path) -> None:
|
||||
base = okf_watch.git(repo, "rev-parse", "HEAD~1")
|
||||
head = okf_watch.git(repo, "rev-parse", "HEAD")
|
||||
okf_watch.prove_can_find(repo, base, head) # must not raise
|
||||
|
||||
|
||||
def test_the_self_check_fails_loudly_when_the_query_finds_nothing(repo: Path) -> None:
|
||||
"""An empty known-positive means the query is broken, not that upstream is quiet."""
|
||||
head = okf_watch.git(repo, "rev-parse", "HEAD")
|
||||
with pytest.raises(okf_watch.WatchError) as exc:
|
||||
okf_watch.prove_can_find(repo, head, head)
|
||||
assert "known-positive" in str(exc.value)
|
||||
|
||||
|
||||
def test_the_self_check_fails_loudly_when_the_range_is_unreachable(repo: Path) -> None:
|
||||
with pytest.raises(okf_watch.WatchError):
|
||||
okf_watch.prove_can_find(repo, "deadbeef", "HEAD")
|
||||
|
||||
|
||||
# --- idempotence: report on change, not on state ---------------------------
|
||||
|
||||
|
||||
def test_a_first_normative_change_is_reported(tmp_path: Path) -> None:
|
||||
state = okf_watch.State.load(tmp_path / "s.json", pin="PIN")
|
||||
assert okf_watch.decide(state, ["aaa", "bbb"]) == ["aaa", "bbb"]
|
||||
|
||||
|
||||
def test_the_same_upstream_state_is_not_reported_twice(tmp_path: Path) -> None:
|
||||
path = tmp_path / "s.json"
|
||||
state = okf_watch.State.load(path, pin="PIN")
|
||||
okf_watch.decide(state, ["aaa"])
|
||||
state.record(["aaa"])
|
||||
state.save(path)
|
||||
again = okf_watch.State.load(path, pin="PIN")
|
||||
assert okf_watch.decide(again, ["aaa"]) == []
|
||||
|
||||
|
||||
def test_only_the_new_commits_are_reported(tmp_path: Path) -> None:
|
||||
"""Upstream advancing must not re-announce what was already announced."""
|
||||
path = tmp_path / "s.json"
|
||||
state = okf_watch.State.load(path, pin="PIN")
|
||||
state.record(["aaa"])
|
||||
state.save(path)
|
||||
later = okf_watch.State.load(path, pin="PIN")
|
||||
assert okf_watch.decide(later, ["aaa", "bbb"]) == ["bbb"]
|
||||
|
||||
|
||||
def test_moving_our_pin_resets_what_counts_as_already_reported(tmp_path: Path) -> None:
|
||||
"""A pin move means we absorbed everything behind it. Nothing before it is news."""
|
||||
path = tmp_path / "s.json"
|
||||
state = okf_watch.State.load(path, pin="OLDPIN")
|
||||
state.record(["aaa"])
|
||||
state.save(path)
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["pin"] == "OLDPIN"
|
||||
after = okf_watch.State.load(path, pin="NEWPIN")
|
||||
assert after.reported == []
|
||||
|
||||
|
||||
# --- what gets sent --------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_coord_body_is_ascii_because_coord_send_requires_it() -> None:
|
||||
body = okf_watch.render_body(
|
||||
pin="ad30107",
|
||||
head="beef123",
|
||||
commits=[("beef123", "spec: tighten timestamps")],
|
||||
normative=["SPEC.md"],
|
||||
quiet=["CONTRIBUTING.md"],
|
||||
)
|
||||
body.encode("ascii") # raises UnicodeEncodeError if not
|
||||
|
||||
|
||||
def test_the_body_names_the_repository_it_watched() -> None:
|
||||
""" "No change" is only meaningful with the object named. So is a hit."""
|
||||
body = okf_watch.render_body(
|
||||
pin="ad30107", head="beef123", commits=[("beef123", "x")], normative=["SPEC.md"], quiet=[]
|
||||
)
|
||||
assert okf_watch.CANONICAL_REPO in body
|
||||
assert "ad30107" in body and "beef123" in body
|
||||
|
||||
|
||||
def test_the_body_carries_the_denominator_not_just_the_hits() -> None:
|
||||
body = okf_watch.render_body(
|
||||
pin="p",
|
||||
head="h",
|
||||
commits=[("a", "one"), ("b", "two")],
|
||||
normative=["SPEC.md"],
|
||||
quiet=["CONTRIBUTING.md", "LICENSE.md"],
|
||||
)
|
||||
assert "1 of 3" in body
|
||||
|
||||
|
||||
# --- the override that fired a false alarm ---------------------------------
|
||||
|
||||
|
||||
def test_an_overridden_pin_cannot_send_a_real_message(tmp_path: Path, repo: Path) -> None:
|
||||
"""Measured 2026-08-23: this exact combination delivered two false alarms.
|
||||
|
||||
`--pin` exists so the reporting branch can be exercised on demand against
|
||||
real upstream history. Exercising it must never be indistinguishable from
|
||||
upstream actually moving, so the override forces dry-run rather than
|
||||
trusting the operator to pair two flags correctly.
|
||||
"""
|
||||
args = okf_watch.parse_args(["--pin", "deadbeef", "--state", str(tmp_path / "s.json")])
|
||||
assert args.dry_run is True
|
||||
|
||||
|
||||
def test_the_real_pin_leaves_dry_run_alone() -> None:
|
||||
assert okf_watch.parse_args([]).dry_run is False
|
||||
|
|
@ -47,3 +47,18 @@ def test_the_declared_version_agrees_with_the_packaged_one() -> None:
|
|||
tomllib = pytest.importorskip("tomllib")
|
||||
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
assert llm_ingestion_okf.__version__ == pyproject["project"]["version"]
|
||||
|
||||
|
||||
def test_operational_tooling_stays_out_of_the_wheel() -> None:
|
||||
"""`tools/` is ours, not the consumer's.
|
||||
|
||||
The upstream watch drives git and the coord mailbox — machinery that is
|
||||
meaningful on this machine and meaningless in a consumer's site-packages.
|
||||
It lives outside `src/` so it cannot ship, and this test is what makes
|
||||
that a promise instead of an accident of the current build config.
|
||||
"""
|
||||
tomllib = pytest.importorskip("tomllib")
|
||||
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
packages = pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"]
|
||||
assert packages == ["src/llm_ingestion_okf"]
|
||||
assert (PROJECT_ROOT / "tools" / "okf_watch.py").is_file(), "the test must have a subject"
|
||||
|
|
|
|||
391
tools/okf_watch.py
Normal file
391
tools/okf_watch.py
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Weekly watch on the canonical Open Knowledge Format repository.
|
||||
|
||||
Answers one question: **has canonical moved past our pin, and does the move
|
||||
touch anything that bears the contract?** On a hit it sends a coord message
|
||||
immediately. On a miss it says nothing and exits 0.
|
||||
|
||||
It is not part of the package. It lives outside `src/`, so it never enters a
|
||||
wheel, and a consumer's install surface is unchanged by its existence. Stdlib
|
||||
only, no network library: it drives `git` against the local read-only mirror.
|
||||
|
||||
## Why this file is built the way it is
|
||||
|
||||
Almost every run of this watch produces a NEGATIVE result. That is the hazard
|
||||
it is designed around, and the three rules that follow are not stylistic:
|
||||
|
||||
1. **A failed call is never an empty result.** `git()` raises on a non-zero
|
||||
exit and carries stderr. A helper that returned the empty stdout of a
|
||||
failed `git log` would manufacture "upstream is quiet" out of a broken
|
||||
query, which is the exact defect this watch exists to prevent.
|
||||
2. **It proves it can find, every run.** `prove_can_find()` re-runs the whole
|
||||
detect-and-classify path over a range known to have changed `SPEC.md`
|
||||
(`ad30107^1..ad30107`, the ISO-datetime tightening). If that range comes
|
||||
back empty, the query is broken and the run aborts LOUDLY rather than
|
||||
reporting a clean sweep.
|
||||
3. **The network failing is loud.** `git fetch` failing raises. It does not
|
||||
degrade to "no change since last time".
|
||||
|
||||
## Why quiet is the enumerated list, not signal
|
||||
|
||||
Listing what counts as normative can only match what upstream has already
|
||||
invented. Anything new falls outside the list and the watch goes silent about
|
||||
it — failing in the direction nobody notices. So the enumeration runs the
|
||||
other way: a small list of paths measured not to bear the contract, and
|
||||
everything else reports. Over-firing is visible and fixable by widening
|
||||
QUIET_PREFIXES. Under-firing is neither.
|
||||
|
||||
`README.md` is deliberately NOT quiet. Upstream announced the repository move
|
||||
in a README commit, and that move is the change with the longest reach this
|
||||
library has seen.
|
||||
|
||||
## What this watch does NOT catch
|
||||
|
||||
It sees commits. It cannot see *meaning*. When upstream tightened v0.2 in
|
||||
place without a version bump, this watch would have fired correctly (SPEC.md
|
||||
changed) — but nothing in a commit list says "a value that conformed last
|
||||
month does not conform now", and nothing said `is_stale` had reversed. The
|
||||
watch's output is therefore "run the runbook", never "here is your exposure".
|
||||
`docs/upstream-okf-upgrade-runbook.md` is the part that reads meaning, and it
|
||||
is a human procedure on purpose.
|
||||
|
||||
It also cannot see upstream abandoning this repository *silently*. It caught
|
||||
the last move only because upstream committed a notice to README. A relocation
|
||||
announced somewhere other than this git history is invisible here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL_REPO = "GoogleCloudPlatform/open-knowledge-format"
|
||||
CANONICAL_URL = f"https://github.com/{CANONICAL_REPO}.git"
|
||||
|
||||
#: Our pin. Moves only when the runbook has been run and the plan re-recorded;
|
||||
#: `docs/plan/okf-v0.2-alignment.md` is the record of record.
|
||||
PIN = "ad30107c31c06aec8a7d5636e0d1058118604e6f"
|
||||
|
||||
#: A range in canonical that really did change SPEC.md, used every run to show
|
||||
#: the query can find. `ad30107` is the merge of `okf-iso-datetimes`; its first
|
||||
#: parent range carries three SPEC.md commits.
|
||||
KNOWN_POSITIVE = (
|
||||
"ad30107c31c06aec8a7d5636e0d1058118604e6f^1",
|
||||
"ad30107c31c06aec8a7d5636e0d1058118604e6f",
|
||||
)
|
||||
|
||||
#: Paths measured not to bear the contract. Everything else is normative.
|
||||
QUIET_PREFIXES = (
|
||||
".github/",
|
||||
".gitignore",
|
||||
"CONTRIBUTING.md",
|
||||
"CODE_OF_CONDUCT.md",
|
||||
"LICENSE.md",
|
||||
"src/reference_agent/viewer/",
|
||||
)
|
||||
#: Generated viewer output, committed alongside bundles it does not define.
|
||||
QUIET_SUFFIXES = ("viz.html",)
|
||||
|
||||
DEFAULT_MIRROR = Path.home() / "repos" / "_okf-canonical"
|
||||
DEFAULT_STATE = Path.home() / ".claude" / "okf-watch-state.json"
|
||||
SELF_REPO = "llm-ingestion-okf"
|
||||
COORDINATOR_REPO = ".claude"
|
||||
|
||||
|
||||
class WatchError(RuntimeError):
|
||||
"""Anything that stops this watch from measuring. Never swallowed.
|
||||
|
||||
Raised for a failed git call, an unreachable mirror, a failed fetch, or a
|
||||
known-positive that finds nothing. Each of those would otherwise surface
|
||||
as "no change" — a negative nobody measured.
|
||||
"""
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
"""Run git, or raise. Returns stripped stdout; an empty return means empty.
|
||||
|
||||
The distinction the whole file rests on: a non-zero exit raises, so a
|
||||
caller reading `""` knows the query ran and found nothing.
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True, timeout=300)
|
||||
except FileNotFoundError as exc: # pragma: no cover - git absent
|
||||
raise WatchError(f"git not found on PATH: {exc}") from exc
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise WatchError(f"git {' '.join(args)} timed out after 300s") from exc
|
||||
if proc.returncode != 0:
|
||||
raise WatchError(
|
||||
f"git {' '.join(args)} failed with exit {proc.returncode} in {repo}: "
|
||||
f"{proc.stderr.strip() or '(no stderr)'}"
|
||||
)
|
||||
return proc.stdout.strip()
|
||||
|
||||
|
||||
def classify(paths: list[str]) -> tuple[list[str], list[str]]:
|
||||
"""Split changed paths into (normative, quiet), preserving input order."""
|
||||
normative: list[str] = []
|
||||
quiet: list[str] = []
|
||||
for p in paths:
|
||||
if p.startswith(QUIET_PREFIXES) or p.endswith(QUIET_SUFFIXES):
|
||||
quiet.append(p)
|
||||
else:
|
||||
normative.append(p)
|
||||
return normative, quiet
|
||||
|
||||
|
||||
def changed_paths(mirror: Path, base: str, head: str) -> list[str]:
|
||||
"""Paths touched between two refs, de-duplicated, order preserved."""
|
||||
out = git(mirror, "log", "--first-parent", "--name-only", "--format=", f"{base}..{head}")
|
||||
seen: dict[str, None] = {}
|
||||
for line in out.splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
seen.setdefault(line, None)
|
||||
return list(seen)
|
||||
|
||||
|
||||
def commits_between(mirror: Path, base: str, head: str) -> list[tuple[str, str]]:
|
||||
out = git(mirror, "log", "--first-parent", "--format=%h%x1f%s", f"{base}..{head}")
|
||||
rows: list[tuple[str, str]] = []
|
||||
for line in out.splitlines():
|
||||
if "\x1f" in line:
|
||||
sha, subject = line.split("\x1f", 1)
|
||||
rows.append((sha, subject))
|
||||
return rows
|
||||
|
||||
|
||||
def prove_can_find(
|
||||
mirror: Path, base: str = KNOWN_POSITIVE[0], head: str = KNOWN_POSITIVE[1]
|
||||
) -> None:
|
||||
"""Run the real query over a range known to be positive, or abort.
|
||||
|
||||
Verification law, face 4, made executable: a negative result is only a
|
||||
measurement once the instrument has been shown able to produce a positive.
|
||||
"""
|
||||
paths = changed_paths(mirror, base, head)
|
||||
normative, _ = classify(paths)
|
||||
if not normative:
|
||||
raise WatchError(
|
||||
f"known-positive range {base}..{head} returned no normative paths. "
|
||||
"The query is broken, not upstream quiet. Refusing to report a clean sweep."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
"""What has already been announced, so a hit is announced exactly once.
|
||||
|
||||
Keyed on the pin: moving the pin means we absorbed everything behind it,
|
||||
so nothing before the new pin is news any more.
|
||||
"""
|
||||
|
||||
pin: str
|
||||
reported: list[str] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path, pin: str) -> State:
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return cls(pin=pin)
|
||||
if raw.get("pin") != pin:
|
||||
return cls(pin=pin)
|
||||
reported = [str(s) for s in raw.get("reported", [])]
|
||||
return cls(pin=pin, reported=reported)
|
||||
|
||||
def record(self, shas: list[str]) -> None:
|
||||
for s in shas:
|
||||
if s not in self.reported:
|
||||
self.reported.append(s)
|
||||
|
||||
def save(self, path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps({"pin": self.pin, "reported": self.reported}, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def decide(state: State, normative_shas: list[str]) -> list[str]:
|
||||
"""The commits worth a message: normative, and not announced before."""
|
||||
return [s for s in normative_shas if s not in state.reported]
|
||||
|
||||
|
||||
def render_body(
|
||||
*,
|
||||
pin: str,
|
||||
head: str,
|
||||
commits: list[tuple[str, str]],
|
||||
normative: list[str],
|
||||
quiet: list[str],
|
||||
) -> str:
|
||||
"""The coord message. ASCII only - coord-send requires it."""
|
||||
total = len(normative) + len(quiet)
|
||||
lines = [
|
||||
"Canonical OKF has moved past our pin.",
|
||||
"",
|
||||
f" repository : {CANONICAL_REPO}",
|
||||
f" our pin : {pin[:7]}",
|
||||
f" upstream : {head[:7]}",
|
||||
f" paths : {len(normative)} of {total} changed paths are contract-bearing",
|
||||
"",
|
||||
"New commits (first-parent):",
|
||||
]
|
||||
lines += [f" {sha} {subject}" for sha, subject in commits] or [" (none)"]
|
||||
lines += ["", "Contract-bearing paths:"]
|
||||
lines += [f" {p}" for p in normative] or [" (none)"]
|
||||
if quiet:
|
||||
lines += ["", f"Not counted (quiet list): {len(quiet)} path(s)."]
|
||||
lines += [
|
||||
"",
|
||||
"This watch sees commits, not meaning. It cannot tell you whether a",
|
||||
"value that conformed last month still conforms - the 2026-08 round",
|
||||
"tightened v0.2 in place with no version bump, and reversed is_stale,",
|
||||
"and neither fact is legible in a commit list. Next step is the",
|
||||
"procedure, not a conclusion:",
|
||||
"",
|
||||
" docs/upstream-okf-upgrade-runbook.md",
|
||||
"",
|
||||
"Sent by tools/okf_watch.py in llm-ingestion-okf.",
|
||||
]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _coord_send_script() -> Path:
|
||||
"""Newest repo-mailbox scripts dir, resolved at runtime.
|
||||
|
||||
Hard-coding a plugin version is a rot hazard: the path moves every plugin
|
||||
release and a stale one fails as "command not found", which reads like the
|
||||
watch having nothing to say.
|
||||
"""
|
||||
base = Path.home() / ".claude" / "plugins" / "cache" / "ktg-plugin-marketplace" / "repo-mailbox"
|
||||
if not base.is_dir():
|
||||
raise WatchError(f"repo-mailbox plugin cache not found at {base}")
|
||||
|
||||
def key(p: Path) -> tuple[int, ...]:
|
||||
try:
|
||||
return tuple(int(x) for x in p.name.split("."))
|
||||
except ValueError:
|
||||
return (-1,)
|
||||
|
||||
candidates = [d for d in base.iterdir() if (d / "scripts" / "coord-send.sh").is_file()]
|
||||
if not candidates:
|
||||
raise WatchError(f"no coord-send.sh under any version in {base}")
|
||||
return max(candidates, key=key) / "scripts" / "coord-send.sh"
|
||||
|
||||
|
||||
def send(to: str, subject: str, body: str, *, fyi: bool, dry_run: bool) -> None:
|
||||
script = _coord_send_script()
|
||||
args = ["bash", str(script), "--to", to, "--from", SELF_REPO, "--subject", subject]
|
||||
if fyi:
|
||||
args.append("--fyi")
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(args)}\n{body}")
|
||||
return
|
||||
proc = subprocess.run(args, input=body, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise WatchError(
|
||||
f"coord-send to {to} failed with exit {proc.returncode}: "
|
||||
f"{proc.stderr.strip() or '(no stderr)'}"
|
||||
)
|
||||
|
||||
|
||||
def run(mirror: Path, state_path: Path, *, dry_run: bool, no_fetch: bool, pin: str = PIN) -> int:
|
||||
if not (mirror / ".git").is_dir():
|
||||
raise WatchError(f"mirror {mirror} is not a git repository - cannot measure anything")
|
||||
|
||||
if not no_fetch:
|
||||
git(mirror, "fetch", "--quiet", "origin", "main")
|
||||
|
||||
prove_can_find(mirror)
|
||||
|
||||
head = git(mirror, "rev-parse", "origin/main")
|
||||
commits = commits_between(mirror, pin, head)
|
||||
paths = changed_paths(mirror, pin, head)
|
||||
normative, quiet = classify(paths)
|
||||
|
||||
if not normative:
|
||||
print(
|
||||
f"okf-watch: no contract-bearing change. {CANONICAL_REPO} at {head[:7]}, "
|
||||
f"our pin {pin[:7]}, {len(commits)} commit(s) ahead, "
|
||||
f"{len(quiet)} of {len(paths)} changed paths on the quiet list. "
|
||||
"Known-positive passed, so this zero is a measurement."
|
||||
)
|
||||
return 0
|
||||
|
||||
state = State.load(state_path, pin=pin)
|
||||
fresh = decide(state, [sha for sha, _ in commits])
|
||||
if not fresh:
|
||||
print(
|
||||
f"okf-watch: {len(normative)} contract-bearing path(s) at {head[:7]}, "
|
||||
"already announced. Silent by design - report on change, not on state."
|
||||
)
|
||||
return 0
|
||||
|
||||
subject = f"Canonical OKF moved past our pin: {len(normative)} contract-bearing path(s)"
|
||||
body = render_body(pin=pin, head=head, commits=commits, normative=normative, quiet=quiet)
|
||||
send(SELF_REPO, subject, body, fyi=False, dry_run=dry_run)
|
||||
send(COORDINATOR_REPO, subject, body, fyi=True, dry_run=dry_run)
|
||||
|
||||
if not dry_run:
|
||||
state.record(fresh)
|
||||
state.save(state_path)
|
||||
print(f"okf-watch: reported {len(fresh)} new commit(s) at {head[:7]}.")
|
||||
return 0
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
ap.add_argument(
|
||||
"--mirror", type=Path, default=Path(os.environ.get("OKF_MIRROR", DEFAULT_MIRROR))
|
||||
)
|
||||
ap.add_argument(
|
||||
"--state", type=Path, default=Path(os.environ.get("OKF_WATCH_STATE", DEFAULT_STATE))
|
||||
)
|
||||
ap.add_argument(
|
||||
"--dry-run", action="store_true", help="print the coord messages instead of sending"
|
||||
)
|
||||
ap.add_argument(
|
||||
"--no-fetch", action="store_true", help="skip the network call (offline testing)"
|
||||
)
|
||||
ap.add_argument(
|
||||
"--pin",
|
||||
default=PIN,
|
||||
help=(
|
||||
"override the pin for ONE run. Exists so the hit path can be demonstrated on "
|
||||
"demand - a watch whose reporting branch has never run is a watch nobody has "
|
||||
"seen work. IMPLIES --dry-run: it does not move the pin, and it must not be "
|
||||
"able to page anyone. On 2026-08-23 this flag without --dry-run delivered two "
|
||||
"false alarms, so the pairing is enforced here rather than remembered."
|
||||
),
|
||||
)
|
||||
args = ap.parse_args(argv)
|
||||
if args.pin != PIN:
|
||||
args.dry_run = True
|
||||
return args
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
return run(
|
||||
args.mirror, args.state, dry_run=args.dry_run, no_fetch=args.no_fetch, pin=args.pin
|
||||
)
|
||||
except WatchError as exc:
|
||||
print(f"okf-watch: FAILED - {exc}", file=sys.stderr)
|
||||
print(
|
||||
"okf-watch: this is NOT 'no change'. Nothing was measured this run.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue