From d2055c05c8f149087f49f159e809cc7c06ff3297 Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 00:53:51 +0700 Subject: [PATCH 1/4] A far goal with kill conditions: behavior accountable to intent --- docs/DIRECTION.md | 139 ++++++++++++++++++ docs/decisions/015-intent-behavior-outcome.md | 84 +++++++++++ 2 files changed, 223 insertions(+) create mode 100644 docs/DIRECTION.md create mode 100644 docs/decisions/015-intent-behavior-outcome.md diff --git a/docs/DIRECTION.md b/docs/DIRECTION.md new file mode 100644 index 0000000..ee4823b --- /dev/null +++ b/docs/DIRECTION.md @@ -0,0 +1,139 @@ +# Direction + +`v0.1` answered a narrow question: does a model of a system's behavior stay true as the +code moves? It answers that now, mechanically, and the measurements are in +[`DEVELOPMENT-PLAN.md`](DEVELOPMENT-PLAN.md) and the issue tracker. + +It is the wrong question to stop on. A model that stays true to the code is still only +accountable to the code. + +## North star + +> **TraceOS makes software behavior accountable to intent.** + +"Accountable" is doing real work in that sentence and has to be defined before it can be +promised: + +> A claim about behavior is **accountable** when it can be traced back to the decision +> that asked for it, and forward to an outcome that is capable of contradicting it. + +Both directions are load-bearing. Trace back and nothing else, and the model is a +justification engine — every claim gets a story, none gets tested. Trace forward and +nothing else, and it is monitoring with extra steps. The pair is what neither side has. + +``` + Intent a frozen decision: why this behavior was asked for + ↕ + Behavior the Flow graph: what the system is claimed to do + ↕ + Outcome what actually happened, and whether it matches +``` + +Change can enter at any of the three. Reconciliation is how the three are brought back +into agreement — or how the disagreement is recorded when they cannot be. + +## Why this direction and not "more model" + +The sharpest finding of the review that produced #39–#45 was not a bug. It is that +**`claim` is the only place meaning lives in this system, and it is the field no +mechanism reads** (#45). Confidence is derived, correctly, from artifact staleness and +from an authored `supports` bit. So `confirmed` means *the addresses still resolve and +somebody said yes*. It has never meant *this sentence is true*. + +That is a floor, and a peer system built on the opposite principle — annotations living +inside the code, symbol-granular for free — turned out to share it exactly. Both detect +staleness. Neither detects wrongness. The case that proved it was a claim reading *"a +dropped issue never gets a ship mark"* which was false in production; what caught it was +an incident and a person reading a diff. + +**Outcome is the only layer where reality can contradict the model.** Intent and Behavior +are both statements, and two statements can only disagree about words. That is the whole +argument for this direction: it is the first shape of TraceOS in which wrongness is +addressable at all. + +## Horizons + +Each names what it claims, and what result would kill it. A horizon with no kill +condition is a roadmap item, not a direction. + +### H1 — Erosion, gated (largely built) + +What the model declared does not quietly stop resolving. Symbol-granular decay via +`git log -L` (§6.3.1), locator rot, `REFERENCE_NEVER_OBSERVED`, narrowing that reports +what it excluded (INV-025). + +*Claims:* the model does not rot silently. +*Kills it:* erosion fires often enough on a healthy repository to be tuned out. Unmeasured +— #10 is the number, and no horizon past this one is worth starting before it exists. + +### H2 — Outcome carries evidence + +Outcome today is `declared` and nothing else (INV-015): pure AUTHORED. It gains evidence +the way an Assertion has evidence, including a `runtime` kind whose observation is the +result of a query against a real system rather than a person's judgement. + +This is the first time `refutes` can be produced by a machine, and therefore the first +crack in #42. + +*Requires:* splitting INV-018. "Is the running system healthy" stays out of scope +forever; "did the declared outcome occur" comes in. These are different questions and +the invariant currently conflates them. +*Claims:* a wrong claim about behavior can be detected, not merely a stale one. +*Kills it:* take three incidents that actually happened in a real repository and ask +whether this mechanism would have caught any before they became incidents. **0 of 3 and +the direction is vocabulary, not capability.** + +### H3 — Intent as provenance, never as truth + +An Intent entity whose evidence is a **frozen record** (§6.2.1) — an ADR, a closed issue, +a commit message, a decision minute. Verification is provenance: *is this the record that +asked for it, and has that record been superseded* — never *is this really the intent*, +which nothing can answer. + +Supersession reuses the identity ledger (INV-014) unchanged: a business decision replaced +by another is exactly a `supersedes` with a reason. + +*Claims:* every load-bearing behavior can name the decision that asked for it. +*Kills it:* in practice, teams cannot cite a frozen record for most behavior, because +most decisions were never written down. Then Intent is a field that stays empty and the +layer is decorative. + +### H4 — The loop crosses layers + +A change entering at any layer produces an impact map that reaches the other two: +a decision changes → which flows → which outcomes must be re-verified. Reconciliation +compares **expected / implemented / observed** rather than model against code. + +*Claims:* the graph answers a question neither code annotations nor monitoring can. +*Kills it:* #5 — impact tiers have never been measured on a dense graph, and the reference +model suggests four tiers collapse into one. If they collapse, the cross-layer map is a +list of everything, which is the same as no map. + +## What this direction refuses + +Named here because a direction is defined as much by what it declines when the pressure +comes: + +- **Becoming a map of the code.** Already in §16. `init` reads no source file for this + reason, and progressive discovery is not a performance decision. +- **Gating coverage.** #43: a scope wide enough to assert coverage fires at 92%; a scope + narrow enough to pass asserts nothing. Erosion gates; coverage reports. +- **Claiming a sentence is true.** #45. `confirmed` will be described as address freshness + plus an observer's judgement everywhere it appears, including the `explore` legend. +- **Six parallel change domains.** Requirement, business, architecture, code, config and + operational changes are all real, but four of them have no diff, no locator and no git. + They enter as triggers citing a record, never as surfaces to scan. The gate already + fails 92% on the one surface that *does* have artifacts. +- **A new entity because the vocabulary would be tidier.** The admission rule stands: + nothing enters unless something fails without it. + +## The number that governs all of it + +**#10 — nobody has measured what it costs to keep a model current.** A peer's trial gave +a fragment: four `validate` rounds, five assertions, two files of fifty-six. Nobody has +measured the weekly cost of keeping that from rotting. + +If that number is small, every horizon above is worth building. If it is large, none of +them matter, because the model will be abandoned before it is ever contradicted by an +outcome. It is the cheapest measurement on this page and the only one that decides +whether the others get made. diff --git a/docs/decisions/015-intent-behavior-outcome.md b/docs/decisions/015-intent-behavior-outcome.md new file mode 100644 index 0000000..058410f --- /dev/null +++ b/docs/decisions/015-intent-behavior-outcome.md @@ -0,0 +1,84 @@ +# ADR-015 — Intent ↔ Behavior ↔ Outcome + +**Status:** PROPOSED +**Supersedes:** nothing yet. `v0.1`'s framing ("preserve system understanding across +change") stays in force until the kill condition below is run. + +## Context + +`v0.1` models behavior and verifies that the model has not rotted against the code. The +review that produced #39–#45 established that this is a smaller claim than the project +had been making, for a reason that is structural rather than fixable: + +`claim` is the only field carrying meaning, and no mechanism reads it (#45). Confidence +derives from artifact staleness plus an authored `supports` bit, so `confirmed` means +*the addresses resolve and somebody said yes*. A peer system built on the opposite +principle — annotations inside the code, symbol-granular without any of this machinery — +shares the floor exactly. Both detect staleness; neither detects wrongness. + +Behavior verified only against code is accountable only to code. + +## Decision + +Re-frame the system around three layers rather than one: + +``` +Intent ↕ Behavior ↕ Outcome +``` + +- **Intent** — why a behavior was asked for. Evidence is a **frozen record** (§6.2.1): + an ADR, a closed issue, a commit message, a decision minute. Verified by **provenance, + never by truth** — *is this the record that asked for it, has it been superseded* — + because *is this really the intent* is not answerable by any mechanism. Supersession + reuses INV-014 unchanged. +- **Behavior** — the existing Flow graph. Unchanged. +- **Outcome** — gains evidence the way an Assertion has it, including a `runtime` kind + whose observation is a query result rather than a judgement. This is the only layer at + which reality can contradict the model, and therefore the only route to detecting a + wrong claim rather than a stale one. + +Change may enter at any layer. Reconciliation compares **expected / implemented / +observed**. + +### What the framing does not change + +The skill set stays at three. `traceos init` stays a bootstrap operation, the validator +stays an engine capability, implementation stays the agent's own work. Four routes into +the model stay four: Declared / Observed / Inferred / Unknown — `Implemented` is an +evidence *kind*, not a route, and `Contradicted` is DERIVED by the validator and must +never be an authored label (INV-001). + +## Consequences + +**INV-018 must be split.** It currently conflates two questions: + +| Question | Status | +|---|---| +| is the running system healthy — uptime, latency, CPU, incidents | out of scope, permanently | +| did the **declared outcome** occur | in scope, and the point of H2 | + +Without that split, Outcome cannot carry runtime evidence and the direction is +vocabulary only. With it, the boundary needs restating precisely enough that nobody +reads it as permission to build monitoring. + +**INV-015 changes.** Outcomes are declared *and* evidenced, rather than declared only. + +**Intent is the weakest layer and must be labelled as such.** It has no artifact to hash, +no symbol for `git log -L`, no locator that can rot in the way code does. Promoting an +unverifiable layer to the headline is exactly the failure #45 names, and provenance is +the only honest verification available for it. + +## Kill condition + +This ADR is PROPOSED, not ACCEPTED, and one measurement decides it: + +> Take three incidents that actually happened in a real repository. For each, ask whether +> a model carrying Intent and an evidenced Outcome would have caught it before it became +> an incident. + +**0 of 3 and this is a vocabulary change**, which is not worth a specification rewrite. +1 of 3 or better and it is the first mechanism in this project capable of contradicting a +claim, which is worth every other thing on the roadmap. + +Prerequisite: #10, the cost of keeping a model current. A direction whose models are +abandoned before an outcome ever contradicts them is decided by attrition, not by this. From 0a1c8267234f63141a6cef21ef0bb6ba8a3d0bc9 Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 01:00:26 +0700 Subject: [PATCH 2/4] The ambition goes at the top, with what is built beside it --- README.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4db597..65f82eb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,30 @@ # TraceOS +> ## Software behavior, accountable to intent. + +A claim about what a system does is **accountable** when it can be traced back to the +decision that asked for it, and forward to an outcome capable of contradicting it. +Nothing today does both: code annotations trace backward, monitoring traces forward, and +neither can tell you that a behavior quietly stopped matching what it was for. + +**Where that stands, honestly:** + +| Layer | | Status | +|---|---|---| +| **Behavior** | the Flow graph, evidence, impact, erosion detection | **works today** — v0.1, modelled against itself in CI | +| **Outcome** | an outcome carrying runtime evidence, able to refute a claim | **not built** — the layer that would detect a *wrong* claim rather than a stale one | +| **Intent** | a behavior citing the frozen decision that asked for it | **not built** — verified by provenance, never by truth | + +The direction, the four horizons and the measurement that would kill each of them are in +[`docs/DIRECTION.md`](docs/DIRECTION.md). Nothing on that page is claimed as shipped. + +--- + > **Trace before change. Reconcile after change.** -A semantic layer that lets a coding agent see a system as behavior rather than as -files — so it can work out what a change might affect *before* making it, and check -whether the model is still true *after*. +That is the part that runs. A semantic layer that lets a coding agent see a system as +behavior rather than as files — so it can work out what a change might affect *before* +making it, and check whether the model is still true *after*. A coding agent goes `search → modify → test`. It can change `PaymentService` without knowing that payment participates in purchase, refund, subscription, notification and From f0b47095657860d26acf593560cc6d744c5c10e7 Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 01:23:16 +0700 Subject: [PATCH 3/4] An observation must outlive the commit it was made on --- docs/semantic-specification.md | 27 ++++++++++++++ tests/run_tests.py | 24 +++++++++++++ traceos/engine.py | 65 +++++++++++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/docs/semantic-specification.md b/docs/semantic-specification.md index 0a38fea..ba3aa16 100644 --- a/docs/semantic-specification.md +++ b/docs/semantic-specification.md @@ -1001,6 +1001,33 @@ the per-language funcname heuristics git already ships, and falls back to the whole-file hash when git cannot resolve the anchor — loud, never silent, because a bound that cannot be established is not a bound that found nothing (INV-026). +#### 6.3.2 An observation must outlive the commit it was made on + +Narrowing first asked `git log -L :symbol:path ..HEAD`, which needs the +branch's commits to still exist. **A squash merge deletes them.** Measured here: one +merge left 7 of this repository's own 25 observed refs unreachable, and the next merge +orphaned more. It reproduced only in CI, because a developer's clone keeps them alive in +the reflog and in stale branches — so the failure was invisible exactly where the model +was authored and total where it was checked. + +An observation therefore records `observed_symbol`: a hash of the cited symbol's text at +the moment it was read. The line range is derived from git's own hunk header at check +time and never stored, so INV-022 is untouched; what is stored is a hash of what the +observer actually read, which is what an observation is for. + +Order of answers, strongest first: + +| | Needs history | Answers | +|---|---|---| +| whole-file hash unchanged | no | not changed | +| `observed_symbol` unchanged | no | symbol not changed | +| `git log -L ..HEAD` | yes | whether the symbol moved in that range | +| none of the above | — | could not establish → whole-file, loud, and it says why | + +`OBSERVED_REF_UNREACHABLE` reports observations made before this existed, and the +discharge is to observe again — which is a real read at a commit that still exists, not +a stamp. + **The author now owes what file granularity used to cover by accident.** A claim must cite every symbol whose change could falsify it. Before narrowing, a claim spanning two functions but citing one still decayed when its sibling moved; after narrowing it does diff --git a/tests/run_tests.py b/tests/run_tests.py index 18549e9..6ae3356 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1296,6 +1296,30 @@ def run(*a): [n["assertion"] for n in decay["narrowed"]] == ["assert.example.works"], str(decay["narrowed"]), ) + + # Ref-independent narrowing: the observation must survive its own commit + # being rewritten, because a squash merge does exactly that. Measured on this + # repository, one squash orphaned 7 of 25 observed refs. + sym_before = git.symbol_blob("mod.py", "alpha") + run("checkout", "-q", "--orphan", "rewritten") + run("add", "-A") + run("commit", "-qm", "history rewritten, every old commit orphaned") + check( + "TOOL symbol decay", + "the old ref is now unreachable, as after a squash merge", + git.reachable(head) is False, + ) + check( + "TOOL symbol decay", + "a content hash still narrows when the ref is gone", + git.changed_since(head, "mod.py", blob, None, "alpha", sym_before) is False, + ) + check( + "TOOL symbol decay", + "and it still reports a real change to the cited symbol", + git.changed_since(head, "mod.py", blob, None, "alpha", "0" * 40) is True, + ) + finally: shutil.rmtree(tmp, ignore_errors=True) diff --git a/traceos/engine.py b/traceos/engine.py index 4bf4c97..3f03409 100644 --- a/traceos/engine.py +++ b/traceos/engine.py @@ -385,6 +385,54 @@ def reachable(self, ref: str) -> bool: return False return self._run("merge-base", "--is-ancestor", ref, "HEAD") is not None + def symbol_blob(self, path: str, anchor: str) -> str | None: + """Content hash of the named symbol as it stands now. No history required. + + `git log -L :symbol:path -1` reports the most recent commit that touched the + symbol, and its hunk header carries the symbol's line range *at HEAD*. The + range is derived here and never stored, so INV-022 is untouched - what is + stored is a hash of the text, which is what an observation is entitled to + record about an artifact it just read. + + This is what makes an observation survive a squash merge. Asking + `..HEAD` needs the branch's commits to still exist, and a squash + replaces them: measured here, one merge orphaned 7 of 25 observed refs, and + the next merge orphaned more. A content hash has no such dependency. + """ + prefix = [] + attributes = self._attributes_file() + if attributes: + prefix = ["-c", f"core.attributesfile={attributes}"] + out = self._run(*prefix, "log", "-L", f":{anchor}:{path}", "-1", "--format=%x00") + if not out: + return None + lo = hi = None + for line in out.splitlines(): + if line.startswith("@@"): + try: + plus = line.split("+", 1)[1].split(" ", 1)[0] + except IndexError: + continue + start, _, count = plus.partition(",") + try: + lo = int(start) + hi = lo + (int(count) if count else 1) - 1 + except ValueError: + return None + if lo is None: + return None + target = (self.repo / _norm_path(path)) if self.repo else None + if target is None or not target.is_file(): + return None + try: + lines = target.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError: + return None + body = "\n".join(line.rstrip() for line in lines[lo - 1 : hi]) + if not body.strip(): + return None + return hashlib.sha1(body.encode("utf-8")).hexdigest() + def symbol_changed_since(self, ref: str, path: str, anchor: str) -> bool | None: """Did the named symbol change, rather than the file that contains it? @@ -439,6 +487,7 @@ def changed_since( blob: str | None = None, norm_blob: str | None = None, anchor: str | None = None, + symbol_blob: str | None = None, ) -> bool | None: """spec 6.3 artifact_changed_since. None means it could not be determined. @@ -459,6 +508,10 @@ def changed_since( if current_norm is not None and current_norm == norm_blob: return False if anchor: + if symbol_blob: + current_symbol = self.symbol_blob(path, anchor) + if current_symbol is not None: + return current_symbol != symbol_blob narrowed = self.symbol_changed_since(ref, path, anchor) if narrowed is not None: return narrowed @@ -796,6 +849,7 @@ def stale_references( observation.get("observed_blob"), observation.get("observed_norm"), anchor or None, + observation.get("observed_symbol"), ) if changed is True: stale.append(reference) @@ -932,6 +986,7 @@ def computed_confidence( observation.get("observed_blob"), observation.get("observed_norm"), anchor or None, + observation.get("observed_symbol"), ) if changed is True: return "uncertain" @@ -1578,17 +1633,25 @@ def observe( f"to history, which a squash or rebase will invalidate (spec 6.3)" ) + path_part, _, anchor = match.partition("#") entry = { "assertion": assertion_id, "reference": match, "observed_at": at_time.isoformat().replace("+00:00", "Z"), "observed_ref": ref, "observed_blob": blob, - "observed_norm": git.normalised_blob(match.split("#", 1)[0]), + "observed_norm": git.normalised_blob(path_part), "supports": supports, "observer": observer, "kind": kind or declared[match]["kind"], } + # Ref-independent narrowing. Without it, decay can only ask which symbol moved by + # walking `observed_ref..HEAD`, and a squash merge deletes that ref: measured here, + # one merge orphaned 7 of 25 observations and the next orphaned more. + if anchor: + symbol = git.symbol_blob(path_part, anchor) + if symbol: + entry["observed_symbol"] = symbol out_dir = model_dir / "observations" out_dir.mkdir(exist_ok=True) with (out_dir / f"{at_time:%Y-%m}.jsonl").open("a", encoding="utf-8") as fh: From 945f6b1a3c127c2c7dc204d5effda23a06a54200 Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 01:23:36 +0700 Subject: [PATCH 4/4] Discharge with content-hashed observations --- examples/traceos-itself/observations/2026-09.jsonl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/traceos-itself/observations/2026-09.jsonl b/examples/traceos-itself/observations/2026-09.jsonl index b106c04..18d5184 100644 --- a/examples/traceos-itself/observations/2026-09.jsonl +++ b/examples/traceos-itself/observations/2026-09.jsonl @@ -314,3 +314,6 @@ {"assertion": "assert.trace.unknown-is-gated", "reference": "traceos/cli.py", "observed_at": "2026-09-11T18:16:19.318636Z", "observed_ref": "9357adb7547eb637839404088bab8937ed572ace", "observed_blob": "3cc0665262e431fa4f8edb635bbbfc4ee2b43387", "observed_norm": "23963bf5a67d5392ce14a4c65a239ec001dc427e", "supports": "supports", "observer": "agent", "kind": "implementation"} {"assertion": "assert.reconcile.unverifiable-is-not-verified", "reference": "traceos/engine.py#Git", "observed_at": "2026-09-11T18:19:54.627968Z", "observed_ref": "4e9014f677a75821a3b8db89bf1f1aa31c5e44a4", "observed_blob": "4bf4c97834b4748b848846a1db55eb5cdd6dd4d5", "observed_norm": "c115deb68fa520612eb03056e83794c674430aa5", "supports": "supports", "observer": "agent", "kind": "implementation"} {"assertion": "assert.trace.unknown-is-gated", "reference": "traceos/cli.py", "observed_at": "2026-09-11T18:19:54.829412Z", "observed_ref": "4e9014f677a75821a3b8db89bf1f1aa31c5e44a4", "observed_blob": "7a2c4566f24d0a8bb63250a1b6c96a2802255d33", "observed_norm": "ecde6d31c34960f1b3d99c85f7898d00e3d9a030", "supports": "supports", "observer": "agent", "kind": "implementation"} +{"assertion": "assert.reconcile.confidence-decays", "reference": "traceos/engine.py#computed_confidence", "observed_at": "2026-09-11T18:23:29.861510Z", "observed_ref": "f0b47095657860d26acf593560cc6d744c5c10e7", "observed_blob": "3f034098d829a936f5477e18640514a13487bcbc", "observed_norm": "601e1938ea6f5e7627742ce917a5e330a22689db", "supports": "supports", "observer": "agent", "kind": "implementation", "observed_symbol": "3932bff17583b96baa7e6a8bd272b6184dad9e7f"} +{"assertion": "assert.reconcile.unverifiable-is-not-verified", "reference": "traceos/engine.py#Git", "observed_at": "2026-09-11T18:23:29.993936Z", "observed_ref": "f0b47095657860d26acf593560cc6d744c5c10e7", "observed_blob": "3f034098d829a936f5477e18640514a13487bcbc", "observed_norm": "601e1938ea6f5e7627742ce917a5e330a22689db", "supports": "supports", "observer": "agent", "kind": "implementation", "observed_symbol": "d772d948cfb6e3b4d3e68b55791c37251c7915fb"} +{"assertion": "assert.understand.observes-before-asserting", "reference": "traceos/engine.py#observe", "observed_at": "2026-09-11T18:23:30.159444Z", "observed_ref": "f0b47095657860d26acf593560cc6d744c5c10e7", "observed_blob": "3f034098d829a936f5477e18640514a13487bcbc", "observed_norm": "601e1938ea6f5e7627742ce917a5e330a22689db", "supports": "supports", "observer": "agent", "kind": "implementation", "observed_symbol": "7c7b8a3be2ef800da0814a052933509c8e201e61"}