# title
Wisp acceptance assertions (TESTS/acceptance.json) — protocol v0

# description
The embedded contract a built phenotype must satisfy. A Wisp carries its own acceptance criteria so that 'it built' and 'it built correctly' stay different claims, and so that the publisher's expectations can be checked by a recipient who never spoke to the publisher.

CHECKABLE WITHOUT EXECUTING THE PHENOTYPE. Every kind here is decided by reading bytes or by parsing HTML with scripting DISABLED. A conforming validator MUST NOT load the phenotype in a JavaScript-enabled context, MUST NOT follow any reference outside the workspace, and performs no network request.

NO PUBLISHER-SUPPLIED REGULAR EXPRESSION IS EVALUATED. This is the change that removed the html_lacks_pattern kind and every `pattern`/`value_matches` field an earlier draft of this schema described. Bounding a hostile pattern by inspection does not work — the defence that was tried admitted `^(a|aa)+$`, which took 1.9 s on 36 characters — so the capability is gone rather than sandboxed. reference/wisp.py's compile_wisp_pattern() now does nothing but raise, and the vocabulary below is closed and linearly evaluated. Adding a kind is a schema-version change, never a publisher's choice.

WHAT PASSING PROVES, AND WHAT IT DOES NOT. Passing establishes PHENOTYPE CONFORMANCE and nothing else. It is not content integrity (that is the manifest inventory), not publisher identity (that requires an out-of-band trust anchor), not user authorization (that was the consent grant), and not safety. THE TEST SET IS WRITTEN BY THE PUBLISHER: a publisher cannot test its own trustworthiness, and an empty or shallow test set produces a green result that means very little. A reader MUST report the number of assertions alongside the outcome so that 'passed 0 of 0' can never read as 'passed'.

SEVERITY. A failing 'must' means the phenotype is NON-CONFORMING; the reader says so plainly and does not present the result as clean — reference/wisp.py refuses to open such a build. A failing 'should' is recorded as a warning. A reader MUST NOT silently downgrade a 'must'.

EVIDENCE STRENGTH IS NOT UNIFORM, and each kind states its own below. Two kinds are STRUCTURAL — visible_text_contains and element_count parse the document, so text hidden in a comment, a script or a style block can never satisfy them. That distinction exists because a counterfeit page passed an earlier byte-grep contract with every required phrase buried in an HTML comment.

CONSTRAINTS THIS SCHEMA CANNOT EXPRESS (validate_acceptance_contract() and run_acceptance() enforce them):
  1. assertions[].id MUST be unique within the document.
  2. Every `target` MUST resolve inside the derived workspace after symlink resolution; an escape is an assertion FAILURE, never a read.
  3. An assertion whose target is missing FAILS. v0 has no 'skip': a check that could not be made is not a check that passed.

VALIDATOR REQUIREMENT: this schema uses 2020-12 `unevaluatedProperties`, so a validator that does not implement it will silently accept malformed assertions.

STATUS: ENFORCED. reference/wisp.py declares this vocabulary as ASSERTION_KINDS, structurally validates a Wisp's contract in validate_acceptance_contract() BEFORE the workspace is claimed, evaluates it in run_acceptance(), and records each outcome in UNFOLD_RECORD.json. tests/test_schema_conformance.py validates the reference Wisp's packed TESTS/acceptance.json against this file with a real Draft 2020-12 validator.

# properties.schema.description
Identifies this document's contract. A validator that does not know this exact string MUST report the assertions as unevaluated rather than guess. validate_acceptance_contract() refuses any other value.

# properties.notes.description
Optional publisher commentary on what the test set does and does not cover. UNTRUSTED PROSE — displayed, never acted on, and never a substitute for an assertion.

# properties.assertions.description
The assertions, evaluated in array order. Bounded at 512 so a hostile Wisp cannot exhaust a validator through the test set itself.

# $defs.line.description
One line of inert text: no control characters, no newlines.

# $defs.prose.description
Multi-line inert text: newlines permitted, other control characters refused.

# $defs.workspacePath.description
The file under test, relative to the derived workspace root. Forward slashes only, at most 8 components, no absolute path, no '.' or '..', no control characters, no leading space and no trailing space or dot in any component. A test may only look at files the unfold produced — it is not a channel for reading the user's machine, and run_acceptance() re-resolves containment through safe_join() before opening anything.

# $defs.assertion.description
One declarative assertion. The base fields apply to every kind; the kind-specific fields are fixed by the matching branch below, and any other property is refused. Text-carrying kinds all name their operand `text`; there is no general-purpose `value` field except where a non-text operand genuinely has no other name (element_count, json_field_equals).

# $defs.assertion.oneOf[].description
The target exists and is a regular file. Evidence: existence only — it says nothing about content, which is why it is never the whole test set.

# $defs.assertion.oneOf[].description
The file's bytes hash to an exact expected digest. Use for assets a phenotype must reproduce unchanged. Do NOT use for generated files: their bytes legitimately differ between readers and projections, and an over-tight digest assertion turns a conforming build into a false failure.

# $defs.assertion.oneOf[].properties.sha256.description
Expected digest of the file's exact bytes.

# $defs.assertion.oneOf[].description
STRUCTURAL. The document's VISIBLE text contains `text`, compared case-insensitively. The document is parsed; comments, <script>, <style>, <template> and <title> are excluded, and the remaining text nodes are joined with single spaces. This is the assertion that proves publisher content actually reached a person's eyes rather than merely reaching the file — text hidden in a comment cannot satisfy it.

