The run folder and run.json¶
.obelize/runs/<run-id>/ is the evidence a run leaves behind; run.json is its
index. tests/unit/test_run_folder_contract.py fails when this file, the models
in src/obelize/models/ and .github/workflows/e2e.yml disagree.
Layout¶
.obelize/
.gitignore `*`, so git ignores everything in this folder
latest the id of the most recently completed run
runs/
20260917T142530Z-3f9a1c72/
run.json metadata and the index (this file's subject)
findings.json every finding, with verdict, bail and caused_by
plan.json the planned edits, whether or not they were written
(`plan` and `apply` only; a scan has no plan)
patch.diff unified diff, git-apply compatible
packs/<provider>/<slug>/ one folder per pack the run used, in `packs[]`:
pack.yaml the pack, copied verbatim
pack.sha256 its hash
REPORT.md the human-readable report
snapshots/before/… files as they were before the patch
snapshots/after/… files as they were after the patch
verify/… command output logs, junit XML, verify.json
model/model.json what a model was asked, and the rows it was not
model/proposals-<n>.json one per consultation: context range, prompt hash,
token counts, the proposal and whether it passed
undo.json written if the run was reverted
journal.json only while an apply is writing, or after one was
interrupted: the rows it meant to write
What each file may contain is in PRIVACY.md. Every mode writes
run.json, findings.json, packs/ and REPORT.md; the
rest only when they apply: plan.json and patch.diff on plan and apply,
snapshots/ on apply for the files it wrote, verify/ when a verification
command ran and after obelize verify, model/ when a model provider is
configured, undo.json after obelize undo. An absent artefact does not make
the folder malformed.
An apply puts its originals down first. After the tree gate and before any
write, it creates the folder with a snapshots/before/ copy of every file the
plan would write and journal.json (the run id and the plan's file_edits
rows); a link or a refusal there stops the run with the repository untouched. On
finishing it writes run.json, removes journal.json and the unused snapshots,
then writes latest. After an interruption, obelize undo --run <id> works from
the journal and skips a file the run never reached as hash_mismatch: it is
already the original.
.obelize/ is always excluded from scanning, before any user configuration.
scan and fix write .obelize/.gitignore unless something is already at
that name, so a git add -A never picks up a run folder; obelize never edits
your own .gitignore.
obelize verify and obelize undo update an existing folder, never create
one, and never move latest. verify replaces run.json's verify (with no
baseline: no pre-patch tree is left to measure), exit_code and
timings.verify_ms, verify/after/ and verify/verify.json; it keeps
verify/baseline/, and REPORT.md gains a block saying the verdict was replaced. A recorded baseline
that did not pass leaves the folder untouched. undo
adds undo.json and nothing else. The folder may come from someone else's
checkout, so neither follows a link in it: both open directories from
.obelize/ down without following links, refuse a link where a directory
belongs, and write beside a name then rename over it.
A folder is read by the release that wrote it: a 0.1.0 folder fails the schemas of a later release, so undo a 0.1.0 apply with 0.1.0.
.obelize/latest¶
One run id and a trailing newline, so run_id=$(cat .obelize/latest) needs no
trimming. A regular file, never a symlink
(THREAT_MODEL.md). Written last, after run.json, so it
always names a complete run. The last run to finish owns it; a script that
cares passes --run <id>. Obelize never deletes a run folder or latest.
Run id¶
20260917T142530Z-3f9a1c72
^^^^^^^^^^^^^^^^ ^^^^^^^^
UTC, ISO 8601 8 random hex characters
basic format
Pinned as ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{8}$. Lexicographic order is
chronological. No colons, which Windows filenames forbid. The suffix is random,
not a hash or a secret: it separates runs started in the same second.
run.json¶
Top level¶
Modes says which of scan, plan (a fix without --apply) and apply
fill the field. A field that does not apply is null; an array with nothing to
report is empty. Neither is ever absent.
| Field | Type | Modes | Description |
|---|---|---|---|
run_id |
string | all | The folder name. |
obelize_version |
string | all | The writing obelize's __version__. |
mode |
string | all | scan, plan or apply. |
exit_code |
integer | all | The process exit code (CLI.md); known last, so run.json follows every other artefact. |
argv |
array of strings | all | The arguments as received, excluding argv[0], redacted like command output (THREAT_MODEL.md): a credential of a shape it lists reads [REDACTED], one it does not list is kept, so review the folder before sharing it. --trust-repo-config is included. |
python |
object | all | {version, implementation}, for example {"version": "3.12.9", "implementation": "CPython"}. No interpreter path: it names the machine and often the person. |
platform |
object | all | {system, release, machine} from platform. |
git_sha |
string or null | all | HEAD at the start of the run; null when the target is not the top of a git repository. |
git_branch |
string or null | all | The current branch; null when detached or when the target is not the top of a git repository. |
git_dirty |
boolean or null | all | Whether a tracked file anywhere in the repository had uncommitted changes at the start. false outside git, where the dirty-tree refusal cannot protect you; null when git could not say, which refuses an apply as tree_unknown. |
packs |
array | all | One row per pack the run used, in id order; empty when none applied. See packs[]. |
config |
object | all | See config. |
counts |
object | all | See counts. |
file_edits |
array | all | Files written, empty on scan and plan. See file_edits[]. |
refused |
array | all | Planned writes that did not happen; empty on scan and plan. See refused[]. |
idempotent |
boolean or null | apply |
See idempotent. |
withheld |
array | all | One row per withheld finding. See withheld[]. |
verify |
object or null | plan, apply |
See verify. |
model |
object or null | plan, apply |
null unless a model provider was configured. A scan never carries one. See model. |
limitations |
array | all | What the run could not look at. See limitations[]. |
timings |
object | all | The only place a wall clock appears. See timings. |
packs[]¶
| Field | Type | Description |
|---|---|---|
id |
string | <provider>/<from>-to-<to> (PACK_SPEC.md), or inventory/<distribution> for a library listed with scan --library. |
pack_version |
string | The pack content's own semver. |
sha256 |
string | 64 lowercase hex characters, matching packs/<id>/pack.sha256 and the copy in pack.yaml. An inventory has neither: its hash is that of the modules it listed. |
source |
string | bundled, file, or inventory for a library with no pack. No path: it could be absolute and outside the repository. |
blocked |
string or null | runtime_unsupported when the project declares a Python the new distribution does not install on (read from pyproject.toml, setup.cfg, Poetry's key, a Pipfile's [requires] or a string python_requires= in setup.py), legacy_version_unsupported when the legacy distribution is pinned below the pack's floor, unpinned, or undeclared while the code uses it, or new_version_unsupported when a manifest pins the new distribution so that it admits no release the pack writes (pypdf==3.0.0): this pack reports its findings, its rows are withheld for that reason, and it proposes no migration. An unparsable declaration leaves it unblocked. A pack that names its newest legacy release does not block on an unpinned declaration unless the repository shows another release: code or text that names a class or module that release lacks is one (the newest-release decision). The message of a legacy_version_unsupported block adds that a pin moves the release and not the code, so a call to a name the pinned release removed is ported first. |
assumed |
string or null | What the pack took a legacy declaration for where it names no version, or one below the pack's floor that admits it: the pack's from.newest, such as 3.0.1, or, for a pack that names from.since, the range from.version, such as >=0.27.0,<1, where the code uses a name only that range defines. null when nothing was assumed: a declaration that already reaches the floor (PyPDF2>=3) is not, and neither is any run of a pack that names neither field. The terminal and REPORT.md say so too. |
When several packs ran, each ran over what the ones before it wrote, in id order, so an edit's line
is in the file as its own pack saw it, and a rule_id is <pack id>:<rule id>.
config¶
| Field | Type | Description |
|---|---|---|
source |
string | file when .obelize.yml was read, defaults when it was absent. |
include |
string | The effective pattern, command line merged over file. |
exclude |
array of strings | The effective user exclusions; the always-excluded set is in CLI.md. |
max_file_bytes |
integer | The effective limit; a larger file appears in limitations. |
counts¶
Integers summarising findings.json, never a substitute for it.
| Field | Description |
|---|---|
files_selected |
Files the walker chose. |
files_parsed |
Of those, the ones both parse gates accepted. |
findings |
Total findings, all kinds. |
eligible, auto, needs_review, unsupported, not_a_usage |
The status split, summing to findings. |
warnings |
Edits carrying a warning code; outside the split, since a warning withholds nothing. 0 on a scan. |
A scan reports the §10 scan-status split (eligible:
nothing withheld it); plan and apply report the
§3 verdict split (auto: rewritten). The unused name is
0.
file_edits[]¶
What was written; the plan is plan.json. Empty means nothing in the
repository changed.
| Field | Type | Description |
|---|---|---|
path |
string | Repository-relative, forward slashes on every platform. |
before_sha256 |
string | The file as read; its name under snapshots/before/. |
after_sha256 |
string | The file as written. obelize undo reverts a file only while its hash still equals this; obelize verify reports tree_changed otherwise. |
hunks |
integer | Hunks this file contributes to patch.diff. |
rules |
array of strings | The pack rule ids behind the edit, sorted. Empty exactly when proposals is not. |
proposals |
array of integers | The model/proposals-<n>.json behind the edit; at most one. Never beside rules: a model is asked only about a file the rules left alone. |
idempotent¶
true when an apply wrote no file (file_edits empty); null on scan and
plan. It describes bytes, not success: an apply where everything bailed is
true too. e2e.yml pairs it with git diff to check that a second apply
changes nothing.
refused[]¶
Planned writes that did not happen; a reviewer reads this first.
limitations[] is what the run could not look at. Without this
array a refused apply would read as a repository that needed no work.
| Field | Type | Description |
|---|---|---|
code |
string | A SCAN_VOCABULARY.md §13 value. |
path |
string or null | Repository-relative. null exactly for tree_dirty and tree_unknown. |
detail |
string | One sentence a person can act on. |
withheld[]¶
| Field | Type | Description |
|---|---|---|
path |
string | Repository-relative. |
line |
integer | 1-based. |
symbol |
string or null | The resolved legacy qualified name; null on a parse_error row. |
bail |
string | A SCAN_VOCABULARY.md §4 code. |
caused_by |
array of strings or null | Set exactly when bail is file_not_fully_migrated (§6). |
It repeats part of findings.json so that run.json alone, which the
bug-report template asks for, says what was withheld and why.
verify¶
null on a scan; on plan and apply always set, even when nothing ran (a dry
run records not_run / dry_run). commands is empty whenever status is
not_run.
| Field | Type | Description |
|---|---|---|
status |
string | pass, fail, inconclusive or not_run: the worst of the commands', fail > inconclusive > not_run > pass. |
reason |
string or null | From the vocabulary in CLI.md, which maps both to the exit code. null only on pass. |
commands |
array | One entry per command, in the order they ran. See verify.commands[]. |
baseline |
object or null | status, reason and commands before the patch, one level deep. A failed baseline makes the run inconclusive / baseline_failed, never fail. A write refused after the baseline ran keeps it under not_run / no_changes_to_verify. |
edited |
array | The files the run wrote whose bytes were not the run's when obelize verify --current checked the tree as it stood, sorted; empty for any other verification. |
verify.commands[]¶
One row per command that ran, under verify and under verify.baseline.
| Field | Type | Description |
|---|---|---|
command |
string | The command as given, redacted the same way as argv; the process still ran the text you typed. |
source |
string | cli, user_allowlist or repo_config: its trust rung. A pack's verification.suggestions are never executed. |
status |
string | This command's verdict; only exit code 0 passes. |
reason |
string or null | Set exactly when it did not pass: command_failed, command_not_executable or timeout. |
exit_code |
integer or null | null when no process started; negative when a signal killed it on POSIX, and 3221225786 when the deadline ended its job on Windows. A command stopped at its deadline keeps its code, even 0, and reads timeout. |
duration_ms |
integer | |
truncated |
boolean | Whether the output was capped. |
log |
string or null | Path under verify/, relative to the run folder, capped and redacted per PRIVACY.md; null when nothing was printed. |
junit |
string or null | Path under verify/, when a junit XML was produced. |
in_obelize_environment |
boolean | Whether PATH found the program in obelize's own environment, which usually lacks the project's test dependencies; false for a program named by its path. A baseline command with this true that failed or timed out adds a hint, saying which, to the summary and REPORT.md. |
Output is a file, never a field: a log can be a megabyte and run.json goes
into bug reports. verify/baseline/ and verify/after/ each number their
commands from 1 (1.log, any junit report beside it). verify/verify.json is
the verify object alone.
model¶
null whenever model.provider is none, the default. The detail is in
the model/ directory.
| Field | Type | Description |
|---|---|---|
provider |
string | openai_compat, the only v0 value. |
host |
string | The host of base_url, never the full URL, whose path can carry a credential. |
model |
string | The model name sent. |
proposals |
integer | Edits proposed, not questions asked. |
accepted |
integer | Proposals that passed every guard check and were written, which needs --apply with --accept-model. |
tokens_in, tokens_out |
integer | As reported by the endpoint. |
No API key appears anywhere in the run folder, and no raw prompt unless
model.log_prompts is set. A written proposal stays a review item: its row stays
in withheld[], counts do not move, and it earns no exit 0, because the
guard proves an edit safe to apply, not correct.
limitations[]¶
What the run could not look at, so a small finding count never reads as a clean repository.
| Field | Type | Description |
|---|---|---|
code |
string | models.LimitationCode, ten values: SCAN_VOCABULARY.md §11's seven plus file_too_large, input_does_not_parse and internal_error (§4). |
path |
string or null | Repository-relative, when one file is meant. null for unusable_name, whose detail quotes the name. |
detail |
string | One sentence a person can act on. |
For a refused parse, detail names the gate and quotes the parser's message,
never a line of the file (cst.ParserSyntaxError.context and SyntaxError.text
stay out of the run folder). For an internal_error, detail names the exception's
class and never its message, which can quote the file or a path. An unreadable
manifest gets a fixed sentence, because configparser quotes the line.
timings¶
The only wall-clock values, so everything else compares byte for byte between two runs over one input.
| Field | Type | Description |
|---|---|---|
started_at, finished_at |
string | ISO 8601, UTC, to the second, with a Z suffix. |
total_ms |
integer | Measured on a monotonic clock, not subtracted from the timestamps. |
scan_ms, plan_ms, apply_ms, verify_ms, model_ms |
integer or null | null for a phase that did not run; model_ms is set exactly when model is. |
findings.json¶
Byte for byte what obelize scan --json prints. Nothing time-derived (no run
id, timings or absolute path), so two scans of one input are identical.
| Field | Type | Description |
|---|---|---|
obelize_version |
string | The version that produced it. |
packs |
array | One {id, version, sha256} per pack the run used; version is run.json's pack_version. |
counts |
object | files_selected, files_parsed, findings and the scan's status split, as in counts. |
findings |
array | Every finding, in document order. |
A finding¶
| Field | Type | Description |
|---|---|---|
path |
string | Repository-relative, forward slashes on every platform. |
line |
integer | 1-based. |
column |
integer | 0-based, as libcst reports it. |
kind |
string | SCAN_VOCABULARY.md §1. |
confidence_reason |
string | §2. |
symbol |
string or null | The resolved legacy qualified name; the distribution on a manifest finding; null on a parse_error. A mock.patch target is recorded unquoted. |
evidence |
string or null | Always null: source excerpts are kept out of evidence. |
scan_status |
string | §10. |
bail |
string or null | §4. Set exactly when scan_status is needs_review or unsupported. |
caused_by |
array of strings or null | §6. Set exactly when bail is file_not_fully_migrated; sorted, de-duplicated. |
Document order is (path, line, column, kind, symbol), and
src/obelize/models/scan.py refuses any other: libcst yields references in an order
that varies between processes. A finding has no verdict or warning; those
belong to an edit, in plan.json and REPORT.md.
plan.json¶
What the run would write, whether or not it did. Written by plan and
apply; with file_edits[] it defines a dry run:
| Run | plan.json's files[] |
run.json's file_edits[] |
|---|---|---|
| a dry run | everything it would write | empty |
| an apply that wrote it all | everything | the same list |
| an apply that wrote some of it | everything | the subset that landed, with refused[] naming why the rest did not |
No time-derived value and no counts: it lives only beside run.json.
| Field | Type | Description |
|---|---|---|
obelize_version |
string | The version that produced it. |
packs |
array | As in findings.json. |
files |
array | One row per file whose bytes would change, in path order, shaped like file_edits[]. |
edits |
array | Every edit row, written or withheld, sorted by (path, line). |
A planned edit¶
| Field | Type | Description |
|---|---|---|
path, line |
string, integer | Repository-relative, 1-based. |
status |
string | SCAN_VOCABULARY.md §8. |
rule_id |
string or null | <pack id>:<rule id>, the rule behind it; null where no rule claimed the row, and on model_proposed. |
reason |
string or null | §4. Set on needs_review and unsupported, and kept on model_proposed as what the rules refused. |
caused_by |
array of strings or null | Set exactly when reason is file_not_fully_migrated. |
warnings |
array of strings | §5, sorted. |
A file the scan already refused has no row here, since no rule claimed it; so
REPORT.md prints the findings beside the edits, and under Withheld what to change for each reason
the table shows. It also holds what each pack says about the
findings it flagged (Guidance) and what each pack does not handle (Limitations), read from the
packs the run loaded: no field of findings.json, plan.json or run.json carries them. Under
Limitations it also names the files that use the legacy API and that no rule writes, a notebook
with the cells that do and a stub with none, which no field carries either. Under
Dependency manifests it names the lock files beside a manifest pin that was written or would be, or
above it, since obelize does not edit them; that is not recorded in a field either.
patch.diff¶
A unified diff of every file plan.json names, in path order; empty when
nothing would change.
- Split on
\nonly, as git does, sogit applyrestores exact bytes: a CRLF line keeps its\r, a bare-CR file is one line. - Paths quoted as git quotes them, with the
a/andb/prefixes-p1expects. A name with a space is not quoted; its---and+++lines end in a tab. - No
indexline:git applyandpatch -p1accept its absence, and a wrong one would failgit apply --3way. - Never used to write or revert. A write puts down whole content through
fsutil.write; a revert restoressnapshots/before/<sha256>while the file still hashes toafter_sha256. A reverse-applied diff can fail halfway or hit a drifted file.
snapshots/¶
snapshots/before/<before_sha256> and snapshots/after/<after_sha256>, no
extension, written by apply for the files it wrote. Content-addressed, so no
bytes are stored twice; file_edits[] maps each hash to its
path.
The model/ directory¶
Written only when model.provider is not none: model.json is the index,
each proposals-<n>.json the detail.
model/model.json¶
| Field | Type | Description |
|---|---|---|
obelize_version |
string | |
summary |
object | The model object. |
consulted |
integer | Questions asked: the number of proposals-<n>.json files. |
skipped[] |
array of objects | Withheld rows not put to a model: path, line and a skip reason, so the selection can be audited by what it left out. |
model/proposals-<n>.json¶
One per consultation, numbered from 1 in asking order, whether or not
anything was proposed.
| Field | Type | Description |
|---|---|---|
index |
integer | The <n> in the file name. |
path, line |
string, integer | The row asked about. A proposal may edit no other file. |
symbol |
string or null | The legacy symbol at that line. |
bail |
string | What the rules refused with: a SCAN_VOCABULARY.md §4 code. |
context_start_line, context_end_line |
integer | The range sent, 1-based, inclusive. |
prompt_sha256 |
string | Of the exact request body, so a re-asked question shows without either prompt. |
prompt |
string or null | Those bytes; null unless model.log_prompts is true. A prompt is source code. |
provider, model |
string | Who was asked. |
tokens_in, tokens_out |
integer | As reported; 0 when nothing was reported. |
outcome |
string | A proposal outcome. |
failure |
string or null | A provider failure, set exactly when outcome is unanswered. |
refusal |
string or null | A guard refusal, set exactly when outcome is guard_refused. |
detail |
string or null | One line from whichever decided; the endpoint's words are redacted, then cut. |
proposal |
object or null | What came back, unvalidated (path, start_line, end_line, symbol, replaces, replacement, rationale, imports), so a proposal naming ~/.bashrc is refused by name, not lost in a parse error. |
The four Phase 3 vocabularies¶
Closed sets in src/obelize/models/vocabulary.py, published here where a reader meets
them.
A model was not asked (ConsultSkip)¶
skipped[].reason, in the order the checks run.
| Value | Meaning |
|---|---|
not_needs_review |
The row is unsupported: the new SDK has no equivalent to propose. |
not_a_source_file |
A dependency manifest, or a file this run could not write. |
atomicity_only |
The bail is file_not_fully_migrated; the rows in its caused_by are asked about instead. |
pack_refused |
A flag_only rule refused the surface; a model overriding it would move behaviour out of the pack's data. |
consumed_elsewhere |
The bail is configure_consumed_elsewhere: the defect is in the other module. |
context_too_large |
No range holding the whole binding group fits the budget. Nothing is truncated: the "inside what was sent" check would pass a proposal written against something else. |
An adapter could not answer (ProviderFailure)¶
failure: pipeline stages in the order the bytes reach them, not a ranking.
| Value | Meaning |
|---|---|
endpoint_unreachable |
Nothing answered: no route, no listener, a dropped connection, or the deadline passed. |
endpoint_redirected |
A 3xx, refused rather than followed, so the payload and Authorization header never reach a host the run did not print. |
endpoint_refused |
Any other status outside 200–299; its message travels with it, redacted and capped. |
response_too_large |
The body passed the 1 MiB cap. |
response_not_json |
The body is not JSON, typically a wrong base_url. |
response_not_a_completion |
JSON with no choices[0].message.content string. |
answer_not_json |
The content holds no JSON object under any of the three readings. |
answer_not_a_proposal |
An object without the key it was asked for. A bare proposal at the top level is this too: the envelope separates "an edit" from "none". |
proposal_does_not_hold |
EditProposal refuses it: a field it cannot hold, such as an empty path, a missing replaces or a lone surrogate, which no file or report can carry. Unanswered, not refused. |
The guard refused a proposal (GuardRefusal)¶
refusal. Seventeen words in three groups; the first refusal in this order is
recorded, because a hostile proposal trips several and a reviewer needs the
first.
May we touch this file at all:
| Value | Meaning |
|---|---|
path_outside_root |
Absolute, holding a .. component, or not a relative POSIX path. |
path_not_python |
Not a .py file. Manifests are obelize's to write, never a model's. |
path_not_the_consulted_file |
A repository .py path other than the one asked about. |
file_changed_since_read |
The file changed since the plan read it, so the line numbers point elsewhere. |
Is this an answer to the question:
| Value | Meaning |
|---|---|
outside_the_context |
The replaced range is not inside the range sent. |
site_not_replaced |
The range does not contain the line asked about. |
symbol_mismatch |
It names another symbol, or the consultation named none to check. |
replaced_text_differs |
The lines it says it replaces (replaces) are not the lines it names as they were sent, trailing whitespace, line endings and blank lines at the end aside: a range that slipped is not the range that was meant. Also refused when redaction joined lines of what was sent, so that no line number can be trusted. |
Is what it produces Python, and does it bring anything in:
| Value | Meaning |
|---|---|
replacement_too_large |
Over the size limits of a model's replacement: too many lines, too many bytes, or one statement of more than 500 tokens, which libcst's parser may not survive. |
replacement_not_displayable |
A control, format or surrogate character other than tab or line break: the line a reviewer reads is not the line that runs. |
replacement_carries_redaction |
It holds [REDACTED, the placeholder for a secret the model was shown instead of the value, and the lines it replaces hold none: written as it stands it would put the word in the file. |
replacement_not_encodable |
Not writable in the file's own encoding. |
output_does_not_compile |
compile() refuses the result: a syntax error, a nesting too deep to read, or await outside async, which libcst accepts. Asked first, because libcst's parser dies on a deeply nested expression. |
output_does_not_parse |
libcst refuses a result compile() accepted, or the result nests deeper than obelize can walk. |
replacement_runs_past_its_range |
It changes how the following lines read (a trailing backslash or open bracket, or an opened block or string), though it parses and compiles. |
import_outside_the_target |
It imports a module the pack does not target, __import__ and importlib.import_module included, or lists such an import in imports. |
name_outside_the_question |
It uses a name nothing licenses: not in the replaced lines, not bound by a target-module import or by itself, not an allowed builtin; never a double-underscore name. Catches imp = __import__, exec("import os"), open(...), a module the file already imported. |
What became of a proposal (ProposalOutcome)¶
outcome, first match in this order: three about the answer, five about what
the run did with it.
| Value | Meaning |
|---|---|
unanswered |
The adapter raised; failure says why. The row stays needs_review; the next row is still asked. |
nothing_proposed |
The endpoint answered {"proposal": null}, a normal answer. |
guard_refused |
refusal says which guard refused it. |
not_applied |
Accepted; the run had no --apply. |
not_accepted |
Accepted; --accept-model was not given. |
file_already_proposed |
Accepted, and an earlier proposal holds this file. The guard checks a whole file, so a file takes one model edit per run; the next run asks about the rest. |
write_refused |
Accepted, and the apply was refused: the tree or path gate, asked again over the files a model proposed, or a file that changed between that gate and the write. |
written |
On disk; its Edit row reads model_proposed. |
undo.json¶
Written into an existing run folder by obelize undo only. Not a run; no id of
its own. An undo that put no file back leaves an undo.json already there as it is, so a
repeat of a finished undo does not replace the account of what was put back.
| Field | Type | Description |
|---|---|---|
run_id |
string | The run whose file_edits[] and snapshots it used. |
obelize_version |
string | The __version__ that performed the undo. |
exit_code |
integer | 0 when every file was reverted or already held the original, 4 otherwise; derived from files[]. |
argv |
array of strings | The arguments the undo was invoked with. |
undone_at |
string | UTC, to the second, with the Z. |
files |
array | One row per file the run recorded writing, in path order. No count beside it, which could disagree. |
undo.json files[]¶
| Field | Type | Description |
|---|---|---|
path |
string | Repository-relative, forward slashes on every platform. |
outcome |
string | reverted or skipped. |
reason |
string or null | Set exactly when skipped: one of CLI.md. |
recorded_sha256 |
string | The run's after_sha256, which the file must still have. |
current_sha256 |
string or null | What is there now; null when unreadable. |
snapshot |
string | The pre-migration copy under snapshots/before/, recorded even on a skip. |
hash_mismatch is exactly current_sha256 set and unequal to
recorded_sha256. There is no undo --force: the refusal names
both hashes and where the original is.
Stability¶
Every format here is unstable through 0.x
(CHANGELOG.md) and carries no version number; a
change lands with this file, the model and the schema together. verify and
undo validate run.json against the closed model that wrote it, so a folder
from a different obelize may be refused rather than half-read.
run.schema.json, findings.schema.json, plan.schema.json,
undo.schema.json, model.schema.json, proposal.schema.json and outdated.schema.json (the rows of
obelize outdated --json, which no run folder holds) in
src/obelize/schemas/ are generated from the models and committed;
ci / schema-drift fails when one is stale. Every object is closed and every
field required, because every writer writes every field. A digest, a run id, an instant or a name
carries the regular expression its model checks it with as its pattern; the other checks, such as
the name of an environment variable, stay in the models, so the models are authoritative.
What e2e asserts today¶
.github/workflows/e2e.yml runs the flow against examples/gemini-legacy-app
and checks:
| Assertion | Step |
|---|---|
.obelize/latest names a run folder containing run.json |
after fix --apply |
run_id and obelize_version are non-empty strings |
after fix --apply |
mode is apply |
after fix --apply |
packs holds the one requested pack, and its sha256 is 64 hex characters |
after fix --apply |
git_dirty is false |
after fix --apply |
exit_code is 4 |
after fix --apply |
file_edits is empty and idempotent is true |
after fix --apply |
counts.auto is 0, withheld has fourteen rows, and exactly one of them is configure_consumed_elsewhere |
after fix --apply |
verify.status is not_run and verify.reason is no_changes_to_verify |
after fix --apply |
findings.json, plan.json, patch.diff, REPORT.md and the pack's pack.yaml and pack.sha256 all exist |
after fix --apply |
idempotent is true and file_edits is empty |
after the second fix --apply |
tests/unit/test_run_folder_contract.py checks that the workflow reads only
fields specified here and reads every one this table claims.
The example gets no edit: summarize.py and conversation.py run on
summarizer/config.py's configure, so that file is withheld as
configure_consumed_elsewhere. A
more automatic result is a rules change to argue, not a number to update.
tests/e2e/ asserts the same in-process; the workflow runs a built wheel in a
clean interpreter.