Repository navigation
EWP-0.2.0
Protocol EWP-0.2.0, policy reference-v2. The development iterations previously listed as 0.2.0, 0.2.0-docs, 0.2.1, and 0.2.2 are folded into this entry; none of them was a release. The 26 golden axes are unchanged from 0.1.0 (only their identity fields changed); the hardening pack grew to 26 fixtures, a 35-view invalid pack was added, and all of it is locked.
Identity
- The Python package is
ewp(wasprotocol, a generic top-level name that could collide in a tester's environment):from ewp.warrant import warrant_now,python -m ewp.mcp_server. Theewp-mcpandewp-ingestcommands are unchanged. - Policy is
reference-v2. Subject binding, supersession scope, availability, and input validation changed, soreference-v1no longer names this evaluator's judgments.warrant_nowrefusesreference-v1and any other identity it does not implement. WarrantViewcarriesprotocol_version(EWP-0.2.0). Reproducibility is stated over protocol version + view + policy +evaluated_at.WarrantView.normative()isprotocol_version,proposition_id,view_id,policy_id,policy_version,evaluated_at, and the five axes.rationale_codesandstrengthare diagnostic, not part of the equality contract.docs/PROTOCOL.md,docs/implementer/SCHEMA.md, and the code now agree on this.
Evaluator (reference-v2)
- Subject binding uses declared
subjects[]as exact ids.scopeis never scraped. There is no family inference: a check aboutinvoice-999orprinter-7no longer verifies acustomer-42orserver01proposition. A check that omits subjects cannot verify a view that declares them. - A
superseded_byedge supersedes the proposition only if itsfrom_idis theproposition_idor a variantproposition_id:<suffix>. Unrelated edges in the bounded view no longer mark itSUPERSEDED. - Records whose instant is after
evaluated_at, missing, or unparsable are not available at T. This covers assertions, evidence, and checks. Naive timestamps are UTC; instants are compared, not strings. - Selecting
freshest_checkno longer crashes on an unparsable timestamp; it considers only checks available at T. - Closed enums fail closed:
polarity,result, conflictstatus, and lineagekindoutside their enums make the view invalid (InvalidEvidenceView). Previouslyresult: "pending"verified likesupports,status: "Open"read as no conflict, andpolarity: "oppose"was silently dropped. - Origin trust: only
tool|document|human|api|vendor|sensormay raiseEXTERNAL/HUMAN; endogenous and unknown origins cap atINDIRECT.result=opposesopens conflict and blocksACCEPTED;result=inconclusivecannot raise the class. - Views are bounded: an assertion or evidence item about another proposition, or a conflict row that does not name the proposition, makes the view invalid. Previously a
P-winview whose records were all about another proposition evaluated toACCEPTED. - Field types are enforced:
subjectsmust be lists of strings (a bare string was iterated as characters, socustomer-99could verifycustomer-42on shared letters);freshness_policy_secondsmust be a non-negative int, not bool;degradeda bool;retrieval_scopea string. - Checks whose subjects do not bind still count toward implied conflict (stated explicitly in
POLICY.md). - Identity is unambiguous: repeated assertion/evidence/check/conflict ids in one view are refused (even identical repeats), and one
source_idmust carry oneSourceRef. Previously one source with twolineage_ids counted as two independent lineages in memory while SQLite refused the same view. omitted_sourcesand conflictproposition_idsmust be lists of strings (a string was iterated as characters); lineage endpoints must be non-empty strings (a missing endpoint was stored as"None").- A missing or null required field, a record that is not an object, or a non-numeric or non-finite (
nan,inf) confidence is refused as an invalid view with a clear message (the codec used to raise a bareKeyError/TypeError, and turned a null id into the string"None"); the third evaluator checks the same required fields. - An unparsable
evaluated_atis refused with a clear message by both reference evaluators and the third evaluator (the third evaluator used to returnUNACCEPTED).
Action gate
may_actreturnsDENYfor aSUPERSEDEDproposition at high risk and at leastREQUIRE_CONFIRMATIONotherwise (low risk used to returnMAY_ACT).may_actrefuses an unknown risk level instead of treating it as medium.high_requires_no_open_conflictandreversibleare live.
Store and adapters
- SQLite partitions every record by
proposition_id. Previously two propositions sharing aview_id(e.g.mcp), or sharing record ids likea1/k1/s1, could overwrite each other's completeness, subjects, freshness, or records. - SQLite is append-only for sources, assertions, evidence, and checks: rewriting an existing id with different content raises
ImmutableRecordError. Conflict participants are fixed across views; status and note belong to each snapshot. Writes are transactional. Databases from earlier development schemas are refused with a clear message. - SQLite persists view completeness (
omitted_sources,retrieval_scope,degraded, freshness, subjects). - Mem0 and Graphiti (fake ingest and live client) park and restore the view's
subjects[]and each check'ssubjects[]. A Mem0 round trip used to turn acustomer-42/customer-99mismatch intoEXTERNAL/ACCEPTED. - Mem0 ingest writes each assertion and each evidence item as its own memory with its own polarity and original timestamp. Previously it kept one memory per source, forced
polarity=supports, and used Mem0'screated_at(ingest time) as observation time, which dropped opposition and could make evidence look future-dated. - Graphiti ingest emits an edge for evidence whose text is not already an assertion, so opposing evidence-only lineages survive
raw_view. Edges may carrypolarity=opposes. Parked sidecars need a matchingproposition_idand are found by kind/content/name, not by Graphiti's assigned UUID. - Mem0 memories without
metadata.ewp.proposition_iddo not enter a proposition view. - The JSON codec keeps falsy values:
freshness_policy_seconds: 0used to become 30 days and an emptyretrieval_scopeused to read ascomplete. A missingview_idis derived from the content. - SQLite views are immutable
(proposition_id, view_id)snapshots with explicit membership over the append-only ledger.get_view(pid, id)returns exactly that snapshot;get_view(pid)the latest;extend_viewappends atomically. Previously a second view for a proposition replaced the first's metadata and merged its evidence under the oldview_id. Unknown propositions raiseMissingViewError. Older development databases are refused.SQLiteAdaptercloses as a context manager. - JSON store writes are serialized per proposition with an OS file lock (released by the OS if a writer dies), and
latest.jsonis replaced atomically. Two concurrent writers used to both pass the identity check, and oneview_idvanished from the history; readers could also hit a half-writtenlatest.json. On Windows a replace fails while any process has the file open (another EWP reader, a virus scanner), and an open can fail mid-replace; reads and replaces now retry that sharing conflict briefly instead of failing (4 readers against 4 writers failed about 60 times per run). Snapshot files are written atomically too, and a snapshot is committed once itsview_idis in the history: only committed snapshots are immutable. A file whoseview_idis not in the history is an orphan of an interrupted write (partial, identical, or corrected), and a retry replaces it after the same cross-snapshot record-id check as any other commit. A partial file used to make every retry fail withJSONDecodeError, and a complete one made the retry a no-op that left the snapshot out of the history. An explicit read returns only committed snapshots; a damaged committed file is an error. - JSON store: files are named by hashes of the ids (a proposition id could previously escape the store root, and
a/bcollided witha_b), writes and reads are validated (astatus: "Open"conflict used to be stored as resolved), and views are immutable snapshots like SQLite. - Mem0 keeps every
SourceReffield (source_id,observed_at,extractor_id),assertion_confidence: 0, and theview_id. - Graphiti edges carry
roles, so an evidence-only record no longer comes back as an assertion (which could liftUNACCEPTEDtoTENTATIVE) and an assertion-only record no longer gains supporting evidence. Ingest storessnapshot_id/extractor_id; parkedsubjectsandview_idare decoded safely. - Live Graphiti (experimental):
search_viewapplies the parked sidecar (it used to drop checks, conflicts, lineage, subjects, and freshness); ingest writes evidence-only sources and setsreference_timefrom the source'sobserved_at. - New
ewp-ingestcommand loads EvidenceViews from JSON into a ledger; trusted origins require--attest-trusted-origins. - All ids are proposition-local, including
conflict_id(it was ledger-global in SQLite, so unrelated propositions could not both usec1). Schema version 4. - One canonical, record-order-independent form of a view (
codec.canonical_dict) drives content-derived ids, SQLite and JSON snapshot comparison, and adapter conformance. Reordered but identical snapshots were refused as mutations and got different ids. - SQLite writes run in
BEGIN IMMEDIATEtransactions, soextend_viewis atomic across processes sharing a ledger: concurrent appends from separate processes used to drop records from the latest snapshot.SQLiteAdapter(path, read_only=True)opens an existing ledger read-only and refuses a missing path. - Mem0 implements immutable snapshots: every memory is tagged with its
view_id, the sidecar (written last) carries a digest and sequence,raw_view(pid, id)returns exactly that snapshot, identical re-ingest is a no-op, and changed content under an existing id is refused.raw_view("P", "v1")used to return every record ever written forP, with duplicates. - The docs state that the Graphiti mappings keep one current view per proposition and are not snapshot stores.
- Graphiti sources take their
observed_atfrom the episode, not from whichever edge cites it, so one episode behind two facts is one consistentSourceRef. ewp-ingestreports an unusable ledger (older schema) with a message instead of a traceback. A bad--evaluated-atis refused before the ledger is opened (it used to store every view and then exit 1 with "3 view(s) not stored").- Record ids are stable across a proposition's snapshots in every store: the JSON store and Mem0 now refuse a later snapshot that reuses an assertion/evidence/check/source id for different content or changes a conflict's participants, as SQLite's ledger already did (one shared check,
codec.record_identity_conflicts). SQLite stores check subjects sorted, so reordered subjects are not a change. A cross-store test runs one sequence of writes against all three. - The JSON codec normalizes optional source ids (
extractor_id,parent_source_id) withstr()like required ones (a number came back from SQLite as text, so an identical re-put was refused). - Mem0 keeps each record's own
proposition_id(a variantpid:<suffix>was overwritten with the view's, and a later snapshot with the same record was then falsely refused). Graphiti lists the collapse as a named, warrant-neutral field loss. ewp-ingestand the MCP server report SQLite errors (busy, locked, corrupt) asEWP_REFUSE_LEDGER_UNAVAILABLE/ a per-view failure instead of a traceback or generic-32000.- The JSON codec reads lineage endpoints from
from_id/to_idonly (undocumentedfrom/toaliases were accepted by the codec but not by the third evaluator). - Mem0: memories tagged with a
view_idbut no sidecar (an interrupted ingest) are never read, including when the proposition has no completed snapshot yet. - Mem0 reads follow the mem0ai 2.x client contract: entity ids in
filters, never top-level (both current clients reject top-leveluser_idon reads, so the adapter's reads failed against them; the test fake accepted the old call). OSSget_allis bounded bytop_k(default 20) and does not page: the adapter asks forread_limitand refuses a read at the limit; the hosted client's pages are all read and checked againstcount. The test fakes now follow both 2.x signatures, andtests/test_mem0_client.pyruns the adapter against a real OSSmem0.Memory(mem0ai 2.2.0, offline); CI installs it. - Live adapters never turn a missing store time into the epoch: a Graphiti episode or a Mem0 memory with no time yields records that are unavailable at every T (they read as 1970, available at every T). A Mem0 per-check memory (
kind: ewp_check, written outside EWP ingest) with an emptyobserved_atstays unavailable instead of taking the memory's ingest time. - Mem0
addis called once per memory (a retry onTypeErrorcould write a memory twice under one ingest, and that snapshot would never match its digest). A memory'sassertion_confidenceis not coerced withfloat(). - Mem0 snapshot reads are checked against the digest committed in the sidecar. A read that lost one opposing memory used to return the rest as
retrieval_scope=complete(TENTATIVE/OPENbecameACCEPTED/NONE); it now fails. Memories written outside EWP, which have no digest, read asmem0.unverified(DEGRADED); a sidecar without a digest, or without a positive integer sequence number, is refused (a missingseqused to read as 0 and let latest pick an older snapshot). Every memory carries its ingest's sequence number, and a latest read that finds records of a newer uncommitted ingest fails instead of serving the older snapshot as current (a read that lost the newest sidecar used to return the previous snapshot as complete). Two snapshots committed concurrently with the same sequence number have no latest (a random tie-break used to pick one, possibly the older, as complete). A store that hides a whole newer snapshot cannot be detected; the docs say so. - Graphiti: only edges of the adapter's
group_idare read, even when a client's search or listing returns others (a search that ignoredgroup_idsput another user's fact in the view). An edge that does not state its group is not trusted to be in it. Episodes that state another group are not read either, so another group's parked checks or provenance cannot enter the view. The parked sidecar'sdegraded,freshness_policy_seconds, andomitted_sourcesare carried as written and refused when mistyped (a"true"string used to read as not degraded, andint(True)as a 1-second freshness). - Graphiti: records are selected by the
proposition_idtheir episodes declare, not by group, one episode at a time, so an edge Graphiti merged from episodes of two propositions appears in each view with its own episodes (an all-episodes rule dropped such an edge from both views while both claimed to be complete). With the documentedgroup_id="user-42",proposition_id="P1"arrangement the live adapter used to return an empty view marked complete. Thefact_textfallback isDEGRADED(graphiti.fact_text) and never joins an edge declared for another proposition. - Graphiti: a record's time is its own, carried on its episode (see below); when an episode declares none, the record takes the episode's reference time, which an edge's
reference_timecan make later but never earlier, and the edge'svalid_at(when the fact held in the world) never counts. An episode observed on September 24 withvalid_atSeptember 20 used to count as evidence at a September 21 evaluation. An episode that cannot be loaded is never turned into a record (its role, polarity, and time are unknown; it used to become supporting evidence dated 1970, and later the edge's ingest time); it is listed inomitted_sourcesof both raw and search views (including when search adds edges the group listing lacked), so the view isDEGRADED. The parkedomitted_sourcesis merged with what the read could not load, not replaced by it. Record times travel on the episode (asserted_at/observed_at, one per role), so a record's own time comes back exactly: the fake ingest stamped every assertion edge with the first assertion's time, and a record whose own time was empty took its episode's, becoming available where the kernel reads it as unavailable. A source whose records of one kind carry different times is refused at ingest. Live Graphiti never replaces an episode, so each re-ingest adds a parked sidecar: the newest by Graphiti'screated_atapplies (listing order used to decide, so an older sidecar could drop a newer open conflict), and two different sidecars created at the same instant fail the read. - Graphiti live ingest writes one episode per source declaring every role it plays and its evidence polarity, and reads take role and polarity from the episode. An opposing-evidence episode on an extracted edge used to come back as
supports, and a source that was both assertion and evidence lost its evidence polarity. A source with both supporting and opposing evidence is refused at ingest. An episode that cannot be loaded degrades every view of its group, in a shared group or one named for the proposition, instead of being dropped or read as support. - Record arrays (
assertions,evidence,checks,conflicts,lineage) that are present but not lists are refused by the codec and the third evaluator;"checks": false,"evidence": "","conflicts": 0, and"lineage": {}used to read as empty.adapter_metamust be an object,assertion_confidencemust be a JSON number that a float can hold ("0.99"andtrueused to be converted byfloat(), and an integer beyond float range crashed the codec withOverflowErrorwhile the third evaluator accepted it), the view'sproposition_idmust not be empty, andview_idmust be an id ({}orfalseused to be replaced by a derived id). Both reference evaluators refuse the same shapes from Python callers (checks=""orlineage={}used to evaluate as empty), and check everySCHEMA.mdrequired field on the view itself (a view whose sources all hadcontent_hash=Noneevaluated toACCEPTED). Seven new invalid views. - SQLite and JSON
get_viewno longer takeomitted_sources/retrieval_scope/degraded/freshness_policy_secondsoverrides, which returned changed metadata under the storedview_id. Two test helpers relied on them, masking any loss of that metadata in the canonical runner. - Mem0 retry after an interrupted ingest: every memory carries the ingest's
ingest_id, and a snapshot is the memories of the ingest its sidecar committed. A retry used to read the crashed attempt's records too and was refused for duplicate ids. Two racing commits of different content under oneview_idare refused on read instead of producing a mixed view.
MCP façade
- Stdio uses the MCP stdio transport: newline-delimited JSON-RPC, one message per line. (A development iteration shipped Content-Length framing, which no MCP client speaks.) Protocol revisions
2025-06-18,2025-03-26, and2024-11-05are negotiated. Notifications are never answered. - Every write needs the server-side ingest role (
--allow-ingeston stdio, the token on HTTP), including conflict, lineage, and view-metadata writes. Without it the server is evaluate-only. Previously an anonymous caller could resolve an open conflict, cleardegraded, or stretch freshness and turn a stale, degraded view intoACCEPTED. Trusted origins also needingest_attestation=true. - Inline views are hypothetical.
ewp_warrant_nowdemotes their trusted origins toinline:<origin>unless the caller holds the ingest role and attests;ewp_may_actrefuses them. Previously any caller could send a made-up view and getMAY_ACTon a high-risk, irreversible action. ewp_may_actdecides on the latest stored snapshot at the server clock. A suppliedevaluated_atis only a sanity check (refused beyond 300 s) and never the decision time; a suppliedview_idmust be the latest (EWP_REFUSE_STALE_VIEW_FOR_ACTION). A caller could previously pick an older snapshot, or a time up to 300 s earlier, that lacked a recent opposing check and turnedDENYintoMAY_ACT. It requiresaction.riskinlow|medium|highand a booleanaction.reversible(a missing risk used to default tolow;"false"used to read as reversible), and acceptsrisk_policyonly from a caller with the ingest role, honoring its flags.ewp_memory_contextdefaultsevaluated_atto server time.ewp_may_actrefuses a caller-builtWarrantView. Incremental records must supplycontent_hashandobserved_at.- HTTP:
--allow-ingestis refused with--http(it would grant every caller the ingest role). The token comes fromEWP_INGEST_TOKENor--ingest-token-file, not argv, and is compared in constant time. Bodies over 1 MiB get413from headers alone. Notifications get202. HTTP remains plain JSON-RPC, not MCP Streamable HTTP. - The pre-freeze claim/confidence sketch stays in
historical/docs/MCP_CONTRACT.md. ewp_list_propositionsfinds propositions by id or assertion text so an agent can discover what is stored.- Reads report the stored
view_id(they used to report"mcp") and accept an optionalview_id; appends create a new snapshot and return its id (new_view_idoptional). - A proposition or
view_idthat was never stored returnsEWP_REFUSE_MISSING_VIEWinstead of silently evaluating an empty view. ewp_evidence_recordrequirespolarity. The ingest role is checked before the payload is examined.- Error shapes follow MCP: tool argument errors are
isErrorresults (EWP_REFUSE_INVALID_ARGUMENTS), resource errors are JSON-RPC errors (-32002for a missing proposition). - A server that cannot write opens its ledger read-only:
--dbis required, a missing file exits withledger not foundinstead of creating an empty ledger, and the handle itself cannot write. ewp_memory_contextwarns when anEXTERNAL/HUMANcheck opposes the claim, sinceverification=EXTERNALthen means checked and contradicted.- A request with
"id": nullgets a-32600reply instead of silence; so does an id that is not a string or integer (it used to be echoed back). Non-objectparams, and a non-stringname,uri, orprotocolVersion, get-32602(they returned a generic-32000with a Python error). HTTP answers405for methods other than GET and POST. Resource URIs decode like URIs: proposition ids are percent-decoded path segments (an id with/or a space was unreadable), the query keeps+(anevaluated_atwith+00:00became a space and failed), and a proposition namedwarrantis readable.tests/test_transport_fuzz.pysends 500 malformed JSON-RPC and HTTP requests and requires a specific error for each and a server that keeps answering.resources/templates/listlists the proposition resources. - The opposing-check warning names the class of the check that opposes (it used to name the strongest class, which can come from a different, supporting check).
ingest_attestationcounts only when it is JSONtrue;"false",1,[], and{}were read as attesting. Aviewargument that is present but not an object is refused (falseused to read as no inline view). Incremental writes normalizeextractor_id/parent_source_idto strings like full views. Tool arguments are checked: anullid is missing (it used to become"None"),check/evidence/action/source/risk_policymust be objects,new_view_idmust be an id (5used to be stored as a number), incrementalassertion_confidencemust be a JSON number (trueand"0.99"were converted byfloat()), and incremental source fields that are present are kept, not replaced with defaults (an emptylineage_idused to become a separate lineage, while a full view kept it).
Conformance and release
- Hardening pack (
ewp/laundering.py) adds subject-binding cases (customer-42/invoice-999,server01/customer-42, omitted check subjects, a shared subject that verifies), an unparsable check time, and an unrelatedsuperseded_byedge (26 fixtures with the two below). tests/test_adapter_roundtrip.pyruns all 52 fixtures through SQLite, JSON, fake Graphiti, and Mem0 and requires identical normative axes. It found the Mem0 polarity and timestamp losses above.RELEASE.lock.jsonhashes fixtures (now including the hardening pack), evaluator (now includingversions.py), the policy text (POLICY.md,policy.json), goldens, and the implementer pack. Hashes are over LF-normalized content;.gitattributeskeeps checkouts LF.tests/report.pyprintsCONFORMANTonly for a CI stamp whose hashes match the current tree; a stale stamp reportsCLAIM_ONLY.docs/implementer/third_eval.pyimplementsreference-v2fromPOLICY.md/policy.jsononly and matches all 52 expected files.tests/test_goldens.pychecks the reference evaluator against the same 52.tests/test_mcp.pydrives the real server over stdio as a subprocess and, when themcppackage is installed, through the official MCP Python SDK client (verified againstmcp2.2.0, negotiating2025-06-18). The GitHub workflow installsmcpso this runs in CI.- Removed
protocol/pathological_fixtures.py, which was not valid Python and was not imported. tests/test_adapter_roundtrip.pyalso compares everySCHEMA.mdfield: the codec, SQLite, JSON, and Mem0 must return views unchanged; fake Graphiti may lose only the fields listed inGRAPHITI_FIELD_LOSSES.- Hardening fixtures
zero_freshness_is_staleandvariant_record_propositions; invalid pack of 35 views (one per input rule) indocs/implementer/invalid/, refused by the kernel, the codec, and the third evaluator. - The CI stamp carries
ci_surface_sha256over every file CI exercises (kernel, adapters, MCP, tests, implementer pack, examples, workflow), hashed before the run;tests/report.pyrequires it to match. Report counts come from the packs. - Differential fuzz tests:
tests/test_parity_fuzz.pyreplaces every field of every implementer fixture with malformed values (about 88,000 views) and requires the kernel, the second evaluator, and the third evaluator to refuse together or agree, never crash;tests/test_mcp_fuzz.pyrequires every malformed MCP tool argument to succeed or be refused with anEWP_*code. They found: the kernel crashed on a list-valued enum field and both evaluators on a non-stringevaluated_at; the third evaluator crashed on a non-stringproposition_idand did not checkview_id; the codec converted non-string lineage endpoints andproposition_idwithstr(); andewp_evidence_recordreturned a generic-32000for an out-of-range confidence. A store-neutrality fuzz (tests/test_store_fuzz.py) round-trips every accepted malformed view through SQLite, JSON, and Mem0: Mem0 replaced empty strings it had written with defaults on read (the digest check then refused the snapshot), and SQLite could not store afreshness_policy_secondsbeyond 64 bits. Mem0 now fills in defaults only for missing values, andfreshness_policy_secondsis capped at2^53 - 1, the largest integer every JSON implementation represents exactly. All fixed; four invalid views added (35).ewp_memory_contextrefuses a non-stringevaluated_at(a falsy one used to mean "now") andewp_evidence_recorda non-stringtext. - The GitHub workflow uses
actions/checkout@v7andactions/setup-python@v7, which run on Node.js 24 (v4 and v5 targeted the deprecated Node.js 20). - CI runs
runner_pathological.pyandtest_ingest_cli.py, and a tester-path job:pip install .,ewp-ingeston the example, and anewp-mcphandshake. pyproject.tomldeclares the license as an SPDX expression (license = "MIT",license-files) withsetuptools>=77; the table form is deprecated, and setuptools will stop building it after 2027-02-18.- The README lock block and the PDF read pack counts from the packs themselves (the README showed
Invalid refused: 24/24after the pack grew). RELEASE.lock.jsonand the CI stamp are written atomically (temp file + replace, with a short retry), so a Windows scanner briefly holding the file no longer fails--write-lock. UnusedSQLiteAdapter.has_propositionremoved.- Removed
protocol/validate.py(unused), a dead expression inrunner_graphiti.py, and a test assertion that could not fail. - Tests: four OS processes appending concurrently to one ledger; read-only ledger (missing path, tool writes, raw SQL writes); per-proposition conflict ids and order-independent snapshots; Mem0 v1/v2 snapshots, idempotent re-ingest, interrupted ingest; read-only agent server end to end; the opposing-check warning; null ids and resource templates.
- The third evaluator treats a
nullfield as its schema default and comparesSourceRefs field by field, like the kernel (it refused valid views that spelled an optional field asnullor omitted it). A parity test runs every fixture through both evaluators in those spellings.
Documentation
docs/PROTOCOL_v0.1.mdis nowdocs/PROTOCOL.mdand states the 0.2.0 contract.docs/EWP_v0.2.0.pdfis rebuilt;docs/build_guide_pdf.pyreads identity and hashes fromRELEASE.lock.jsonso the guide cannot drift from the lock.- README, CONFORMANCE, CONTRIBUTING, SECURITY, VERSION,
docs/MCP_CONTRACT.md,docs/PLATFORMS.md,docs/DESIGN_NOTE.md, and the implementer pack describereference-v2, the ingest role, stdio framing, and the lock. - Security: origin metadata is assumed established at the ingestion/adapter boundary. EWP does not authenticate evidence entering the ledger.
QUICKSTART.mdandexamples/quickstart.json: install, load evidence, connect Claude Code or Claude Desktop, bring your own data.POLICY.md,SCHEMA.md,PROTOCOL.md,MCP_CONTRACT.md,CONFORMANCE.md, andLIVE_ADAPTERS.mddescribe bounded views, typed fields, snapshots, field-level round trips, discovery, and the experimental live Graphiti mapping.POLICY.mdandPROTOCOL.md:EXTERNAL/HUMANclassify the verification channel, not source quality.SCHEMA.md: record arrays default to[];view_idis part of interchange, and deriving it from content is a reference-ingest convenience.docs/PLATFORMS.md: build complete views by key, not similarity; extracting nothing is a valid ingest result; anorigin_locatorconvention for pointing at exact passages.docs/OPEN_QUESTIONS.md: retraction without replacement (unresolved, with what each current encoding yields), assertion polarity, validity time, separate capabilities for ingest and action policy.- README, QUICKSTART, and the PDF name the version as EWP-0.2.0 only.
- README, CONTRIBUTING, CONFORMANCE, SCHEMA,
MCP_CONTRACT.md,LIVE_ADAPTERS.md, and the PDF describe the real Mem0 client test, the fuzz tests, concurrent-safe stores, resource URI encoding, and the Graphiti episode rules.tests/report.pywording says "exact CI surface". The example data notes that itsACCEPTEDrow turnsSTALEafter 2027-09-20.
Known limits
- Conflict rows and lineage edges carry no timestamp, so they are not filtered by availability at T: an edge recorded later still applies when replaying an earlier T.
- Live
graphiti-core 0.30.2is not validated. - HTTP is not MCP Streamable HTTP.
- Assertions carry no polarity: every available assertion counts as grounds for
TENTATIVE, including one that denies the proposition. Planned for a later policy.