From 69c46b7f48c45a39741692c1c9b9afe5695a3573 Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 01:26:08 +0700 Subject: [PATCH 1/4] Intent and Outcome: the two layers that make behavior accountable --- docs/semantic-specification.md | 103 ++- examples/ecommerce/model/flows/purchase.md | 4 +- examples/ecommerce/model/intents.md | 27 + .../traceos-itself/model/flows/reconcile.md | 5 +- examples/traceos-itself/model/flows/render.md | 4 +- examples/traceos-itself/model/flows/trace.md | 4 +- .../traceos-itself/model/flows/understand.md | 4 +- examples/traceos-itself/model/intents.md | 45 ++ skills/reconciling-reality/SKILL.md | 22 + skills/understanding-system/SKILL.md | 20 + tests/run_tests.py | 181 +++++ traceos/check_locators.py | 51 +- traceos/engine.py | 187 +++++- traceos/schema/traceos.schema.json | 631 +++++++++++++++--- 14 files changed, 1161 insertions(+), 127 deletions(-) create mode 100644 examples/ecommerce/model/intents.md create mode 100644 examples/traceos-itself/model/intents.md diff --git a/docs/semantic-specification.md b/docs/semantic-specification.md index ba3aa16..d8de367 100644 --- a/docs/semantic-specification.md +++ b/docs/semantic-specification.md @@ -187,6 +187,46 @@ The validator MUST warn and MUST NOT silently promote a terminal node to an Outc --- +### 4.6 Intent — INV-028 + +An **Intent** says why a behavior was asked for. A Flow declares the Intents it serves +with `realizes:`. + +```yaml +intents: + - id: intent.confidence-must-be-able-to-fall + statement: >- + Confidence has to drop without anyone editing the model, or the model records + only that somebody was once confident. + record: { kind: decision, locator: "docs/decisions/003-confidence-is-derived.md#Decision" } +``` + +**INV-028 INTENT-IS-PROVENANCE.** An Intent is verified by provenance, never by truth. + +Nothing can establish that a sentence is genuinely why something was built. What *can* +be established is whether the Intent names the **frozen record** that asked for it +(§6.2.1), whether that record still resolves, and whether it has been superseded. Those +are the only questions this layer answers, and the spec says so rather than letting a +reader infer more from a green check. + +A model that tried to verify Intent by reading the code would be circular: the code is +the thing the Intent is supposed to explain. + +**TraceOS defines no format for the record.** Architecture decision records, closed +issues and commit trailers already exist, with established conventions and tooling to +author and supersede them. What did not exist is anything that notices when an accepted +decision has stopped matching the behavior — and a citation that stops resolving is the +cheapest available signal of exactly that. + +Supersession reuses the identity ledger unchanged (INV-014): a decision replaced by a +later decision is a `supersedes` with a reason, which is what an ADR's own +"superseded by" line already means. + +`FLOW_WITHOUT_INTENT` is reported at `info` and never gates. Most behavior in a real +system predates anyone writing the decision down, and a gate that demanded otherwise +would be satisfied by invented intents — which is worse than none, for the same reason a +rubber-stamped observation is worse than a missing one (§13.0.1). + ## 5. Relationships ### 5.1 The matrix — INV-006 @@ -509,9 +549,14 @@ The lifecycle was mislabelled; the resolver is not missing anything. ### INV-018 NO-HEALTH -Runtime health is out of scope for v0.1. TraceOS asks *what is the system's -behavior*; health asks *is the running system healthy*. Uptime, latency, CPU, memory, -availability and incident state belong to monitoring, which §Non-goals excludes. +Runtime health is out of scope. TraceOS asks *what is the system's behavior*; health +asks *is the running system healthy*. Uptime, latency, CPU, memory, availability and +incident state belong to monitoring, which §Non-goals excludes. + +This invariant once carried a second question it should not have: *did the outcome this +model declares actually occur*. That is verification, not monitoring, and §6.4.1 draws +the line — the subject decides. Behavior evidenced by a runtime artifact is evidence; +the machine's condition is not behavior. Two judgement scales remain, both DERIVED: @@ -1052,6 +1097,56 @@ a symbol whose file had moved 21 times and whose symbol had moved once. The rema were re-observation requests for code the change never touched, which is the material a rubber stamp is made of (§13.0.1). +### 6.4 An Outcome declares how it is checked — INV-027 + +**INV-027 OUTCOME-DECLARES-ITS-CHECK.** An Outcome names the Assertion that checks +whether it occurred, or it is reported as declaring none. + +```yaml +outcomes: + - { id: purchase.completed, + states: [{ subject: order, value: confirmed }], + verified_by: assert.purchase.confirms-order } +``` + +**This is the only place in the model where reality can contradict it.** Intent and +Behavior are both statements, and two statements can disagree only about words. An +Outcome can be measured. So an observation that `refutes` an Outcome's check is the +first machine-produced evidence that a claim is **wrong** rather than merely **stale** — +which is the distinction every other mechanism in this specification is unable to make. + +| Finding | Level | Means | +|---|---|---| +| `OUTCOME_REFUTED` | error | the model says this outcome occurs; its own check says it did not | +| `OUTCOME_CHECK_UNKNOWN` | error | `verified_by` names an Assertion this model does not have | +| `OUTCOME_NEVER_CHECKED` | warn | a check is named and has never been observed | +| `OUTCOME_NOT_VERIFIABLE` | warn | no check is declared | + +An Outcome with no check is reported, never failed. What is forbidden is silence: an +unverifiable Outcome and a verified one must not read the same (INV-025). + +#### 6.4.1 What this does and does not admit about runtime + +INV-018 keeps runtime **health** out of scope permanently — uptime, latency, CPU, +memory, availability and incident state belong to monitoring. That has not changed. + +What INV-027 admits is a different question, which INV-018 previously conflated with it: + +| Question | Status | +|---|---| +| *is the running system healthy* | out of scope, permanently (INV-018) | +| *did the outcome this model declares actually occur* | in scope — it is verification, not monitoring | + +The boundary is the subject. An assertion about **the system's behavior**, evidenced by +something a runtime produced, is evidence like any other, and `kind: runtime` has been a +legal evidence kind since v0.1. An assertion about **the system's condition** is not +behavior and does not enter. + +TraceOS defines no query language for this. Reading an indicator from a data source is a +solved problem with an open specification of its own, and an Outcome's runtime evidence +cites such a definition the way an Intent cites an ADR — by locator, never by +reimplementation. + --- ## Appendix A — Invariant index @@ -1084,3 +1179,5 @@ rubber stamp is made of (§13.0.1). | INV-024 | Decay must be discharged in the change that touches it | 13.0.1 | | INV-025 | A boundary reports what it excluded; it never reports silence | 13.0.2 | | INV-026 | A gate has three outcomes; an empty scan is not a pass | 13.0.3 | +| INV-027 | An Outcome declares the Assertion that checks it | 6.4 | +| INV-028 | Intent is verified by provenance, never by truth | 4.6 | diff --git a/examples/ecommerce/model/flows/purchase.md b/examples/ecommerce/model/flows/purchase.md index a9b7298..8133efe 100644 --- a/examples/ecommerce/model/flows/purchase.md +++ b/examples/ecommerce/model/flows/purchase.md @@ -18,8 +18,10 @@ relationships: - { type: emits, source: node.purchase.confirm, target: event.order.created } - { type: transitions_to, source: node.purchase.confirm, target: state.order.confirmed } - { type: transitions_to, source: node.purchase.reject, target: state.order.rejected } +realizes: + - intent.a-confirmed-order-is-a-promise outcomes: - - { id: purchase.confirmed, states: [{ subject: order, value: confirmed }] } + - { id: purchase.confirmed, states: [{ subject: order, value: confirmed }], verified_by: assert.purchase.confirms-order } - { id: purchase.rejected, states: [{ subject: order, value: rejected }] } assertions: - id: assert.purchase.confirms-order diff --git a/examples/ecommerce/model/intents.md b/examples/ecommerce/model/intents.md new file mode 100644 index 0000000..2d16259 --- /dev/null +++ b/examples/ecommerce/model/intents.md @@ -0,0 +1,27 @@ +--- +id: system.shop.intents +type: intents +intents: + - id: intent.a-confirmed-order-is-a-promise + statement: >- + Once an order reads CONFIRMED the customer has been told it will be fulfilled, + so nothing downstream may quietly leave it in that state without paying. + record: { kind: decision, locator: "docs/decisions/0004-order-confirmation.md#Decision" } + lifecycle: current + - id: intent.a-refund-never-outruns-the-charge + statement: >- + A refund is issued only against a settled charge, because a refund on an + unsettled one leaves money owed in both directions. + record: { kind: decision, locator: "docs/decisions/0009-refund-ordering.md#Decision" } + lifecycle: current +--- + +## What belongs here + +Why each Flow exists, citing the frozen record that asked for it (spec 6.2.1). + +Verified by **provenance, never by truth** (INV-028). The locators above point at +decision records this example repository does not ship — `check_locators` reports them +as unresolved, which is the correct and intended reading: *the citation is declared and +cannot be verified here*. That is the state most real models start in, and it is +reported rather than hidden. diff --git a/examples/traceos-itself/model/flows/reconcile.md b/examples/traceos-itself/model/flows/reconcile.md index 3015ec5..77e8a22 100644 --- a/examples/traceos-itself/model/flows/reconcile.md +++ b/examples/traceos-itself/model/flows/reconcile.md @@ -24,8 +24,11 @@ relationships: - { type: interacts_with, source: node.reconcile.integrity, target: external.reader } - { type: transitions_to, source: node.reconcile.integrity, target: state.integrity.reported } - { type: depends_on, source: flow.reconcile, target: external.repository } +realizes: + - intent.confidence-must-be-able-to-fall + - intent.derived-is-never-authored outcomes: - - { id: reconcile.reported, states: [{ subject: integrity, value: reported }] } + - { id: reconcile.reported, states: [{ subject: integrity, value: reported }], verified_by: assert.reconcile.integrity-is-computed } assertions: - id: assert.reconcile.confidence-decays claim: "confidence falls when an artifact moves under an observation, with no model file edited" diff --git a/examples/traceos-itself/model/flows/render.md b/examples/traceos-itself/model/flows/render.md index 4c4bf26..6bf3c5a 100644 --- a/examples/traceos-itself/model/flows/render.md +++ b/examples/traceos-itself/model/flows/render.md @@ -15,8 +15,10 @@ relationships: - { type: depends_on, source: node.render.compute, target: state.integrity.reported } - { type: interacts_with, source: node.render.publish, target: external.reader } - { type: transitions_to, source: node.render.publish, target: state.view.published } +realizes: + - intent.a-view-is-not-a-source outcomes: - - { id: render.published, states: [{ subject: view, value: published }] } + - { id: render.published, states: [{ subject: view, value: published }], verified_by: assert.render.view-not-truth } assertions: - id: assert.render.no-second-implementation claim: "every figure on the page is computed by the engine and embedded as a result, so no traversal runs in the browser" diff --git a/examples/traceos-itself/model/flows/trace.md b/examples/traceos-itself/model/flows/trace.md index 5a68764..2f2d9ac 100644 --- a/examples/traceos-itself/model/flows/trace.md +++ b/examples/traceos-itself/model/flows/trace.md @@ -24,8 +24,10 @@ relationships: - { type: next, source: node.trace.tier, target: node.trace.gate } - { type: emits, source: node.trace.gate, target: event.change.traced } - { type: transitions_to, source: node.trace.gate, target: state.impact.tiered } +realizes: + - intent.derived-is-never-authored outcomes: - - { id: trace.tiered, states: [{ subject: impact, value: tiered }] } + - { id: trace.tiered, states: [{ subject: impact, value: tiered }], verified_by: assert.trace.four-tiers } assertions: - id: assert.trace.four-tiers claim: "impact is reported in four tiers and never as a list of changed files" diff --git a/examples/traceos-itself/model/flows/understand.md b/examples/traceos-itself/model/flows/understand.md index d1fb9f0..242b01a 100644 --- a/examples/traceos-itself/model/flows/understand.md +++ b/examples/traceos-itself/model/flows/understand.md @@ -18,8 +18,10 @@ relationships: - { type: next, source: node.understand.observe, target: node.understand.assert } - { type: interacts_with, source: node.understand.observe, target: external.repository } - { type: transitions_to, source: node.understand.assert, target: state.model.established } +realizes: + - intent.reference-is-an-address outcomes: - - { id: understand.modeled, states: [{ subject: model, value: established }] } + - { id: understand.modeled, states: [{ subject: model, value: established }], verified_by: assert.understand.observes-before-asserting } assertions: - id: assert.understand.observes-before-asserting claim: "an evidence reference is authored, then checked, and only the check is recorded" diff --git a/examples/traceos-itself/model/intents.md b/examples/traceos-itself/model/intents.md new file mode 100644 index 0000000..cad01e9 --- /dev/null +++ b/examples/traceos-itself/model/intents.md @@ -0,0 +1,45 @@ +--- +id: system.traceos.intents +type: intents +intents: + - id: intent.derived-is-never-authored + statement: >- + A value the system can compute must never be stored as something a person + wrote, because the stored copy goes stale and then lies about exactly the + property it exists to express. + record: { kind: decision, locator: "docs/decisions/001-three-tier-entities.md#Decision" } + lifecycle: current + - id: intent.confidence-must-be-able-to-fall + statement: >- + Confidence has to drop without anyone editing the model, or the model records + only that somebody was once confident. + record: { kind: decision, locator: "docs/decisions/003-confidence-is-derived.md#Decision" } + lifecycle: current + - id: intent.reference-is-an-address + statement: >- + A reference names where to look, never what was found; verification is a + separate act that must be recorded each time it happens. + record: { kind: decision, locator: "docs/decisions/002-evidence-reference-vs-observation.md#Decision" } + lifecycle: current + - id: intent.a-view-is-not-a-source + statement: >- + Anything rendered is regenerated from the model and never edited in place, so + no reader has to work out which copy is the real one. + record: { kind: decision, locator: "docs/decisions/008-artifact-is-derived.md#Decision" } + lifecycle: current +--- + +## What belongs here + +An Intent says **why** a behavior was asked for, and cites the **frozen record** +(§6.2.1) that asked — here, this repository's own ADRs. + +Intent is verified by provenance, never by truth (INV-028). The checkable questions +are *does the record still resolve* and *has it been superseded*. Whether the sentence +is genuinely the reason is not answerable by any mechanism, and a model that tried to +settle it by reading the code would be circular: the code is the thing the intent is +supposed to explain. + +TraceOS adds no format for these records. ADRs, closed issues and commit trailers +already exist, with tooling to author and supersede them. What did not exist is +anything that notices when an accepted decision has stopped matching the behavior. diff --git a/skills/reconciling-reality/SKILL.md b/skills/reconciling-reality/SKILL.md index 2cfdf10..9a0dd91 100644 --- a/skills/reconciling-reality/SKILL.md +++ b/skills/reconciling-reality/SKILL.md @@ -112,6 +112,28 @@ Outcomes are explicit declarations bound to States (INV-015). If the new reality produces an outcome the Flow does not declare, update the declaration and the graph. Do not infer an Outcome from a terminal Node. +### 7a. Reconcile Outcomes against what actually happened + +An Outcome names the Assertion that checks whether it occurred (INV-027), or is reported +as declaring none. This is the only comparison in the whole loop where **reality can +contradict the model** — Intent and Behavior are both statements, and two statements can +disagree only about words. + +So a `refutes` here is not the same kind of event as a `refutes` anywhere else: + +| Elsewhere | On an Outcome's check | +|---|---| +| the evidence moved, the claim may be stale | the claim is **wrong**, and something acted on it | + +`OUTCOME_REFUTED` is an error and takes integrity to INVALID. Do not reconcile it by +softening the claim. Find out what the system actually does, correct the Assertion, and +leave the refuting observation in the log — it is the record of a model that was wrong +in production, which is the most expensive kind of evidence there is and the only kind +nothing else in this system can produce. + +An Outcome with no declared check is reported, never failed. Declare one when the +behavior is load-bearing enough that being wrong about it would cost something. + ### 8. Validate and recompute Run the validator: identity, matrix conformance, references, assertion consistency, diff --git a/skills/understanding-system/SKILL.md b/skills/understanding-system/SKILL.md index 31cfc9d..7d4ed02 100644 --- a/skills/understanding-system/SKILL.md +++ b/skills/understanding-system/SKILL.md @@ -85,6 +85,26 @@ qualifies only when something depends on it (INV-003). Events are semantic occurrences, emitted and listened to (INV-005). +### 4a. Say why the Flow exists, when a record says so + +A Flow can declare the **Intents** it serves. An Intent is not a description of the +behavior — it is the reason the behavior was asked for, and it cites the **frozen +record** that asked: an ADR, a closed issue, a decision minute (§6.2.1). + +```yaml +realizes: + - intent.confidence-must-be-able-to-fall +``` + +**Intent is verified by provenance, never by truth (INV-028).** The checkable questions +are whether the record still resolves and whether it has been superseded. Whether the +sentence is really the reason is not answerable, and trying to settle it by reading the +code is circular — the code is what the Intent explains. + +Do not invent one. A Flow with no Intent is reported at `info` and never gated, because +most behavior predates anyone writing the decision down. An invented Intent is worse +than none, exactly as a rubber-stamped observation is worse than a missing one. + ### 4b. Know which way each statement entered the model Every statement in the model arrived by one of four routes, and they are not diff --git a/tests/run_tests.py b/tests/run_tests.py index 6ae3356..048b89e 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -2230,6 +2230,186 @@ def confidence(entries, evidence=None): case_12_human_decision, case_13_scheduled, ] + + +def inv_outcome_can_be_refuted(): + """INV-027 / INV-028. The only place reality can contradict the model. + + Intent and Behavior are both statements; two statements disagree only about words. + An Outcome can be measured, so a `refutes` against its check is the first + machine-produced evidence that a claim is *wrong* rather than merely *stale* - the + distinction nothing else in this system can make. + """ + base = { + "id": "flow.pay", + "type": "flow", + "domain": "pay", + "lifecycle": "current", + "coverage_declared": "stub", + "trigger": {"kind": "user_action", "actor": "external.buyer"}, + "realizes": ["intent.money-moves-once"], + "nodes": [{"id": "node.pay.charge", "type": "action", "name": "Charge"}], + "relationships": [ + { + "type": "transitions_to", + "source": "node.pay.charge", + "target": "state.order.paid", + } + ], + "outcomes": [ + { + "id": "pay.settled", + "states": [{"subject": "order", "value": "paid"}], + "verified_by": "assert.pay.settles", + } + ], + "assertions": [ + { + "id": "assert.pay.settles", + "claim": "a charge that returns ok leaves the order paid", + "subject": "pay.settlement", + "lifecycle": "current", + "about": ["node.pay.charge"], + "evidence": [ + {"kind": "runtime", "locator": "ops/slo/settlement.yaml#paid_rate"} + ], + } + ], + } + intents = { + "id": "system.x.intents", + "type": "intents", + "intents": [ + { + "id": "intent.money-moves-once", + "statement": "a buyer is charged once per order", + "record": { + "kind": "decision", + "locator": "docs/decisions/007-context-as-selector.md#Decision", + }, + } + ], + } + externals = { + "id": "system.x.externals", + "type": "externals", + "externals": [{"id": "external.buyer", "name": "Buyer"}], + } + + clean = scratch_model({"pay": base, "intents": intents, "ext": externals}) + got = codes(T.validate(clean, None, {}, NOW)) + check( + "INV-027 outcome refuted", + "a declared check with no observation reports, it does not fail", + "OUTCOME_NEVER_CHECKED" in got and "OUTCOME_REFUTED" not in got, + str(sorted(got)), + ) + check( + "INV-027 outcome refuted", + "an Intent citing a frozen record raises nothing", + "INTENT_WITHOUT_RECORD" not in got and "INTENT_UNKNOWN" not in got, + str(sorted(got)), + ) + + refuted = scratch_model({"pay": base, "intents": intents, "ext": externals}) + refuted.observations.append( + { + "assertion": "assert.pay.settles", + "reference": "ops/slo/settlement.yaml#paid_rate", + "observed_at": stamp(1), + "supports": "refutes", + "observer": "runtime", + "kind": "runtime", + } + ) + findings = T.validate(refuted, None, {}, NOW) + got = codes(findings) + check( + "INV-027 outcome refuted", + "an outcome refuted by its own check is an error, not a warning", + "OUTCOME_REFUTED" in got + and any(f.code == "OUTCOME_REFUTED" and f.level == "error" for f in findings), + str(sorted(got)), + ) + check( + "INV-027 outcome refuted", + "integrity goes INVALID: the model is wrong, not merely stale", + T.integrity(findings, {}, None) == "INVALID", + T.integrity(findings, {}, None), + ) + + # Positive control. A later supporting observation must lift it, or the check is + # answering "there exists a refutes" rather than "the latest observation refutes". + refuted.observations.append( + { + "assertion": "assert.pay.settles", + "reference": "ops/slo/settlement.yaml#paid_rate", + "observed_at": stamp(2), + "supports": "supports", + "observer": "runtime", + "kind": "runtime", + } + ) + check( + "INV-027 outcome refuted", + "a later supporting observation clears it", + "OUTCOME_REFUTED" not in codes(T.validate(refuted, None, {}, NOW)), + ) + + unknown = {**base, "realizes": ["intent.nobody-declared-this"]} + got = codes( + T.validate( + scratch_model({"pay": unknown, "intents": intents, "ext": externals}), + None, + {}, + NOW, + ) + ) + check( + "INV-027 outcome refuted", + "realizing an undeclared Intent is an error", + "INTENT_UNKNOWN" in got, + str(sorted(got)), + ) + + no_intent = {k: v for k, v in base.items() if k != "realizes"} + findings = T.validate( + scratch_model({"pay": no_intent, "intents": intents, "ext": externals}), + None, + {}, + NOW, + ) + check( + "INV-028 intent is provenance", + "a Flow with no Intent is reported at info and never gates", + any(f.code == "FLOW_WITHOUT_INTENT" and f.level == "info" for f in findings) + and not [f for f in findings if f.level == "error"], + str(sorted(codes(findings))), + ) + + opinion = { + "id": "system.x.intents", + "type": "intents", + "intents": [ + {"id": "intent.money-moves-once", "statement": "because", "record": {}} + ], + } + got = codes( + T.validate( + scratch_model({"pay": base, "intents": opinion, "ext": externals}), + None, + {}, + NOW, + ) + ) + check( + "INV-028 intent is provenance", + "an Intent with no record is an opinion, and is rejected", + "INTENT_WITHOUT_RECORD" in got, + str(sorted(got)), + ) + + INVARIANTS = [ inv_reality_ne_code, inv_code_change_ne_behavior_change, @@ -2239,6 +2419,7 @@ def confidence(entries, evidence=None): inv_state_ne_health, inv_evidence_ne_assertion, inv_confidence_per_reference, + inv_outcome_can_be_refuted, ] diff --git a/traceos/check_locators.py b/traceos/check_locators.py index 2873282..ee8d48b 100644 --- a/traceos/check_locators.py +++ b/traceos/check_locators.py @@ -50,27 +50,38 @@ def main() -> int: weak_anchor: list[str] = [] checked = 0 - for aid, assertion in sorted(model.assertions.items()): - for evidence in assertion.get("evidence") or []: - locator = evidence["locator"] - path_part, _, anchor = locator.partition("#") - target = repo / path_part - checked += 1 - if not target.is_file(): - missing_file.append(f"{aid}: {locator}") - continue - if not anchor: - continue - how = anchor_present( - target.read_text(encoding="utf-8", errors="ignore"), anchor + cited: list[tuple[str, str]] = [ + (aid, evidence["locator"]) + for aid, assertion in sorted(model.assertions.items()) + for evidence in assertion.get("evidence") or [] + ] + # INV-028: an Intent's record is an address like any other, and it rots like any + # other. It is the one address whose target is frozen, so when it stops resolving + # the record moved or was rewritten - which is exactly when a decision has quietly + # stopped being the decision. + cited += [ + (iid, intent["record"]["locator"]) + for iid, intent in sorted(model.intents.items()) + if (intent.get("record") or {}).get("locator") + ] + + for aid, locator in cited: + path_part, _, anchor = locator.partition("#") + target = repo / path_part + checked += 1 + if not target.is_file(): + missing_file.append(f"{aid}: {locator}") + continue + if not anchor: + continue + how = anchor_present(target.read_text(encoding="utf-8", errors="ignore"), anchor) + if not how: + missing_anchor.append(f"{aid}: {locator}") + elif how == "tail": + weak_anchor.append( + f"{aid}: {locator} (only '{anchor.rsplit('.', 1)[-1]}' " + f"was verified, anywhere in the file)" ) - if not how: - missing_anchor.append(f"{aid}: {locator}") - elif how == "tail": - weak_anchor.append( - f"{aid}: {locator} (only '{anchor.rsplit('.', 1)[-1]}' " - f"was verified, anywhere in the file)" - ) for label, items in ( ("file does not exist", missing_file), diff --git a/traceos/engine.py b/traceos/engine.py index 3f03409..c598ccc 100644 --- a/traceos/engine.py +++ b/traceos/engine.py @@ -102,6 +102,7 @@ def __init__(self, root: pathlib.Path): self.nodes: dict[str, dict] = {} self.node_flow: dict[str, str] = {} self.events: dict[str, dict] = {} + self.intents: dict[str, dict] = {} self.externals: dict[str, dict] = {} self.states: set[str] = set() self.dimensions: dict[str, dict] = {} @@ -161,6 +162,9 @@ def _index(self, path: pathlib.Path, doc: dict) -> None: dtype = doc.get("type") if dtype == "system": self.system = doc + elif dtype == "intents": + for intent in doc.get("intents") or []: + self.intents[intent["id"]] = {**intent, "_where": where} elif dtype == "flow": fid = doc.get("id", where) self.flows[fid] = doc @@ -694,6 +698,155 @@ def validate_observation_refs(model: Model, git: Git | None) -> list[Finding]: "observations/", ) ) + + +def latest_observation(model: Model, assertion_id: str, reference: str | None = None): + """The most recent observation for an assertion, optionally for one reference.""" + best = None + for observation in model.observations: + if observation.get("assertion") != assertion_id: + continue + if reference is not None and observation.get("reference") != reference: + continue + if best is None or observation["observed_at"] >= best["observed_at"]: + best = observation + return best + + +def validate_intents(model: Model) -> list[Finding]: + """INV-028. Intent is verified by provenance, never by truth. + + Nothing can establish that a sentence is really why a behavior was asked for, so + the only checkable question is whether it names the frozen record that asked + (6.2.1) and whether that record still resolves. A model that verified intent by + reading the code would be circular: the code is what the intent is supposed to + explain. + + The community already writes those records - ADRs in `docs/decisions/`, closed + issues, commit trailers - and tooling exists to author and supersede them. TraceOS + does not add a format. It cites one, and notices when the citation stops + resolving. + """ + findings = [] + for iid, intent in sorted(model.intents.items()): + where = intent.get("_where", iid) + record = intent.get("record") or {} + if not record.get("locator"): + findings.append( + Finding( + "error", + "INTENT_WITHOUT_RECORD", + f"{iid}: an Intent must cite the record that asked for it " + f"(INV-028); a statement with no record is an opinion", + where, + ) + ) + elif record.get("kind") != "decision": + findings.append( + Finding( + "warn", + "INTENT_RECORD_NOT_A_DECISION", + f"{iid}: record kind is '{record.get('kind')}'. A frozen " + f"decision is the only evidence that cannot go stale under " + f"its own subject (6.2.1)", + where, + ) + ) + + declared = set(model.intents) + for fid, flow in model.flows.items(): + where = flow.get("id", fid) + realizes = flow.get("realizes") or [] + for iid in realizes: + if iid not in declared: + findings.append( + Finding( + "error", + "INTENT_UNKNOWN", + f"{fid} realizes {iid}, which is not declared", + where, + ) + ) + if not realizes and flow.get("lifecycle") == "current": + findings.append( + Finding( + "info", + "FLOW_WITHOUT_INTENT", + f"{fid}: no Intent declares why this Flow exists. Reported, " + f"never gated - most behavior predates anyone writing the " + f"decision down", + where, + ) + ) + return findings + + +def validate_outcome_verification(model: Model) -> list[Finding]: + """INV-027. An Outcome declares the Assertion that checks it, or says it has none. + + This is the only place in the model where reality can contradict it. Intent and + Behavior are both statements, and two statements can disagree only about words; an + Outcome can be measured, so a `refutes` against one is the first machine-produced + evidence that a claim is *wrong* rather than merely *stale* (6.4). + + An outcome that declares no check is reported, not failed. Silence is the thing + forbidden: an unverifiable outcome and a verified one must not read the same + (INV-025). + """ + findings = [] + for fid, flow in model.flows.items(): + where = flow.get("id", fid) + for outcome in flow.get("outcomes") or []: + oid = outcome.get("id", "?") + verified_by = outcome.get("verified_by") + if not verified_by: + findings.append( + Finding( + "warn", + "OUTCOME_NOT_VERIFIABLE", + f"{fid}: outcome '{oid}' declares no Assertion that " + f"checks whether it occurred (INV-027)", + where, + ) + ) + continue + assertion = model.assertions.get(verified_by) + if assertion is None: + findings.append( + Finding( + "error", + "OUTCOME_CHECK_UNKNOWN", + f"{fid}: outcome '{oid}' is verified_by {verified_by}, " + f"which is not an Assertion in this model", + where, + ) + ) + continue + observation = latest_observation(model, verified_by) + if observation is None: + findings.append( + Finding( + "warn", + "OUTCOME_NEVER_CHECKED", + f"{fid}: outcome '{oid}' names a check ({verified_by}) " + f"that has never been observed", + where, + ) + ) + continue + if observation.get("supports") == "refutes": + findings.append( + Finding( + "error", + "OUTCOME_REFUTED", + f"{fid}: outcome '{oid}' is refuted by its own check - " + f"{verified_by} was observed {observation.get('observed_at')} " + f"by {observation.get('observer', 'unknown')} and did not " + f"hold. The model says this outcome occurs; evidence says " + f"it did not (INV-027)", + where, + ) + ) return findings @@ -1154,6 +1307,8 @@ def validate( findings += validate_relationships(model) findings += validate_outcomes(model) findings += validate_observation_refs(model, git) + findings += validate_intents(model) + findings += validate_outcome_verification(model) findings += validate_identity(model) findings += validate_evidence(model) findings += validate_confidence(model, at_time, git) @@ -1805,6 +1960,36 @@ def write(rel: str, text: str) -> None: """, ) + write( + "model/intents.md", + f"""--- +id: system.{slug}.intents +type: intents +intents: [] +--- + +## What belongs here + +An Intent says **why** a behavior was asked for, and cites the frozen record that asked +for it - an ADR, a closed issue, a decision minute (spec 6.2.1): + +```yaml +intents: + - id: intent.money-moves-once + statement: a buyer is charged once per order + record: {{ kind: decision, locator: "docs/decisions/0007-idempotent-charges.md#Decision" }} +``` + +Intent is verified by **provenance, never by truth** (INV-028): whether the record still +resolves, and whether it has been superseded. Whether the sentence is really the reason +is not answerable by any mechanism. + +Leave this empty until a record exists. A Flow with no Intent is reported and never +gated, because most behavior predates anyone writing the decision down - and an invented +Intent is worse than none. +""", + ) + write( "model/events.md", f"""--- @@ -1850,7 +2035,7 @@ def write(rel: str, text: str) -> None: relationships: - { type: transitions_to, source: node.example.step, target: state.example.done } outcomes: - - { id: example.done, states: [{ subject: example, value: done }] } + - { id: example.done, states: [{ subject: example, value: done }], verified_by: assert.example.works } assertions: - id: assert.example.works claim: "describe what is true, not what the code looks like" diff --git a/traceos/schema/traceos.schema.json b/traceos/schema/traceos.schema.json index d4fd774..85e8dd1 100644 --- a/traceos/schema/traceos.schema.json +++ b/traceos/schema/traceos.schema.json @@ -3,70 +3,137 @@ "$id": "https://traceos.dev/schema/v0.1/traceos.schema.json", "title": "TraceOS v0.1 model document", "description": "Validates the YAML frontmatter of a TraceOS model document. Structure only - semantics are defined by docs/semantic-specification.md. Rules that this schema cannot express (relationship matrix, next-same-flow, contradiction, confidence<=computed, coverage declared vs measured) are enforced by the validator.", - "$defs": { "id": { "type": "string", - "pattern": "^(system|domain|flow|node|state|event|external|assert)\\.[a-z0-9-]+(\\.[a-z0-9-]+)*$" + "pattern": "^(system|domain|flow|node|state|event|external|assert|intent)\\.[a-z0-9-]+(\\.[a-z0-9-]+)*$" + }, + "lifecycle": { + "enum": [ + "current", + "proposed", + "planned", + "deprecated" + ] + }, + "confidence": { + "enum": [ + "uncertain", + "likely", + "confirmed" + ] }, - "lifecycle": { "enum": ["current", "proposed", "planned", "deprecated"] }, - "confidence": { "enum": ["uncertain", "likely", "confirmed"] }, "contextSelector": { "type": "object", "description": "INV-008: selects applicability. Empty or absent matches every context.", - "additionalProperties": { "type": ["string", "number", "boolean"] } + "additionalProperties": { + "type": [ + "string", + "number", + "boolean" + ] + } }, "supersedes": { "type": "array", "description": "INV-014: identity history metadata. Never traversed, never a relationship.", - "items": { "$ref": "#/$defs/id" } + "items": { + "$ref": "#/$defs/id" + } }, "stateRef": { "type": "object", - "required": ["subject", "value"], + "required": [ + "subject", + "value" + ], "additionalProperties": false, "properties": { - "subject": { "type": "string" }, - "value": { "type": ["string", "number", "boolean"] } + "subject": { + "type": "string" + }, + "value": { + "type": [ + "string", + "number", + "boolean" + ] + } } }, "evidenceReference": { "type": "object", "description": "AUTHORED half of Evidence. The observation half is RECORDED, in observations/*.jsonl.", - "required": ["kind", "locator"], + "required": [ + "kind", + "locator" + ], "additionalProperties": false, "properties": { "kind": { - "enum": ["implementation", "test", "runtime", "configuration", "external_contract", "documentation"] + "enum": [ + "implementation", + "test", + "runtime", + "configuration", + "external_contract", + "documentation", + "decision" + ] }, "locator": { "type": "string", "minLength": 1, "description": "INV-022: symbol path, never a line number.", - "not": { "pattern": ":[0-9]+$" } + "not": { + "pattern": ":[0-9]+$" + } }, - "note": { "type": "string" } + "note": { + "type": "string" + } } }, "assertion": { "type": "object", - "required": ["id", "claim", "subject", "lifecycle"], + "required": [ + "id", + "claim", + "subject", + "lifecycle" + ], "additionalProperties": false, "properties": { - "id": { "$ref": "#/$defs/id" }, - "claim": { "type": "string", "minLength": 1 }, + "id": { + "$ref": "#/$defs/id" + }, + "claim": { + "type": "string", + "minLength": 1 + }, "subject": { "type": "string", "description": "Contradiction is detected per subject with overlapping selectors." }, - "when": { "$ref": "#/$defs/contextSelector" }, - "lifecycle": { "$ref": "#/$defs/lifecycle" }, + "when": { + "$ref": "#/$defs/contextSelector" + }, + "lifecycle": { + "$ref": "#/$defs/lifecycle" + }, "about": { "type": "array", "description": "Entities this assertion is about. Drives the reverse index locator -> assertion -> node (spec 11.1).", - "items": { "$ref": "#/$defs/id" } + "items": { + "$ref": "#/$defs/id" + } + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/$defs/evidenceReference" + } }, - "evidence": { "type": "array", "items": { "$ref": "#/$defs/evidenceReference" } }, "confidence_asserted": { "$ref": "#/$defs/confidence", "description": "INV-009: optional human override. Validator checks asserted <= computed." @@ -79,99 +146,314 @@ }, "node": { "type": "object", - "required": ["id", "type", "name"], + "required": [ + "id", + "type", + "name" + ], "additionalProperties": false, "properties": { - "id": { "$ref": "#/$defs/id" }, + "id": { + "$ref": "#/$defs/id" + }, "type": { - "enum": ["action", "decision", "event", "interaction"], + "enum": [ + "action", + "decision", + "event", + "interaction" + ], "description": "INV-003: there is no 'state' node type." }, - "name": { "type": "string", "minLength": 1 }, - "supersedes": { "$ref": "#/$defs/supersedes" } + "name": { + "type": "string", + "minLength": 1 + }, + "supersedes": { + "$ref": "#/$defs/supersedes" + } } }, "relationship": { "type": "object", - "required": ["type", "source", "target"], + "required": [ + "type", + "source", + "target" + ], "additionalProperties": false, "properties": { "type": { - "enum": ["next", "triggers", "invokes", "emits", "depends_on", "interacts_with", "transitions_to"], + "enum": [ + "next", + "triggers", + "invokes", + "emits", + "depends_on", + "interacts_with", + "transitions_to" + ], "description": "INV-006: endpoint pairs are checked by the validator against the matrix." }, - "source": { "$ref": "#/$defs/id" }, - "target": { "$ref": "#/$defs/id" }, + "source": { + "$ref": "#/$defs/id" + }, + "target": { + "$ref": "#/$defs/id" + }, "condition": { "type": "string", "description": "INV-007: only meaningful on 'next'. Enforced by the validator." }, - "when": { "$ref": "#/$defs/contextSelector" } + "when": { + "$ref": "#/$defs/contextSelector" + } } }, "outcome": { "type": "object", - "description": "INV-015: declared by the Flow, bound to States. Never inferred from topology.", - "required": ["id", "states"], + "description": "INV-015: declared by the Flow, bound to States. Never inferred from topology. INV-027: it also declares the Assertion that checks whether it occurred, or says in the open that it declares none.", + "required": [ + "id", + "states" + ], "additionalProperties": false, "properties": { - "id": { "type": "string", "minLength": 1 }, - "states": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/stateRef" } } + "id": { + "type": "string", + "minLength": 1 + }, + "states": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/stateRef" + } + }, + "verified_by": { + "$ref": "#/$defs/id" + } } }, "trigger": { "type": "object", "description": "INV-004: a Flow property, not an entity.", - "required": ["kind"], + "required": [ + "kind" + ], "additionalProperties": false, "properties": { - "kind": { "enum": ["event", "schedule", "state_change", "user_action", "external_event"] }, - "ref": { "$ref": "#/$defs/id" }, + "kind": { + "enum": [ + "event", + "schedule", + "state_change", + "user_action", + "external_event" + ] + }, + "ref": { + "$ref": "#/$defs/id" + }, "semantic": { "type": "string", "description": "For kind=schedule. Semantic only - never a cron expression." }, - "actor": { "$ref": "#/$defs/id" } + "actor": { + "$ref": "#/$defs/id" + } }, "allOf": [ - { "if": { "properties": { "kind": { "const": "event" } }, "required": ["kind"] }, - "then": { "required": ["ref"] } }, - { "if": { "properties": { "kind": { "const": "external_event" } }, "required": ["kind"] }, - "then": { "required": ["ref"] } }, - { "if": { "properties": { "kind": { "const": "state_change" } }, "required": ["kind"] }, - "then": { "required": ["ref"] } }, - { "if": { "properties": { "kind": { "const": "schedule" } }, "required": ["kind"] }, - "then": { "required": ["semantic"] } }, - { "if": { "properties": { "kind": { "const": "user_action" } }, "required": ["kind"] }, - "then": { "required": ["actor"] } } + { + "if": { + "properties": { + "kind": { + "const": "event" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "ref" + ] + } + }, + { + "if": { + "properties": { + "kind": { + "const": "external_event" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "ref" + ] + } + }, + { + "if": { + "properties": { + "kind": { + "const": "state_change" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "ref" + ] + } + }, + { + "if": { + "properties": { + "kind": { + "const": "schedule" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "semantic" + ] + } + }, + { + "if": { + "properties": { + "kind": { + "const": "user_action" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "actor" + ] + } + } ] + }, + "intent": { + "type": "object", + "description": "INV-028: why a behavior was asked for. Verified by provenance - is this the record that asked for it, has it been superseded - never by truth, which no mechanism can establish.", + "required": [ + "id", + "statement", + "record" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/id" + }, + "statement": { + "type": "string", + "minLength": 1 + }, + "record": { + "$ref": "#/$defs/evidenceReference" + }, + "lifecycle": { + "$ref": "#/$defs/lifecycle" + }, + "supersedes": { + "$ref": "#/$defs/supersedes" + } + } } }, - "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "id": { "$ref": "#/$defs/id" }, - "type": { "enum": ["system", "flow", "externals", "events", "contexts"] }, - - "integrity": { "not": {}, "description": "INV-001: DERIVED, never authored." }, - "impact": { "not": {}, "description": "INV-001: DERIVED, never authored." }, - "effective_reality":{ "not": {}, "description": "INV-002: DERIVED, never authored." }, - "coverage_measured":{ "not": {}, "description": "INV-001: DERIVED, never authored." }, - "health": { "not": {}, "description": "INV-018: out of scope in v0.1." } + "id": { + "$ref": "#/$defs/id" + }, + "type": { + "enum": [ + "system", + "flow", + "externals", + "events", + "contexts", + "intents" + ] + }, + "integrity": { + "not": {}, + "description": "INV-001: DERIVED, never authored." + }, + "impact": { + "not": {}, + "description": "INV-001: DERIVED, never authored." + }, + "effective_reality": { + "not": {}, + "description": "INV-002: DERIVED, never authored." + }, + "coverage_measured": { + "not": {}, + "description": "INV-001: DERIVED, never authored." + }, + "health": { + "not": {}, + "description": "INV-018: out of scope in v0.1." + } }, - "allOf": [ { - "if": { "properties": { "type": { "const": "system" } }, "required": ["type"] }, + "if": { + "properties": { + "type": { + "const": "system" + } + }, + "required": [ + "type" + ] + }, "then": { - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false, "properties": { - "id": true, "type": true, - "name": { "type": "string" }, - "domains": { "type": "array", "items": { "type": "string" } }, - "staleness_window_days": { "type": "integer", "minimum": 1, "default": 90 }, + "id": true, + "type": true, + "name": { + "type": "string" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "staleness_window_days": { + "type": "integer", + "minimum": 1, + "default": 90 + }, "repo_prefix": { "type": "string", "description": "Where this model's locators sit relative to the repository root, e.g. \"packages/api\" in a monorepo. Without it every path a diff produces misses, and a miss is invisible." @@ -180,41 +462,118 @@ } }, { - "if": { "properties": { "type": { "const": "flow" } }, "required": ["type"] }, + "if": { + "properties": { + "type": { + "const": "flow" + } + }, + "required": [ + "type" + ] + }, "then": { - "required": ["domain", "lifecycle", "trigger", "coverage_declared"], + "required": [ + "domain", + "lifecycle", + "trigger", + "coverage_declared" + ], "additionalProperties": false, "properties": { - "id": true, "type": true, - "domain": { "type": "string" }, - "lifecycle": { "$ref": "#/$defs/lifecycle" }, - "coverage_declared": { "enum": ["complete", "partial", "stub"] }, - "trigger": { "$ref": "#/$defs/trigger" }, - "supersedes": { "$ref": "#/$defs/supersedes" }, - "nodes": { "type": "array", "items": { "$ref": "#/$defs/node" } }, - "relationships": { "type": "array", "items": { "$ref": "#/$defs/relationship" } }, - "outcomes": { "type": "array", "items": { "$ref": "#/$defs/outcome" } }, - "assertions": { "type": "array", "items": { "$ref": "#/$defs/assertion" } } + "id": true, + "type": true, + "domain": { + "type": "string" + }, + "lifecycle": { + "$ref": "#/$defs/lifecycle" + }, + "coverage_declared": { + "enum": [ + "complete", + "partial", + "stub" + ] + }, + "trigger": { + "$ref": "#/$defs/trigger" + }, + "supersedes": { + "$ref": "#/$defs/supersedes" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/$defs/node" + } + }, + "relationships": { + "type": "array", + "items": { + "$ref": "#/$defs/relationship" + } + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/$defs/outcome" + } + }, + "assertions": { + "type": "array", + "items": { + "$ref": "#/$defs/assertion" + } + }, + "realizes": { + "type": "array", + "description": "INV-028: the Intents this Flow exists to serve.", + "items": { + "$ref": "#/$defs/id" + } + } } } }, { - "if": { "properties": { "type": { "const": "externals" } }, "required": ["type"] }, + "if": { + "properties": { + "type": { + "const": "externals" + } + }, + "required": [ + "type" + ] + }, "then": { - "required": ["externals"], + "required": [ + "externals" + ], "additionalProperties": false, "properties": { - "id": true, "type": true, + "id": true, + "type": true, "externals": { "type": "array", "items": { "type": "object", - "required": ["id", "name"], + "required": [ + "id", + "name" + ], "additionalProperties": false, "properties": { - "id": { "$ref": "#/$defs/id" }, - "name": { "type": "string" }, - "note": { "type": "string" } + "id": { + "$ref": "#/$defs/id" + }, + "name": { + "type": "string" + }, + "note": { + "type": "string" + } } } } @@ -222,22 +581,46 @@ } }, { - "if": { "properties": { "type": { "const": "events" } }, "required": ["type"] }, + "if": { + "properties": { + "type": { + "const": "events" + } + }, + "required": [ + "type" + ] + }, "then": { - "required": ["events"], + "required": [ + "events" + ], "additionalProperties": false, "properties": { - "id": true, "type": true, + "id": true, + "type": true, "events": { "type": "array", "items": { "type": "object", - "required": ["id", "name"], + "required": [ + "id", + "name" + ], "additionalProperties": false, "properties": { - "id": { "$ref": "#/$defs/id" }, - "name": { "type": "string" }, - "relationships": { "type": "array", "items": { "$ref": "#/$defs/relationship" } } + "id": { + "$ref": "#/$defs/id" + }, + "name": { + "type": "string" + }, + "relationships": { + "type": "array", + "items": { + "$ref": "#/$defs/relationship" + } + } } } } @@ -245,22 +628,74 @@ } }, { - "if": { "properties": { "type": { "const": "contexts" } }, "required": ["type"] }, + "if": { + "properties": { + "type": { + "const": "intents" + } + }, + "required": [ + "type" + ] + }, + "then": { + "additionalProperties": false, + "properties": { + "id": true, + "type": true, + "intents": { + "type": "array", + "items": { + "$ref": "#/$defs/intent" + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "contexts" + } + }, + "required": [ + "type" + ] + }, "then": { - "required": ["dimensions"], + "required": [ + "dimensions" + ], "additionalProperties": false, "properties": { - "id": true, "type": true, + "id": true, + "type": true, "dimensions": { "type": "array", "items": { "type": "object", - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false, "properties": { - "name": { "type": "string" }, - "values": { "type": "array", "items": { "type": ["string", "number", "boolean"] } }, - "note": { "type": "string" } + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": [ + "string", + "number", + "boolean" + ] + } + }, + "note": { + "type": "string" + } } } } From 03c9e6937f7b43525007da01fe2bb234d8c41712 Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 01:26:38 +0700 Subject: [PATCH 2/4] A validator that returns None passes every test that never runs it --- tests/run_tests.py | 8 ++++++++ traceos/engine.py | 1 + 2 files changed, 9 insertions(+) diff --git a/tests/run_tests.py b/tests/run_tests.py index 048b89e..f41cbce 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1304,6 +1304,14 @@ def run(*a): run("checkout", "-q", "--orphan", "rewritten") run("add", "-A") run("commit", "-qm", "history rewritten, every old commit orphaned") + # validate() with a real Git was reachable only through the CLI, so a + # function that returned None instead of a list passed the whole suite. + findings = T.validate(T.Model(out), None, {}, NOW, git) + check( + "TOOL symbol decay", + "validate runs against a real repository, not only a git-less model", + isinstance(findings, list), + ) check( "TOOL symbol decay", "the old ref is now unreachable, as after a squash merge", diff --git a/traceos/engine.py b/traceos/engine.py index c598ccc..a870a87 100644 --- a/traceos/engine.py +++ b/traceos/engine.py @@ -698,6 +698,7 @@ def validate_observation_refs(model: Model, git: Git | None) -> list[Finding]: "observations/", ) ) + return findings def latest_observation(model: Model, assertion_id: str, reference: str | None = None): From 2266f127da4723f91fe59bce1801b3fc5bfd5f3b Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 01:26:51 +0700 Subject: [PATCH 3/4] Discharge the six references this change moved --- examples/traceos-itself/observations/2026-09.jsonl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/traceos-itself/observations/2026-09.jsonl b/examples/traceos-itself/observations/2026-09.jsonl index 18d5184..3b98bf0 100644 --- a/examples/traceos-itself/observations/2026-09.jsonl +++ b/examples/traceos-itself/observations/2026-09.jsonl @@ -317,3 +317,9 @@ {"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"} +{"assertion": "assert.reconcile.confidence-decays", "reference": "traceos/engine.py#computed_confidence", "observed_at": "2026-09-11T18:26:50.730417Z", "observed_ref": "03c9e6937f7b43525007da01fe2bb234d8c41712", "observed_blob": "a870a87f68df6f8f3608f43fe3e353e63c56e170", "observed_norm": "8d3e91f52e26d5b2f015b02c910bef33b0ea54e0", "supports": "supports", "observer": "agent", "kind": "implementation", "observed_symbol": "38969d00d7e3e83ca76a2d36b8f79fc439af0061"} +{"assertion": "assert.reconcile.integrity-is-computed", "reference": "traceos/schema/traceos.schema.json", "observed_at": "2026-09-11T18:26:50.859206Z", "observed_ref": "03c9e6937f7b43525007da01fe2bb234d8c41712", "observed_blob": "85e8dd176d03570cb5158e0bae1ec345146e2f42", "observed_norm": "c1338723209f735292ad1a1f00404f021e97eeb8", "supports": "supports", "observer": "agent", "kind": "configuration"} +{"assertion": "assert.reconcile.integrity-is-computed", "reference": "skills/reconciling-reality/SKILL.md#Report Integrity", "observed_at": "2026-09-11T18:26:50.997350Z", "observed_ref": "03c9e6937f7b43525007da01fe2bb234d8c41712", "observed_blob": "9a0dd913f963ba2bd574221b18e521d8114b8bdf", "observed_norm": "1d074d8815b8140079f6099842d1b751908dc343", "supports": "supports", "observer": "agent", "kind": "documentation", "observed_symbol": "0834d6200121a538d7ec2d821d60df370c61f60a"} +{"assertion": "assert.reconcile.unverifiable-is-not-verified", "reference": "traceos/engine.py#Git", "observed_at": "2026-09-11T18:26:51.128266Z", "observed_ref": "03c9e6937f7b43525007da01fe2bb234d8c41712", "observed_blob": "a870a87f68df6f8f3608f43fe3e353e63c56e170", "observed_norm": "8d3e91f52e26d5b2f015b02c910bef33b0ea54e0", "supports": "supports", "observer": "agent", "kind": "implementation", "observed_symbol": "ab57e29fc8ce1e431dc098f762decf18e309ef3e"} +{"assertion": "assert.understand.observes-before-asserting", "reference": "skills/understanding-system/SKILL.md#Record Evidence references, then observe", "observed_at": "2026-09-11T18:26:51.264951Z", "observed_ref": "03c9e6937f7b43525007da01fe2bb234d8c41712", "observed_blob": "7d4ed02c376c5ef0facae5ec50e75069e92355a2", "observed_norm": "cae3a5732f59f4ca4c57d9c39a483e748566b39d", "supports": "supports", "observer": "agent", "kind": "documentation", "observed_symbol": "9062a1bf06b12d7e23b7cdd9b7774c4de38d04a0"} +{"assertion": "assert.understand.observes-before-asserting", "reference": "traceos/engine.py#observe", "observed_at": "2026-09-11T18:26:51.398497Z", "observed_ref": "03c9e6937f7b43525007da01fe2bb234d8c41712", "observed_blob": "a870a87f68df6f8f3608f43fe3e353e63c56e170", "observed_norm": "8d3e91f52e26d5b2f015b02c910bef33b0ea54e0", "supports": "supports", "observer": "agent", "kind": "implementation", "observed_symbol": "d4a3dae251d0a995e6decdb6ce84fd030ec7f3cd"} From 850178e71dac9a2defc3943af9964c5f8ef266bb Mon Sep 17 00:00:00 2001 From: junixlabs Date: Sat, 12 Sep 2026 01:27:37 +0700 Subject: [PATCH 4/4] Status table tells what is built from what is proven --- README.md | 12 +++++++++--- docs/DIRECTION.md | 21 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 65f82eb..f64d1a2 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,15 @@ neither can tell you that a behavior quietly stopped matching what it was for. | 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 | +| **Behavior** | the Flow graph, evidence, impact, erosion detection | **works** — modelled against itself in CI | +| **Outcome** | an outcome names the check that says whether it occurred; a `refutes` there is an **error** | **mechanism works, value unproven** — INV-027, the only place reality can contradict the model | +| **Intent** | a flow cites the frozen decision that asked for it, verified by provenance | **mechanism works, value unproven** — INV-028, citing the ADRs you already write rather than adding a format | + +"Value unproven" is not modesty. The mechanism is built, tested and enforced; what has +**not** been run is the measurement that would justify it — take three incidents that +actually happened and ask whether this would have caught any before they became one. +That kill condition is written down in +[ADR-015](docs/decisions/015-intent-behavior-outcome.md) and has not been answered. 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. diff --git a/docs/DIRECTION.md b/docs/DIRECTION.md index ee4823b..73e68eb 100644 --- a/docs/DIRECTION.md +++ b/docs/DIRECTION.md @@ -66,7 +66,14 @@ what it excluded (INV-025). *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 +### H2 — Outcome carries evidence — *mechanism built, value unproven* + +**Built (INV-027).** An Outcome names the Assertion that checks whether it occurred, or is +reported as declaring none. A `refutes` on that check raises `OUTCOME_REFUTED` at error +level and takes integrity to INVALID. What was missing was never the `runtime` evidence +kind — it was anything that treated a refuted outcome differently from a stale reference. + +**Not built, and not a coding task:** the proof that it catches anything. 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 @@ -83,7 +90,17 @@ the invariant currently conflates them. 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 +### H3 — Intent as provenance, never as truth — *built* + +**Built (INV-028).** An Intent cites a frozen record (§6.2.1); `realizes:` on a Flow names +the Intents it serves; `check_locators` checks that record's address like any other, so a +decision whose record was moved or rewritten stops resolving. `FLOW_WITHOUT_INTENT` is +reported at `info` and never gates. + +**Deliberately not built:** any format for the record. Research before building — ADRs, +closed issues and commit trailers already exist with conventions and tooling, and what the +ecosystem lacks is not another decision format but anything that notices when an accepted +decision has stopped matching the behavior. 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