{"slug": "show-hn-a-fail-closed-evidence-engine-for-norms-used-by-ai-agents", "title": "Show HN: A fail-closed evidence engine for norms used by AI agents", "summary": "Angeliasrl released norms-mcp-engine version 0.1.0, a fail-closed evidence engine that computes the evidentiary status and admissibility of norms used by AI agents, without any I/O, network, or transport. The engine, which is not an MCP server, provides claim-by-claim evidence and includes 73 tests, allowing consumers to decline reliance on unverified or inapplicable norms.", "body_md": "**Experimental prerelease. Engine only. Not an MCP server. No I/O, network,\npersistence or transport.**\n\nVersion 0.1.0 · claim-by-claim evidence: [ CLAIM_MAP.md](/Angeliasrl/norms-mcp-engine/blob/main/CLAIM_MAP.md) ·\nnot independently verified.\n\nThe admissibility model and claim-map linter, as pure functions. No I/O, no network, no transport.\n\nThis is not an MCP server.It is the engine an MCP server would consume. There is no entrypoint, no tool handler and no deployment here.`norms-mcp`\n\n, the server, does not yet exist as a published artifact. The naming distinction is deliberate: claiming a transport that is absent would be exactly the kind of overclaim this library exists to make visible.\n\n```\nnpm test    # 73 tests + claim-map sync check; no network, no account required\n```\n\nComputes, for each constant in a normative corpus, its **evidentiary status and\neligibility verdict** — so a consumer can decline to rely on something that is\nnot currently good for anything. This package computes; it publishes nothing,\nbecause it has no transport.\n\nIt enforces nothing. It computes eligibility and reports it. The decision belongs to whoever is asking.\n\nThe Italian word for the property is *opponibile*: something you can assert\nagainst someone, before a third party. *Enforceable* is the wrong translation —\nenforcement means somebody applies it. The closest is **admissibility**: whether\na claim holds up when challenged.\n\nOrigin and verification answer different questions, and a constant does not stop being declared when it becomes ratified.\n\n```\norigin.type          SOURCE_DOCUMENT | OWNER_DECLARATION\nverification_state   RATIFIED | UNCONFIRMED\n```\n\n`UNCONFIRMED` |\n`RATIFIED` |\n|\n|---|---|---|\n`SOURCE_DOCUMENT` |\ntranscribed, never checked | checked against its source |\n`OWNER_DECLARATION` |\nasserted, awaiting a ratification act | asserted, then formally approved |\n\nThe bottom-right cell is the one a single-axis scheme cannot name. It matters beyond bookkeeping: it is what lets a later reader distinguish norms derived from documents from norms that stand because the owner said so.\n\nA binary field cannot distinguish *checked and false* from *never checked*.\n\n```\ncurrency         CURRENT | STALE | UNKNOWN\nauthority_status VALID | INVALID | UNKNOWN\nexpiry_status    ACTIVE | EXPIRED | REVIEW_DUE | UNKNOWN\n```\n\n`UNKNOWN`\n\nfails closed — it yields a negative result — but never appears in\n`blocking`\n\n. A thing nobody has examined is not a thing found wanting.\n\n``` js\nimport { eligibleAsGround } from 'norms-mcp-engine/model';\n\neligibleAsGround(entry);\n// { eligible: false,\n//   blocking: [],                    // nothing was found wanting\n//   unknown: ['authority_status'],   // nobody has checked\n//   unexamined: true }\neligible_as_ground = verification_state == RATIFIED\n                     AND currency         == CURRENT\n                     AND authority_status == VALID\n                     AND expiry_status    == ACTIVE\n```\n\nThat answers *may this be used as a ground at all*. It does not answer *does it\nbear on the decision in front of me* — a constant can be ratified, current,\ncompetently approved and unexpired, and still be about something else.\n\n``` js\nimport { admissibleFor } from 'norms-mcp-engine/model';\n\nadmissibleFor(entry, { subject: 'android' });\n```\n\nWhere the entry declares no `scope`\n\n, the result is `admissible: false`\n\nwith\n`scope_known: false`\n\nand a note that applicability is the caller's judgement. The\nengine reports the match; it never decides.\n\n**Limit (v0.1):** scope matching is exact-value only across `subject`\n\n,\n`jurisdiction`\n\nand `applicable_operations`\n\n. No hierarchies, wildcards or\nnegation. An empty or malformed scope **throws** — it never becomes a match,\nbecause \"unspecified\" must not silently mean \"applies to everything\".\n\n```\n{\n  verification_state: 'RATIFIED',\n  ratification: {\n    date:         '2026-07-31',\n    document:     'POLICY.md',\n    sha256:       '<64 lowercase hex>',\n    section_id:   'sec-4-2',      // stable id, resolved against\n    section_label: '§4.2 Retention'  // human-readable, may drift\n  }\n}\n```\n\n`RATIFIED`\n\nwithout a complete block **throws**. This is the guard that stops\n\"verified in conversation\" from ever becoming a ratification.\n\nThe stored `sha256`\n\nis the digest of the document the verification ran against.\nIf the index moves and the ratification does not, the proof is stale — which is\nnot the same as the constant being false.\n\n``` js\nimport { revalidate } from 'norms-mcp-engine/model';\n\nrevalidate(entry, corpusIndex);\n// { currency: 'STALE', reason: 'fingerprint diverged from index; proof requires revalidation' }\n```\n\nA document absent from the index yields `UNKNOWN`\n\n, not `STALE`\n\n.\n\nForcing an artificial decay condition onto a rule that ought to be permanent is its own error.\n\n```\nexpiry_policy: CONDITIONAL | REVIEWED | PERMANENT\n```\n\n`PERMANENT`\n\nnever expires but **throws** unless it records `permanence.authority`\n\nand `permanence.reason`\n\n. Permanence is a decision, not an absence of one. The\ncheck lives in `validateEntry`\n\n, so it cannot be bypassed by never calling\n`evaluateExpiry`\n\n.\n\n`CONDITIONAL`\n\nwithout declared conditions yields `UNKNOWN`\n\n, not `ACTIVE`\n\n: the\nabsence of conditions is not evidence that none fired.\n\n```\nfor each document, in id-byte order:\n    len(id) || id || len(content) || content\n```\n\nTwo properties, both of which have bitten real systems:\n\n**Lengths are UTF-8 byte counts.** `String.prototype.length`\n\nreturns UTF-16 code\nunits. One character outside the BMP makes two conforming implementations\ndisagree. The prefix is 8-byte unsigned big-endian — fixed width and endianness\nare part of the specification.\n\n**Framing, not concatenation.** Two different corpora can concatenate to the same\nbyte stream. The suite demonstrates the collision and shows framing removing it:\n\n```\nok    COLLISION: plain concatenation is ambiguous\n```\n\nPer-document canonical form: UTF-8, no BOM, LF line endings, trailing whitespace stripped, Unicode NFC.\n\nA claim map written as prose is a convention someone has to remember to apply. This makes it a test.\n\n| Rule | |\n|---|---|\n| R1 | `O` requires every evidence field |\n| R2 | `O` requires `PUBLICLY_INSPECTABLE` or `INDEPENDENTLY_ATTESTED` — a private digest is a commitment, not a verification |\n| R3 | `D` requires `derived_from` and `support_status` |\n| R4 | Support rule: a derived claim cannot carry stronger support than its weakest premise |\n| R5 | `A` requires a named precondition |\n| R6 | `derived_from` must reference claims that exist |\n| R7 | No cycles in `derived_from` |\n| R8 | No duplicate claim ids |\n| R9 | `support_status` must be a recognised value |\n| R10 | `D` with no premises must declare `SPEC` or `NONE` |\n\n``` js\nimport { lintClaimMap } from 'norms-mcp-engine/claimmap';\n\nlintClaimMap(claims);\n// { ok: false, findings: [{ id: '2', rule: 'R4', message: '…' }], counts: {…} }\n```\n\n`claims.mjs`\n\nis the canonical form. `CLAIM_MAP.md`\n\nis generated from it by\n`scripts/build-claimmap.mjs`\n\n, and `npm test`\n\nfails if the committed Markdown has\ndrifted from the data or if the map does not pass R1–R10.\n\nThe inversion is deliberate. A map kept as prose is a convention someone has to remember to apply, and in this project's own history that convention failed three revisions running.\n\nStates: `O`\n\n(observed, resolvable by a third party) · `O-PENDING`\n\n(reported, not\nyet resolvable — **not admissible as evidence**) · `D`\n\n(derived) · `A`\n\n(open,\nprecondition named).\n\n**No MCP server.** No transport, no tool handlers, no entrypoint.**No persistence.** Pure functions; the caller owns storage.**No**`applyRevalidation`\n\n/`applyExpiry`\n\nhelpers.`revalidate`\n\nand`evaluateExpiry`\n\nreturn verdicts; the caller applies them.**No JSON schema.** Shapes are enforced by`validateEntry`\n\nand the suite.**No independent audit of the code.** Seven review rounds; the last two read the source. None was a human reviewer.**No independent verification.** One author, one test run, no external replication.\n\nSee `CLAIM_MAP.md`\n\nfor the full evidentiary status of every claim made here.\n\nEvery public API validates its input and throws a `ModelError`\n\nor `CorpusError`\n\nwith a stable `code`\n\n. A malformed record is never interpreted as a verdict —\nneither negative nor positive — because *malformed* and *ineligible* are\ndifferent facts, and a library about evidentiary status must not conflate them.\n\n`normaliseDocuments`\n\nis the single admission gate for a corpus: the digest and\nthe index apply identical rules, so they cannot disagree about which documents\nexist. `buildIndex`\n\nuses a null-prototype object and `revalidate`\n\nlooks up with\n`Object.hasOwn`\n\n, so an id such as `__proto__`\n\nresolves to a real record or to\nnothing.\n\nApache License 2.0 — Copyright 2026 Francesco Riva. See `LICENSE`\n\nfor the full\ntext. Versions up to commit `83438ef`\n\nwere published under MIT; this and later\nrevisions are under Apache-2.0.\n\n`CLAIM_MAP.md`\n\nis under the same licence. A practical request, not a licence\nterm: it is a record of what was claimed and when, so a modified copy should\nnot circulate under the same name — its value lies in not having been altered\nafter the fact.", "url": "https://wpnews.pro/news/show-hn-a-fail-closed-evidence-engine-for-norms-used-by-ai-agents", "canonical_source": "https://github.com/Angeliasrl/norms-mcp-engine", "published_at": "2026-08-03 12:26:21+00:00", "updated_at": "2026-08-03 12:54:47.593718+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-ethics"], "entities": ["Angeliasrl", "norms-mcp-engine"], "alternates": {"html": "https://wpnews.pro/news/show-hn-a-fail-closed-evidence-engine-for-norms-used-by-ai-agents", "markdown": "https://wpnews.pro/news/show-hn-a-fail-closed-evidence-engine-for-norms-used-by-ai-agents.md", "text": "https://wpnews.pro/news/show-hn-a-fail-closed-evidence-engine-for-norms-used-by-ai-agents.txt", "jsonld": "https://wpnews.pro/news/show-hn-a-fail-closed-evidence-engine-for-norms-used-by-ai-agents.jsonld"}}