# $defs.assertion.oneOf[].properties.text.description
The text that must appear in the extracted visible text. Matching is case-insensitive and whitespace inside the needle must match the collapsed rendering.

# $defs.assertion.oneOf[].description
The file's RAW bytes do not contain `text`, compared case-insensitively. Raw is deliberate and is the one place where it is right: a forbidden literal such as 'unsafe-inline' or 'innerHTML =' must not be permitted to hide in a comment, a script body or an attribute either. Absence in THIS file says nothing about files the assertion was not run against.

# $defs.assertion.oneOf[].properties.text.description
The literal that must NOT occur anywhere in the file.

# $defs.assertion.oneOf[].description
The document carries a <meta http-equiv="Content-Security-Policy"> whose content attribute contains `text`, compared case-sensitively. HONEST SCOPE: a meta CSP is defence in depth, not the primary control. The primary controls are that a Wisp may not contain executable code and that the generated page holds no external reference. A meta-delivered policy cannot express frame-ancestors or sandbox reliably and is enforced only from where it appears, so passing evidences INTENT plus partial enforcement and MUST NOT be reported as proof that the page cannot reach the network.

# $defs.assertion.oneOf[].properties.text.description
Exact substring required in the policy, e.g. "default-src 'none'".

# $defs.assertion.oneOf[].description
STRUCTURAL. The number of `element` start tags found by parsing the document satisfies `op` against `value`. Use to prove structure — that the five panels exist as real elements, that the tab controls exist, that a runtime is present. Counts parsed elements, so a tag written inside a comment does not count.

# $defs.assertion.oneOf[].properties.element.description
Lowercase HTML tag name. A tag name, never a selector: v0 ships no selector engine, because an expression evaluator is exactly what this contract refuses.

# $defs.assertion.oneOf[].properties.value.description
The integer the count is compared against. 'eq' with 0 is how absence of a construct is asserted.

# $defs.assertion.oneOf[].description
The document's source, WITH HTML COMMENTS REMOVED, contains the literal fragment `text` — typically an attribute pair such as role="tablist", or a token that must appear in the inline stylesheet such as prefers-reduced-motion.

HONEST LIMITATION, stated because the kind's name is friendlier than its mechanism: this is a source-text check, not a DOM query. It does not verify that the attribute sits on any particular element, and it will match inside a <script> or <style> body. Comments are stripped first so a counterfeit cannot satisfy it with commented-out markup, but it remains WEAKER evidence than visible_text_contains or element_count, and a safety property MUST NOT rest on it alone.

# $defs.assertion.oneOf[].properties.text.description
The literal source fragment that must be present outside any comment. Compared case-sensitively.

# $defs.assertion.oneOf[].description
THE CENTRAL SAFETY ASSERTION FOR v0. The file contains no src/href introducing an absolute or protocol-relative URL, and no invocation of a network API (fetch(, new XMLHttpRequest, new WebSocket, new EventSource, .sendBeacon(, importScripts(, or an assignment of a remote URL to .src). Detection matches INVOCATIONS rather than mentions, so a page that documents 'this makes no network calls' does not trip it.

HONEST SCOPE: this is a STATIC check of one file. It is strong evidence, not proof of zero egress. Only instrumented observation of the host could support 'no traffic occurred', which is why an unfold record's network.observed is 'unknown' for a reader that only inspected files.

# $defs.assertion.oneOf[].description
A field inside a generated JSON file equals an expected value. Use to prove that declarative state the reader wrote — the unfold record's network declaration, the consent facts, a progress marker — carries the value the publisher expects.

HONEST LIMITATION: a field that is absent reads as null, so an expected_value of null cannot distinguish 'present and null' from 'missing'. Assert a non-null value when the distinction matters.

# $defs.assertion.oneOf[].properties.field.description
Dotted path of object keys from the document root, e.g. 'network.declared'. Object keys only — no array indexing, no wildcards, no JSON Pointer escaping rules to get wrong. A path that does not resolve yields null.

# $defs.assertion.oneOf[].properties.value.description
The expected value, compared by exact equality. Scalars only, and integers only for numbers: the protocol's integers-only discipline holds here so a comparison cannot turn on floating-point formatting.

# $defs.assertionBase.properties.id.description
Stable lowercase identifier, unique within the document. It is what an unfold record's conformance.details[].id refers to, so it MUST NOT be renumbered between Wisp versions if results are to stay comparable. Lowercase is enforced by validate_acceptance_contract() so that an id is a safe key everywhere it is written.

# $defs.assertionBase.properties.kind.description
The assertion vocabulary, identical to reference/wisp.py's ASSERTION_KINDS and to the unfold record's assertionKind. Closed by design: every kind is decidable by reading or parsing, none requires executing the phenotype, and none evaluates a publisher-supplied expression.

# $defs.assertionBase.properties.target.description
The file under test, relative to the derived workspace root.

# $defs.assertionBase.properties.message.description
What a human is told when this assertion FAILS. Authored by the publisher so a failure is explained in the publisher's own terms; UNTRUSTED PROSE, rendered inert, and never treated as a reason to continue past a failure — it is copied into the unfold record and displayed, never acted on.

# $defs.assertionBase.properties.severity.description
'must': failure means the phenotype is non-conforming and the reader says so plainly. 'should': failure is recorded as a warning. Any assertion bearing on safety SHOULD be 'must'; a 'should' on a safety property is a smell a reviewer ought to question.

# $defs.assertionBase.properties.rationale.description
Optional. Why this assertion exists — what it protects. Documentation for a reviewer; never evaluated.

# $defs.digest.description
Lowercase hex SHA-256 with a mandatory 'sha256:' prefix.