This is a formats and rules document. You do not need it to hand work to your agent; read it when you build a tool that handles the records or need the exact behavior. New here? Start with Understanding Jaekit.
On this page
- 1. Declaration and input
- Declaration example (checks/declaration.json)
- 2. Per-invocation report
- 3. Classification and retention
- 4. Actual reference integration
- Reference configuration example (checks/reference.json)
- 5. Runner adapters
- Declaration
- Output location
- Marker
- Targets and attempts
- Results of input failures
- Supported output
- Representative forms
Contract: change check results
한국어 · English
This is a translation of the Korean original. Where the two differ, the Korean original decides.
A change condition under run-rules/3 links the baseline and the current check to the same declaration. This contract does not treat an ordinary nonzero exit as the intended requirement violation. The role of existing runs under /1 and /2, and of ordinary maintain, manual and task checks, stays as it is. New starts use /3 (run record).
1. Declaration and input
Before running, the check author declares the required targets and the expected violations in a JSON file and links it from the ## 결과 계약 (result contract) table in PLAN.
## 결과 계약
| ID | 선언 경로 |
| --- | --- |
| AC-1 | checks/declaration.json |
The declaration file and every file in producer_paths go in the condition table's 검사 경로 (check paths). All of them must be tracked regular files at repository-relative paths, and symlinks are not used. The current bytes of the declaration and of the producer and configuration files must equal HEAD. Even when they are inside another execution document exclusion path, uncommitted content cannot change the meaning of the classification. The baseline overlays these files from HEAD onto the base commit. Dependencies and environment of the temporary worktree are not installed automatically.
The declaration and reference configuration below are a hypothetical greeting example. They are not real user data.
Declaration example (checks/declaration.json)
{
"schema": "check-declaration/v1",
"producer": "jaekit-reference",
"producer_version": "1",
"producer_paths": ["tools/check-result-reference.py", "checks/reference.json"],
"targets": [{"id": "greeting", "violation": "missing-greeting"}]
}
producer, producer_version, and a target's id and violation are identifiers of 1 to 128 ASCII alphanumerics, underscores, dots, colons, slashes and hyphens, starting with an ASCII alphanumeric. There are 1 to 128 producer paths and 1 to 1024 targets. Each id in targets is unique, and every target is required. violation is the identifier to observe when that requirement is not met. The declaration, the targets, the result contract, and the producer version and content are part of the evidence identity. Even when the command string is the same, if these inputs change, earlier evidence and completion are not reused as they are.
2. Per-invocation report
ha passes the following environment variables to the check process.
| Variable | Meaning |
|---|---|
HA_EVIDENCE_PATH |
Local file to write this invocation's structured report to |
HA_EVIDENCE_INVOCATION |
Identifier of this invocation |
HA_EVIDENCE_DECLARATION_DIGEST |
Digest of this declaration and the producer inputs |
The producer links the invocation identifier and digest into the report unchanged. A file from an earlier invocation, or a report for another declaration, target or producer, is not valid. stdout/stderr stay in the existing local run log and do not replace the report file.
{
"schema": "check-result/v1",
"invocation": "<HA_EVIDENCE_INVOCATION>",
"declaration_digest": "<HA_EVIDENCE_DECLARATION_DIGEST>",
"producer": "jaekit-reference",
"producer_version": "1",
"attempts_complete": true,
"observations": [{"target": "greeting", "attempt": 1, "status": "violation", "violation": "missing-greeting"}]
}
status |
Observation and additional fields |
|---|---|
pass |
The target ran and passed. Do not include violation or reason |
violation |
An actual assertion failure. Put the observed violation identifier in violation |
error |
An error that prevented the check. reason is one of dependency_missing, collection_error, import_error, compile_error, browser_start_error, setup_error, execution_error, permission_denied |
skip |
The target did not run. reason is skipped or not_selected |
The reference producer observes each target once, without internal retries. To report retries under the common contract, attempts_complete: true and the full attempt history for each target, increasing from 1 without gaps, are required. There are at most 4096 observations in total. When only the last attempt remains, a number is duplicated or the order is reversed, the result is inconclusive with attempt_history_invalid; without the completeness flag, it is inconclusive with attempt_history_missing. When pass and violation are both present for the same target, the adverse result is kept as fail with inconsistent_attempts, even if the final exit is 0. An integration that cannot obtain retry information is unsupported or inconclusive.
The declaration and the report must be JSON objects of 1 MiB or less; duplicate keys, unknown fields, trailing data and unsupported schemas are refused. The report file must also be a regular file. An unsupported ordinary script can record actual attempts without a declaration, but it does not obtain positive evidence for a /3 change.
3. Classification and retention
| Stage | Stored result |
Meaning |
|---|---|---|
| Current check | pass |
Normal exit and all targets passed |
| Current check | fail |
An actual assertion failure |
| baseline | fail_as_expected |
A declared violation observed with an abnormal exit, and no other failure, error or non-run |
| baseline | unexpected_pass |
All targets pass even in the base state |
| baseline | fail |
An actual assertion failure other than the intended one, or a pass/violation inconsistency among internal attempts |
| Both | error |
Process start, timeout, signal or output storage error, or a reported environment error |
| Both | unknown |
Cannot be classified because something is unsupported, missing, conflicting, not run and the like |
The reason in the compact evidence is a classification reason code. For example, unsupported_declaration, report_missing, report_binding, attempt_history_missing, attempt_history_invalid, target_missing, target_skipped and report_exit_conflict explain an inconclusive result. environment_reported is an environment error, and unexpected_assertion is a failure other than the intended one. Raw messages are not put in reason codes.
A current pass needs both a normal exit and an actual pass of every declared target, and the full internal attempt history must also have no adverse observation. A baseline expected failure needs an abnormal exit, observation of all declared targets, and at least one declared violation. It is not enough when environment errors, non-runs or assertion failures other than the intended one are mixed in. An actual assertion failure other than the expected violation remains an adverse failure.
Process start failure, timeout, termination by an external signal, and import, compile, collection or browser startup failure are not expected failures in themselves. On the other hand, a runnable check that directly observes the absence of a feature and reports it as the declared violation can be valid. Zero targets, all targets skipped, a missing, corrupt or unsupported report, and a conflict between exit and report are not promoted to success or expected failure either.
When an environment error or a skip occurs together with an assertion violation, the run as a whole can be error or unknown. The per-target adverse observation in the compact evidence still remains. A later normal run on the same input does not hide that assertion violation.
Errors and inconclusive results both count toward the existing error retry limit. On the same evidence input, a later pass does not erase an earlier adverse assertion result, an unexpected baseline pass or an exceeded error history. Renaming the check kind does not increase the retry allowance. Actual check attempts and usage remain whether or not the result is adopted, and baselines count too. Refusals of input or of a dirty tree before the run, and failure to prepare the temporary worktree, are distinguished from checks that ran.
When the log disappears or fails to be stored after an actual attempt, error with process_output and the usage are recorded if the record store can still be written. In this case output_path and output_digest are empty, and the raw output may not be viewable. If storing the record itself failed, it is not reported as success, and for a failure after the write, whether it was applied may be uncertain.
The record stores compact evidence in check-evidence/v1: result, reason, declaration_digest, report_digest, invocation, targets. Only target observations go in; the actual values of checked files and raw logs are not copied. The current state is rebuilt from this evidence, so a missing raw log or a change in the current time alone does not change the judgment. The executor can write the declaration and the report. The assurance level stays local, and it does not guarantee an independent understanding of the requirements or third-party verification.
Mixed results and failure attribution of the goal, review and gap producers used for repository development, and their version 2 target makeup, are defined separately in the repository producers contract. That does not change this contract's report format or the installation contract of the general reference producer.
4. Actual reference integration
The reference producer uses only the Python 3 standard library. Copy it into the project that needs it, track it, and put it in the check paths together with the declaration and configuration. It does not automatically install other runners or dependencies. This does not mean the reference producer contains pytest, Vitest or Playwright integration. Adapters that read runner output directly are in §5.
Since v0.1.3, the direct installation and Homebrew provide this producer together with the synthetic example. Without a development checkout, you can copy them from the release path in Get the reference check. Python is needed only when choosing this producer and is not a common required dependency of Spec and Seal.
Reference configuration example (checks/reference.json)
{
"checks": [{
"target": "greeting",
"path": "src/greeting.txt",
"equals": "hello\n",
"violation": "missing-greeting",
"missing": "violation"
}]
}
missing: violation declares that this check directly observes the absence of the target file as a requirement violation. When it is omitted or error, a missing file is an environment error. The optional requires lists file paths that this check's environment needs. When such a file is missing, the result is dependency_missing, and it is not turned into a requirement violation. Actual file content is not put in the report.
Use the following command and check paths in the condition table. Do not create the environment variables by hand to use a standalone producer's report as completion evidence.
| ID | 종류 | 검증 명령 | 검사 경로 | Task |
| --- | --- | --- | --- | --- |
| AC-1 | change | `python3 tools/check-result-reference.py checks/declaration.json checks/reference.json` | checks/declaration.json, checks/reference.json, tools/check-result-reference.py | T001 |
In a base state where src/greeting.txt is missing or has another value, the declared violation is observed. When the current code stores hello and a newline, the same target passes. This example does not stop at injecting the declaration into the judge; it reads the actual file in a separate process. The check author must decide the actual behavior and verification targets of the project it is applied to.
When CRLF/LF conversion of check inputs makes the actual bytes differ from HEAD, the input is refused even if Git sees the tree as clean. The newline difference is diagnosed and checkout conditions are suggested, but files, Git settings and existing digests are not changed automatically, and normalized bytes are not accepted as the same evidence.
5. Runner adapters
A change condition under run-rules/3, run-rules/4, run-rules/5, run-rules/6 or run-rules/7 can link a target to one existing test through a runner adapter instead of a producer. Only an executable whose ha capabilities has check-adapter/v1 in formats.check_declaration and runner-adapters/v1 in features supports this. Adapters are optional. The format and meaning of producer declarations (§1 to §4) stay as they are; without support, use the reference producer or a dedicated producer. /1 and /2 goals, and maintain, manual, task and EX-n checks, are judged by exit code as now even when the result contract has an adapter declaration, and placeholder words are not replaced.
Declaration
The 결과 계약 table and the 검사 경로 rules are the same as in §1. An adapter declaration has no list of producer files. Below is a hypothetical greeting example.
{
"schema": "check-adapter/v1",
"adapter": "go-test-json",
"targets": [{"id": "greeting", "violation": "greeting-missing",
"test": {"package": "example.com/greet/cmd/greet", "name": "TestGreetsWithHello"}}]
}
adapter |
Verification command | test |
|---|---|---|
go-test-json |
go test -json … as is. ha reads its standard output |
package (import path) and name (the full test name that go test -json reports; a subtest is TestX/case) |
junit-xml |
A runner command that writes JUnit XML. It receives the output location through the notation below | classname (empty allowed) and name |
There are 1 to 1024 targets, and id and violation are identifiers as in §1. ha lint shows the following as evidence_invalid: an unknown adapter, a target without test, a name field that does not fit the adapter, an empty name, a name containing control characters (C0, DEL, C1), a name over 1024 bytes, a Go package path containing whitespace, a name that does not fit the Go test name format (the part before the first / is a Go identifier that starts with Test, Fuzz or Example and whose first character after that prefix is not lowercase, and there is no whitespace anywhere in the name; subtest names can contain /, so empty segments as in TestX/a//b are allowed), two targets linked to the same test, a violation starting with jaekit: (reserved), and a violation ending with : or . (it cannot be read from a marker). The violation of a producer declaration (check-declaration/v1) may still end with : or .. Unknown fields, duplicate keys, trailing data and more than 1 MiB are refused as in §2.
The evidence identity includes both the declaration bytes and the version of the adapter's judgment meaning. When an adapter comes to judge the same output differently, that version goes up, and earlier evidence from that adapter is not reused (run record).
Output location
For a junit-xml condition, each run creates a new private directory under ha/runs/<goal>/ in the Git directory and sets junit.xml in that empty directory as the output location. {HA_JUNIT_XML} in the command arguments (it may be part of an argument) is replaced with that path, and the same path is also passed in the environment variable HA_JUNIT_XML. The record's argv and command digest are those of the command before replacement, and the path is not kept in the record. Files from earlier runs and files of the same name in the working tree are not read. Example:
| AC-2 | change | `node --test --test-reporter=junit --test-reporter-destination={HA_JUNIT_XML} test/greet.test.mjs` | test/greet.test.mjs, checks/AC-2.json | T001 |
For a go-test-json condition, the command's standard output is captured separately and read to the end. When a child process keeps standard output open after the command ends, it is read for at most 5 more seconds, and if it has not ended by then, the truncated output is not judged (process_wait in the table below). This read happens before ha cleans up remaining child processes, so it is the same on every operating system. The log keeps standard output and standard error together as now, and ha log shows it. Standard error (module download notices, vet warnings, raw build errors) is not used for judgment.
Marker
A marker is the fixed string jaekit:violation= followed by an identifier. The identifier is the longest run of identifier characters (A-Z a-z 0-9 _ . : / -) immediately following it, with all trailing : and . removed. So the identifier of jaekit:violation=greeting-missing: got "hi" and of jaekit:violation=greeting-missing. is greeting-missing, and : and . in the middle (a.b:c) stay. Matching is case-sensitive, and when the identifier after removal is empty, longer than 128 characters or does not start with an alphanumeric, it is not a marker. Markers are read only from the first line of a failure message, at any position within that line.
- Go: in the linked test's own output, each line whose
OutputTypeiserroris the first line of oneError- orFatal-family call.error-continuelines, lines without a type (t.Log, standard output written directly) andframelines are not first lines of a failure message. - JUnit: for each
failureelement of the linked testcase, it is the first line of themessageattribute when that attribute exists (even if empty), and otherwise the literal first line of the element body.system-out,system-errand the body from its second line on (traceback, stack trace, source excerpts) are not read.
When the linked test fails, the first line of every failure message has a marker, and all those markers have the same identifier, that identifier is the observed violation. When there is no failure message, any one of them lacks a marker, or different identifiers are mixed, it is an unmarked failure and the observed value is jaekit:unmarked-failure. Only when the observed violation equals the declared violation is it the declared violation. Other identifiers and unmarked failures are unexpected_assertion. A declared violation cannot start with jaekit:, so an unmarked failure never becomes any declared violation.
Limit. A case where a compared value or repr contains the marker string and is printed on the first line is not distinguished. Use the marker only in the failure message of an assertion that checks the declared property, and do not put it in compared values or output values.
Targets and attempts
- One target links to one test. Names must match exactly; regular expressions and prefix matches are not used.
- Go tests are found by package import path and full test name. Linking to a subtest observes that subtest; linking to a parent test looks only at the parent test's own failure messages. A parent test that failed only because a subtest failed has no failure message, so it is an unmarked failure. When the linked test runs several times in one command (
-count), the runs become attempts 1, 2, … in order. - JUnit matches a testcase's
classnameandnameexactly. When the declaration'sclassnameis empty, both a testcase without aclassnameattribute and a testcase withclassname=""match, and a testcase with anotherclassnamedoes not. When two testcases with the samenameboth match this way, it is a duplicate of the same testcase. Thefileattribute is not used. JUnit XML has no attempt history, so when two or more testcases are linked or retry elements (rerunFailure,rerunError,flakyFailure,flakyError) are present, the result is inconclusive. - Results of tests that are not linked are not target observations. In a current check, when all linked tests pass but the command fails, it is
report_exit_conflict. In a baseline, failures of tests that are not linked do not change the target judgment.
Observations are classified exactly by the rules of §3 and stored as a check-evidence/v1 summary. report_digest is the digest of the runner output that was read. The runner's failure messages, test output and XML content are not put in the record or the reason codes.
Results of input failures
The following table is the complete list. Output forms outside the list (in the package events of the linked test, a Go action the adapter does not know (other than start, run, pause, cont, pass, fail, skip, output, bench, build-output, build-fail, attr and artifacts, which Go 1.27 go test -json writes) or an OutputType value (other than error, error-continue, frame) appears, or an output event without Output appears; an unsupported child element of the linked testcase (namespaced elements included), a combination of skipped with failure or error, or an element inside failure) are observation_invalid. None of them is a pass, an expected failure or the declared violation.
| Case | Result (same for current and baseline) | Reason |
|---|---|---|
| The runner cannot start (not on PATH, no execute permission) | error |
process_start |
Hit the ha time limit |
error |
process_timeout |
| Terminated by a signal | error |
process_signal |
| Failed to store the command output | error |
process_output |
After a go-test-json command ends, a child process keeps standard output open for more than 5 seconds. The exit code and the output read up to then are not used for judgment. When the command ended by a signal, the signal row above takes precedence |
error |
process_wait |
| The package of the linked Go test does not build | error |
environment_reported, target compile_error |
| The linked Go test started but has no end record (pass, fail, skip) (abnormal exit of the test binary, Go's own test timeout) | error |
environment_reported, target execution_error |
The linked testcase has an error element. The same holds when failure is also present |
error |
environment_reported, target execution_error |
| The linked test is not in the output (name mismatch, not selected, only other names because collection failed) | unknown |
target_missing |
The linked test was skipped (Go skip, JUnit skipped) |
unknown |
target_skipped |
| No runner output. Go: no events on standard output. JUnit: no file at the announced location, or not a regular file (directory, symlink) | unknown |
output_missing |
Runner output is corrupt. An unparsable JSON line, an empty line, truncated JSON, non-UTF-8 bytes, an XML syntax error or truncation, a non-UTF-8 encoding declaration, a DTD or entity declaration, two roots or a root that is not testsuites or testsuite, duplicate attributes |
unknown |
output_corrupt |
Runner output exceeds the size limit (64 MiB of go test -json standard output, 16 MiB of JUnit XML). The truncated first part is not judged |
unknown |
output_too_large |
| Duplicate of the same JUnit testcase, retry elements | unknown |
attempt_history_missing |
| Exit code and observation disagree (nonzero although all targets pass, 0 although there is a violation or error) | unknown |
report_exit_conflict |
As in the current rules, an exit code conflict applies before environment errors and skips. For example, when there is an error element but the runner exits 0, the result is unknown (report_exit_conflict), and the target observation remains. When different pre-observation reasons overlap across several targets, one is chosen in the order attempt_history_missing, observation_invalid, target_missing.
Supported output
go test -jsonadds the output type (OutputType) to failure message lines starting with Go 1.27. When the package events of the linked test have noOutputTypeat all (a Go 1.26 or earlier toolchain), a linked test that failed in that package is an unmarked failure, andha checkprints, after the result line, onenotice: <조건> go-test-json: …line that names that package (and mentionsOutputType).- JUnit XML is UTF-8 (BOM allowed) and has one root element (
testsuitesortestsuite), and testcases may be at any depth. Namespaced elements (ext:failure,ext:testcaseand so on) are not JUnit elements. They do not match as testcases, and inside a linked testcase they are unsupported children. The forms confirmed once with actual output are the Node.js 26.11.0 built-in runner (node --test --test-reporter=junit), pytest 9.1.1 (--junitxml=…), Vitest 5.0.5 (--reporter=junit --outputFile=…), and Jest 30.5.2 with jest-junit 17.0.0 (JEST_JUNIT_OUTPUT_FILE). In that output, passes, marked failures, unmarked failures, skips and a pytest fixture error (error) were judged as in the representative forms table. With default settings, pytest writes the module path (tests.test_greet) inclassname; Vitest writes the file path inclassnameanddescribe > testinname; and jest-junit writesdescribe testin bothclassnameandname. pytest and Vitest failures are read from the first line of themessageattribute, and jest-junit failures, which have nomessageattribute, from the first body line (Error: …). The repository's checks confirm this with synthetic documents of the same shape, without installing these runners. Output from other versions and user settings has not been checked.
Representative forms
These are representative examples of an open-ended input. The rules apply in the same way to forms not in the list.
| ID | Adapter | Form | Expected |
|---|---|---|---|
| G1 | go | The linked test passes and t.Log has a marker |
Target passes |
| G2 | go | Fails with t.Fatalf("jaekit:violation=<선언 id> …") |
Declared violation |
| G3 | go | Marker in t.Log, then fails with an unmarked t.Fatal |
unexpected_assertion |
| G4 | go | t.Error twice, only one marked |
unexpected_assertion |
| G5 | go | Marker only on the second line of a multi-line error | unexpected_assertion |
| G6 | go | Panic inside the test | unexpected_assertion |
| G7 | go | A subtest fails with a marker; linked to the parent test | unexpected_assertion |
| G8 | go | Linked to a subtest, and that subtest fails with a marker | Declared violation |
| G9 | go | The overlaid test file calls a function missing from the base commit, so the build fails | error, compile_error |
| G10 | go | The test binary ends while the linked test is running (os.Exit, crash) |
error, execution_error |
| G11 | go | With -count=2, one pass and one declared violation |
fail, inconsistent_attempts |
| G12 | go | The same test name in two packages, linked in only one package | Only the linked package's result |
| G13 | go | A non-JSON line or truncated JSON on standard output | unknown, output_corrupt |
| G14 | go | Nothing on standard output; text only on standard error | unknown, output_missing |
| G15 | go | Fails with a marker on a toolchain that does not mark the output type | unexpected_assertion, one notice: line |
| G16 | go | A : right after the marker, as in t.Fatalf("jaekit:violation=<선언 id>: got %q", …), or a message that ends with jaekit:violation=<선언 id>. |
Declared violation |
| J1 | junit | A testcase without children | Target passes |
| J2 | junit | A failing pytest assert …, "jaekit:violation=<선언 id>" |
Declared violation |
| J3 | junit | A pytest assert failure without a message. Another source line containing a marker appears in the traceback body | unexpected_assertion |
| J4 | junit | A failing Vitest expect(값, "jaekit:violation=<선언 id>"). The message's first line is jaekit:violation=<선언 id>: expected … |
Declared violation |
| J5 | junit | A failure without a message attribute whose first body line is Error: jaekit:violation=<선언 id> |
Declared violation |
| J6 | junit | A fixture setup error appears as an error element |
error, execution_error |
| J7 | junit | A skipped element |
unknown, target_skipped |
| J8 | junit | No tests collected (empty testsuite) | unknown, target_missing |
| J9 | junit | Two of the same testcase | unknown, attempt_history_missing |
| J10 | junit | A failure with a marker only in system-out |
unexpected_assertion |
| J11 | junit | No file at the announced location, a truncated file, a DTD declaration | unknown (output_missing, output_corrupt) |
| J12 | both | The same output with CRLF line endings | Same result as LF |
| J13 | junit | The declaration's classname is empty, and the testcase has classname="" or no classname |
That testcase's result |
| J14 | junit | The declaration's classname is empty, and there are two testcases with the same name, one without classname and one with classname="" |
unknown, attempt_history_missing |
contracts/check-result.en.mdBuilt from bcaddb7603e8