From 226f0dd8aed813a5d468a4944cb93ebb8feeea2a Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 29 Oct 2026 12:22:55 +0100 Subject: [PATCH 1/2] Vendor the MTHDS core error registry 8.0.1 Vendor the registry snapshot from the standard's v8.0.1 release (commit fdfedc5356ea88a9647a262711dd477c4b1c7a71) and regenerate mthds/protocol/problem/registry.py from it. The release rewords the titles of the codes a run can raise, and the next steps they give a reader other than the method's author, for someone following a run of a method they did not write; no code, domain, retry verdict, step kind or status changes. CORE_ERROR_REGISTRY_VERSION moves to 8.0.1, and with it the version pins in tests/unit/test_versions.py and the shared reading cases, none of which uses a reworded code. The docs name the registry 8.0.1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TF9N1QfNEKCTsX1Q6NCf1 --- CHANGELOG.md | 1 + docs/runners.md | 2 +- docs/versioning.md | 2 +- mthds/protocol/problem/registry.py | 86 +++++++++++-------- mthds/protocol/protocol.py | 2 +- tests/data/problem-reading-cases.json | 2 +- tests/unit/test_versions.py | 2 +- .../mthds-error-registry.json | 72 ++++++++-------- vendor/mthds-error-registry/source.json | 8 +- 9 files changed, 94 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666fffd..ffeaffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed +- **Core error registry `8.0.1`**: `mthds.protocol.problem.registry` is generated from the standard's core error registry `8.0.1`, vendored from the standard's `v8.0.1` release, and `CORE_ERROR_REGISTRY_VERSION` moved from `8.0.0` to `8.0.1`. The titles of the codes a run can raise, and the next steps they give a reader other than the method's author, are reworded for someone following a run of a method they did not write: `pipe.output_invalid`, for one, is now titled "A step's result does not have the structure the method declares" where it read "A pipe's output does not fit its concept". No code, domain, retry verdict, step kind or status changes. - **Dev tooling: the development Python is pinned in `.python-version`**: the Makefile, uv and CI read the version from that file, `PYTHON_VERSION= make install` still builds `.venv` on another supported version, and each job of the CI matrices now fails unless its environment ran the version the job names. Nothing shipped changes. ## [v0.25.0] - 2026-10-18 diff --git a/docs/runners.md b/docs/runners.md index 5552e05..0bb8058 100644 --- a/docs/runners.md +++ b/docs/runners.md @@ -127,7 +127,7 @@ On a route that creates a run, a `5xx` that is no problem document, such as a ga - **As received.** Every member is kept as the runtime sent it, and a member the protocol does not declare, such as a runner's `request_id`, rides `problem.model_extra`; `problem.as_wire()` gives the members back in JSON types. `status` is filled from the answer when the body leaves it out and the answer's status is one HTTP defines, `100` to `599`, and `retry_after` from the `Retry-After` header when the body gives none and its retry verdict is `yes`. The header is read as seconds in ASCII digits, up to `2**53 - 1`, or as an HTTP date in exactly the three forms RFC 9110 (section 5.6.7) has a recipient accept, the IMF-fixdate `Sun, 06 Nov 1994 08:49:37 GMT`, the obsolete RFC 850 form `Sunday, 06-Nov-94 08:49:37 GMT` and the asctime form `Sun Nov 6 08:49:37 1994`; a header the client cannot read, such as one written with other digits, in another date form, naming no real moment or naming a leap second that carries it past the end of the year 9999, gives no delay. - **Typed where the protocol types it.** `code`, `domain`, `retry`, `next_step`, `params`, `location`, `errors` (a `ProblemItem` per fault, each with its `suggested_fix`) and `pipeline_run_id` are typed. A body whose declared member has the wrong type, such as a `status` written as a string, a `null` member or a code outside the code pattern, is no problem document, and reads as `client.response_unreadable`. JSON is decoded as every reader of the protocol decodes it: the constants `NaN` and `Infinity`, a number past the range of a double such as `1e999`, which would read as an infinity, and a body nesting arrays and objects deeper than `MAX_BODY_NESTING` (512 levels, the body's own object or array included, the limit the TypeScript client pins too, checked before the body is parsed) are not JSON, and an integral number reads as an integer. `problem.as_wire()` writes back every problem read within that limit, where pydantic's own `model_dump` refuses a member nested a few hundred levels deep. -- **Open for what it does not know.** A domain, a retry verdict, a step kind or a fix's safety the protocol does not list stays the plain string received. `effective_domain` and `effective_step_kind` read such a value by the code's family, as the generated registry declares it: the domain every core code of the family shares, and the step kind every one of them gives to the actor and to any other reader alike, since a reader that does not know a step kind cannot tell whose step it is. A family whose codes differ says nothing a reader can rely on and reads as a family it knows nothing of, `runtime` and `report`; in the core registry `8.0.0` no family shares a step kind, so an unknown step kind always reads as `report`, while `auth`, `account` and `input` share a domain. A retry verdict is never read by the family: `effective_retry` reads one it does not know as `no`, as the protocol requires, so that nothing is repeated on a guess. An item carries no verdict, so `ProblemItem.effective_domain` reads its domain from its code. A fix operation of a kind the protocol does not list reads as `UnknownFixOp`, and `SuggestedFix.is_applicable` is then false. +- **Open for what it does not know.** A domain, a retry verdict, a step kind or a fix's safety the protocol does not list stays the plain string received. `effective_domain` and `effective_step_kind` read such a value by the code's family, as the generated registry declares it: the domain every core code of the family shares, and the step kind every one of them gives to the actor and to any other reader alike, since a reader that does not know a step kind cannot tell whose step it is. A family whose codes differ says nothing a reader can rely on and reads as a family it knows nothing of, `runtime` and `report`; in the core registry `8.0.1` no family shares a step kind, so an unknown step kind always reads as `report`, while `auth`, `account` and `input` share a domain. A retry verdict is never read by the family: `effective_retry` reads one it does not know as `no`, as the protocol requires, so that nothing is repeated on a guess. An item carries no verdict, so `ProblemItem.effective_domain` reads its domain from its code. A fix operation of a kind the protocol does not list reads as `UnknownFixOp`, and `SuggestedFix.is_applicable` is then false. - **Never repeated on a guess.** This client never repeats a request by itself. `problem.is_retryable` is true only for a retry verdict of `yes`, so a caller that retries does it only then, after `retry_after` seconds when given; `problem.outcome_is_unknown` is true for `outcome_unknown`, where the run may already exist and the caller reads it rather than repeating the call. The registry's generated module (`mthds.protocol.problem.registry`) gives a constant per core code (`CoreCode`), a typed parameters model per code (`problem.params_as(ModelUnknownParams).model`), and each code's entry (`CORE_CODE_SPECS`). A client states its own problems with `make_client_problem` (`mthds.protocol.problem.client_problems`), which fills the code's sentences from its parameters and picks the step aimed at the client's caller: the actor's for the domains `input` and `runtime`, the other reader's for `method`, `account` and `operator`. diff --git a/docs/versioning.md b/docs/versioning.md index 0b9cebb..7a2f6c1 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -43,7 +43,7 @@ The same number appears in a library crate as a **stamp** rather than a constrai The core error registry is published with each release of the standard, under that release's number, and a new code moves no protocol number: that is why the registry has a version of its own here, and why it can stand ahead of `MTHDS_STANDARD_VERSION`, which follows the language rather than the codes. The registry's JSON snapshot and its schema are vendored byte for byte from the `mthds` repository, with a sidecar (`vendor/mthds-error-registry/source.json`) recording the ref, the commit and each file's SHA-256, and the module `mthds/protocol/problem/registry.py` is generated from them. Following a registry release takes three steps: ```bash -make vendor-error-registry MTHDS= REF= +make vendor-error-registry MTHDS= REF= make generate-error-registry # then set CORE_ERROR_REGISTRY_VERSION in mthds/protocol/protocol.py to the registry's version ``` diff --git a/mthds/protocol/problem/registry.py b/mthds/protocol/problem/registry.py index 2d9de84..12fff03 100644 --- a/mthds/protocol/problem/registry.py +++ b/mthds/protocol/problem/registry.py @@ -1,4 +1,4 @@ -"""The MTHDS core error registry 8.0.0, generated from its vendored snapshot: do not edit this file. +"""The MTHDS core error registry 8.0.1, generated from its vendored snapshot: do not edit this file. `scripts/generate_error_registry.py` writes it from `vendor/mthds-error-registry/mthds-error-registry.json` and the registry's schema beside it, `make generate-error-registry` runs it, and @@ -22,7 +22,7 @@ from mthds.protocol.problem.json_types import JsonInteger, JsonNumber -REGISTRY_VERSION: Final[str] = "8.0.0" +REGISTRY_VERSION: Final[str] = "8.0.1" """The version of the core registry this module was generated from, which is the standard version it is published with.""" CORE_BASE_URL: Final[str] = "https://mthds.ai/errors" @@ -392,7 +392,7 @@ class RequestBlobsMissingParams(BaseModel): missing_blobs: Annotated[ list[Annotated[str, Field(pattern=r"^[0-9a-f]{64}$")]], - Field(description="Each hash of the request that the runner does not hold, in lowercase hexadecimal."), + Field(description="Each hash the request's `files` names that the runner does not hold, in lowercase hexadecimal."), ] @@ -2126,7 +2126,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.INPUT_WRONG_CONCEPT: CodeSpec( code="input.wrong_concept", - title="An input is not the declared concept", + title="An input is not of the kind the method declares", domain=ProblemDomain.INPUT, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -2143,7 +2143,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.INPUT_WRONG_MULTIPLICITY: CodeSpec( code="input.wrong_multiplicity", - title="An input has the wrong number of items", + title="An input has the wrong form or the wrong number of items", domain=ProblemDomain.INPUT, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -2154,7 +2154,9 @@ class ClientToolOutdatedParams(BaseModel): ), other_step=StepSpec( kind=StepKind.CHANGE_INPUT, - detail=("Pass this to the caller, who must give the input '{input}' with the multiplicity the method declares."), + detail=( + "Pass this to the caller, who must give the input '{input}' as the method declares it: one item, a list, or a list of a set length." + ), ), params=( ParamSpec(name="input", param_type=ParamType.IDENTIFIER), @@ -2174,7 +2176,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.INPUT_INVALID: CodeSpec( code="input.invalid", - title="An input does not fit its concept's structure", + title="An input does not have the structure the method declares", domain=ProblemDomain.INPUT, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3063,7 +3065,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_OUTPUT_MULTIPLICITY: CodeSpec( code="pipe.output_multiplicity", - title="A pipe's output does not have the declared multiplicity", + title="A step produced a list instead of one result, or the reverse", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3074,7 +3076,9 @@ class ClientToolOutdatedParams(BaseModel): ), other_step=StepSpec( kind=StepKind.CONTACT_OWNER, - detail=("Ask the method's owner to make the output '{output}' of pipe '{pipe}' match its declared multiplicity."), + detail=( + "Ask the method's owner to make the step '{pipe}' produce its output '{output}' as one item or as a list, as the method declares it." + ), ), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), @@ -3100,7 +3104,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_OUTPUT_INVALID: CodeSpec( code="pipe.output_invalid", - title="A pipe's output does not fit its concept", + title="A step's result does not have the structure the method declares", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3111,7 +3115,8 @@ class ClientToolOutdatedParams(BaseModel): detail=("Make pipe '{pipe}' produce an output that fits '{output_concept}', or loosen the concept, then run the method again."), ), other_step=StepSpec( - kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to make pipe '{pipe}' produce an output that fits its concept." + kind=StepKind.CONTACT_OWNER, + detail=("Ask the method's owner to make the step '{pipe}' produce a result with the structure the method declares."), ), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), @@ -3122,7 +3127,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_FUNCTION_RAISED: CodeSpec( code="pipe.function_raised", - title="The method's function raised", + title="The method's function failed", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3131,7 +3136,7 @@ class ClientToolOutdatedParams(BaseModel): actor_step=StepSpec( kind=StepKind.CHANGE_METHOD, detail="Fix the function '{function}' so that it does not raise on these inputs, then run the method again." ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to fix the function '{function}' of pipe '{pipe}'."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to fix the function '{function}' of the step '{pipe}'."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec(name="function", param_type=ParamType.IDENTIFIER), @@ -3159,7 +3164,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_TEMPLATE_RENDER_FAILED: CodeSpec( code="pipe.template_render_failed", - title="A template failed to render", + title="A template in the method could not be filled in", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3168,7 +3173,7 @@ class ClientToolOutdatedParams(BaseModel): actor_step=StepSpec( kind=StepKind.CHANGE_METHOD, detail="Fix the template of pipe '{pipe}' where it reads '{variable}', then run the method again." ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to fix the template of pipe '{pipe}'."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to fix the template of the step '{pipe}'."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec(name="variable", param_type=ParamType.IDENTIFIER), @@ -3177,7 +3182,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_TEMPLATE_BUDGET_EXCEEDED: CodeSpec( code="pipe.template_budget_exceeded", - title="A template exceeded its render budget", + title="A template in the method tried to do more than it may", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3186,9 +3191,7 @@ class ClientToolOutdatedParams(BaseModel): actor_step=StepSpec( kind=StepKind.CHANGE_METHOD, detail="Make the template of pipe '{pipe}' do less in one render, then run the method again." ), - other_step=StepSpec( - kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to make the template of pipe '{pipe}' do less in one render." - ), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to make the template of the step '{pipe}' do less."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec( @@ -3219,7 +3222,10 @@ class ClientToolOutdatedParams(BaseModel): kind=StepKind.CHANGE_METHOD, detail=("Map every value the condition of pipe '{pipe}' can render to an outcome, or give it a default outcome."), ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to map every value of the condition of pipe '{pipe}'."), + other_step=StepSpec( + kind=StepKind.CONTACT_OWNER, + detail=("Ask the method's owner to give the condition of the step '{pipe}' an outcome for every value it can take."), + ), params=(ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER),), params_model=PipeConditionUnmatchedParams, ), @@ -3241,7 +3247,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_BINDING_FAILED: CodeSpec( code="pipe.binding_failed", - title="A binding step could not bind its value", + title="A step could not find a field in the value it reads", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3251,7 +3257,7 @@ class ClientToolOutdatedParams(BaseModel): kind=StepKind.CHANGE_METHOD, detail=("Correct the binding '{binding}' of pipe '{pipe}', or the structure it reads, then run the method again."), ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to fix the binding '{binding}' of pipe '{pipe}'."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to fix how the step '{pipe}' reads '{binding}'."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec(name="binding", param_type=ParamType.IDENTIFIER), @@ -3261,7 +3267,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_INPUT_UNRESOLVED: CodeSpec( code="pipe.input_unresolved", - title="A pipe could not read a value it needs", + title="A step could not read a value it needs", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3279,14 +3285,14 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_FORCED_INPUT_ABSENT: CodeSpec( code="pipe.forced_input_absent", - title="A forced input was absent", + title="A value the method counts on was missing", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, status=422, detail="Pipe '{pipe}' forces '{variable}' with '!', and it was absent in this run.", actor_step=StepSpec(kind=StepKind.CHANGE_METHOD, detail="Remove the '!' on '{variable}' and handle its absence, then run the method again."), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to handle the absence of '{variable}' in pipe '{pipe}'."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to handle a missing '{variable}' in the step '{pipe}'."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec(name="variable", param_type=ParamType.IDENTIFIER), @@ -3295,7 +3301,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_PROMPT_EMPTY: CodeSpec( code="pipe.prompt_empty", - title="A pipe's prompt is empty", + title="A step's prompt came out empty", domain=ProblemDomain.INPUT, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3304,7 +3310,7 @@ class ClientToolOutdatedParams(BaseModel): actor_step=StepSpec( kind=StepKind.CHANGE_INPUT, detail="Give the inputs that the prompt of pipe '{pipe}' is written from, then send the request again." ), - other_step=StepSpec(kind=StepKind.CHANGE_INPUT, detail="Pass this to the caller, whose inputs leave the prompt of pipe '{pipe}' empty."), + other_step=StepSpec(kind=StepKind.CHANGE_INPUT, detail="Pass this to the caller, whose inputs leave the prompt of the step '{pipe}' empty."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec( @@ -3321,7 +3327,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.PIPE_STACK_OVERFLOW: CodeSpec( code="pipe.stack_overflow", - title="The run nested pipes deeper than the limit", + title="The run nested its steps deeper than the limit", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3330,7 +3336,9 @@ class ClientToolOutdatedParams(BaseModel): actor_step=StepSpec( kind=StepKind.CHANGE_METHOD, detail="Find the pipes that call each other without end, and break the loop, then run the method again." ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to stop the method's pipes from nesting without end."), + other_step=StepSpec( + kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to stop the method's steps from calling each other without end." + ), params=(ParamSpec(name="limit", param_type=ParamType.INTEGER),), params_model=PipeStackOverflowParams, ), @@ -3439,7 +3447,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.MODEL_OUTPUT_TRUNCATED: CodeSpec( code="model.output_truncated", - title="The answer was cut at the method's token cap", + title="The model's answer was cut off at the method's length limit", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3449,7 +3457,7 @@ class ClientToolOutdatedParams(BaseModel): kind=StepKind.CHANGE_METHOD, detail=("Raise the step's max_tokens above {max_tokens}, or ask for a shorter answer, then run the method again."), ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to raise the step's token cap."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to raise the step's length limit."), params=( ParamSpec(name="model", param_type=ParamType.IDENTIFIER), ParamSpec(name="max_tokens", param_type=ParamType.INTEGER), @@ -3459,7 +3467,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.MODEL_OUTPUT_SCHEMA_MISMATCH: CodeSpec( code="model.output_schema_mismatch", - title="The model's structured output did not match its schema", + title="The model's answer did not have the expected structure", domain=ProblemDomain.RUNTIME, retry=RetryVerdict.YES, raised_as=RaisedAs.PROBLEM, @@ -3846,7 +3854,7 @@ class ClientToolOutdatedParams(BaseModel): status=422, detail=("Pipe '{pipe}' names a URL that points at a {target_class} target, which a security rule forbids (reason: {reason})."), actor_step=StepSpec(kind=StepKind.CHANGE_METHOD, detail="Make pipe '{pipe}' name a URL on the public internet, then run the method again."), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to change the URL pipe '{pipe}' names."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to change the URL the step '{pipe}' names."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec( @@ -3917,7 +3925,7 @@ class ClientToolOutdatedParams(BaseModel): actor_step=StepSpec( kind=StepKind.CHANGE_METHOD, detail="Make pipe '{pipe}' name a file inside the method's tree, then run the method again." ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to change the path pipe '{pipe}' names."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to change the path the step '{pipe}' names."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec(name="file", param_type=ParamType.IDENTIFIER), @@ -3926,14 +3934,14 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.SECURITY_TEMPLATE_REFUSED: CodeSpec( code="security.template_refused", - title="A template was refused by the template sandbox", + title="A template in the method reached for something it may not use", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, status=422, detail="A template of pipe '{pipe}' reached for '{name}', which the template sandbox forbids.", actor_step=StepSpec(kind=StepKind.CHANGE_METHOD, detail="Remove '{name}' from the templates of pipe '{pipe}', then run the method again."), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to remove '{name}' from the templates of pipe '{pipe}'."), + other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to remove '{name}' from the templates of the step '{pipe}'."), params=( ParamSpec(name="pipe", param_type=ParamType.IDENTIFIER), ParamSpec(name="name", param_type=ParamType.IDENTIFIER), @@ -3942,7 +3950,7 @@ class ClientToolOutdatedParams(BaseModel): ), CoreCode.SECURITY_SCHEMA_REFUSED: CodeSpec( code="security.schema_refused", - title="An output schema was refused", + title="The method declares an output structure the runtime refuses", domain=ProblemDomain.METHOD, retry=RetryVerdict.NO, raised_as=RaisedAs.PROBLEM, @@ -3952,7 +3960,9 @@ class ClientToolOutdatedParams(BaseModel): kind=StepKind.CHANGE_METHOD, detail=("Remove the code-generation extensions and imports from the concept '{concept}', then run the method again."), ), - other_step=StepSpec(kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to remove the extensions from the concept '{concept}'."), + other_step=StepSpec( + kind=StepKind.CONTACT_OWNER, detail="Ask the method's owner to remove the code that the output structure '{concept}' carries." + ), params=(ParamSpec(name="concept", param_type=ParamType.IDENTIFIER),), params_model=SecuritySchemaRefusedParams, ), diff --git a/mthds/protocol/protocol.py b/mthds/protocol/protocol.py index c6f75c7..37e87a7 100644 --- a/mthds/protocol/protocol.py +++ b/mthds/protocol/protocol.py @@ -32,7 +32,7 @@ Version") for what bumps it. """ -CORE_ERROR_REGISTRY_VERSION: Final[str] = "8.0.0" +CORE_ERROR_REGISTRY_VERSION: Final[str] = "8.0.1" """The version of the MTHDS core error registry this SDK reads problems against. The registry is published with each release of the standard and carries that release's diff --git a/tests/data/problem-reading-cases.json b/tests/data/problem-reading-cases.json index 176b026..a46b53b 100644 --- a/tests/data/problem-reading-cases.json +++ b/tests/data/problem-reading-cases.json @@ -1161,5 +1161,5 @@ "name": "lost-after-send" } ], - "registry_version": "8.0.0" + "registry_version": "8.0.1" } diff --git a/tests/unit/test_versions.py b/tests/unit/test_versions.py index f21ea9a..4096cbc 100644 --- a/tests/unit/test_versions.py +++ b/tests/unit/test_versions.py @@ -14,7 +14,7 @@ def test_constants_match_the_standards_cut(self): # cut moving there is exactly what this asserts against. assert MTHDS_STANDARD_VERSION == "8.0.0" assert PROTOCOL_VERSION == "2.0.0" - assert CORE_ERROR_REGISTRY_VERSION == "8.0.0" + assert CORE_ERROR_REGISTRY_VERSION == "8.0.1" assert is_valid_semver(MTHDS_STANDARD_VERSION) assert is_valid_semver(PROTOCOL_VERSION) assert is_valid_semver(CORE_ERROR_REGISTRY_VERSION) diff --git a/vendor/mthds-error-registry/mthds-error-registry.json b/vendor/mthds-error-registry/mthds-error-registry.json index 58063a7..f505e26 100644 --- a/vendor/mthds-error-registry/mthds-error-registry.json +++ b/vendor/mthds-error-registry/mthds-error-registry.json @@ -1,5 +1,5 @@ { - "version": "8.0.0", + "version": "8.0.1", "base_url": "https://mthds.ai/errors", "extension_prefixes": [ { @@ -479,10 +479,10 @@ "items": { "type": "sha256" }, - "description": "Each hash of the request that the runner does not hold, in lowercase hexadecimal." + "description": "Each hash the request's `files` names that the runner does not hold, in lowercase hexadecimal." } }, - "meaning": "A hash entry of the request's `files` names bytes this runner does not hold, or the runner keeps no blob store at all, so it cannot assemble the method's tree. This is the protocol's own refusal of hash entries.", + "meaning": "A hash entry of the request's `files` names bytes this runner does not hold, or the runner keeps no blob store at all, so it cannot assemble the method's tree. This is the protocol's own refusal of hash entries, and the last check of a map: a runner answers it only for a `files` map that breaks no other rule of the method tree's file entries, and never checks the hash entry of a key it refuses.", "what_to_do": "Read `params.missing_blobs`. To a runner whose `GET /version` declares `blob_store: true`, upload each listed blob as `POST /blobs/missing` says; to any other runner, send each listed file inline. Then send the request again.", "see": [ { @@ -714,7 +714,7 @@ }, { "code": "input.wrong_concept", - "title": "An input is not the declared concept", + "title": "An input is not of the kind the method declares", "domain": "input", "retry": "no", "raised_as": "problem", @@ -755,7 +755,7 @@ }, { "code": "input.wrong_multiplicity", - "title": "An input has the wrong number of items", + "title": "An input has the wrong form or the wrong number of items", "domain": "input", "retry": "no", "raised_as": "problem", @@ -768,7 +768,7 @@ }, "other": { "kind": "change_input", - "detail": "Pass this to the caller, who must give the input '{input}' with the multiplicity the method declares." + "detail": "Pass this to the caller, who must give the input '{input}' as the method declares it: one item, a list, or a list of a set length." } }, "params": { @@ -806,7 +806,7 @@ }, { "code": "input.invalid", - "title": "An input does not fit its concept's structure", + "title": "An input does not have the structure the method declares", "domain": "input", "retry": "no", "raised_as": "problem", @@ -2590,7 +2590,7 @@ }, { "code": "pipe.output_multiplicity", - "title": "A pipe's output does not have the declared multiplicity", + "title": "A step produced a list instead of one result, or the reverse", "domain": "method", "retry": "no", "raised_as": "problem", @@ -2603,7 +2603,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to make the output '{output}' of pipe '{pipe}' match its declared multiplicity." + "detail": "Ask the method's owner to make the step '{pipe}' produce its output '{output}' as one item or as a list, as the method declares it." } }, "params": { @@ -2647,7 +2647,7 @@ }, { "code": "pipe.output_invalid", - "title": "A pipe's output does not fit its concept", + "title": "A step's result does not have the structure the method declares", "domain": "method", "retry": "no", "raised_as": "problem", @@ -2660,7 +2660,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to make pipe '{pipe}' produce an output that fits its concept." + "detail": "Ask the method's owner to make the step '{pipe}' produce a result with the structure the method declares." } }, "params": { @@ -2691,7 +2691,7 @@ }, { "code": "pipe.function_raised", - "title": "The method's function raised", + "title": "The method's function failed", "domain": "method", "retry": "no", "raised_as": "problem", @@ -2704,7 +2704,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to fix the function '{function}' of pipe '{pipe}'." + "detail": "Ask the method's owner to fix the function '{function}' of the step '{pipe}'." } }, "params": { @@ -2769,7 +2769,7 @@ }, { "code": "pipe.template_render_failed", - "title": "A template failed to render", + "title": "A template in the method could not be filled in", "domain": "method", "retry": "no", "raised_as": "problem", @@ -2782,7 +2782,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to fix the template of pipe '{pipe}'." + "detail": "Ask the method's owner to fix the template of the step '{pipe}'." } }, "params": { @@ -2806,7 +2806,7 @@ }, { "code": "pipe.template_budget_exceeded", - "title": "A template exceeded its render budget", + "title": "A template in the method tried to do more than it may", "domain": "method", "retry": "no", "raised_as": "problem", @@ -2819,7 +2819,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to make the template of pipe '{pipe}' do less in one render." + "detail": "Ask the method's owner to make the template of the step '{pipe}' do less." } }, "params": { @@ -2862,7 +2862,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to map every value of the condition of pipe '{pipe}'." + "detail": "Ask the method's owner to give the condition of the step '{pipe}' an outcome for every value it can take." } }, "params": { @@ -2919,7 +2919,7 @@ }, { "code": "pipe.binding_failed", - "title": "A binding step could not bind its value", + "title": "A step could not find a field in the value it reads", "domain": "method", "retry": "no", "raised_as": "problem", @@ -2932,7 +2932,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to fix the binding '{binding}' of pipe '{pipe}'." + "detail": "Ask the method's owner to fix how the step '{pipe}' reads '{binding}'." } }, "params": { @@ -2960,7 +2960,7 @@ }, { "code": "pipe.input_unresolved", - "title": "A pipe could not read a value it needs", + "title": "A step could not read a value it needs", "domain": "method", "retry": "no", "raised_as": "problem", @@ -2997,7 +2997,7 @@ }, { "code": "pipe.forced_input_absent", - "title": "A forced input was absent", + "title": "A value the method counts on was missing", "domain": "method", "retry": "no", "raised_as": "problem", @@ -3010,7 +3010,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to handle the absence of '{variable}' in pipe '{pipe}'." + "detail": "Ask the method's owner to handle a missing '{variable}' in the step '{pipe}'." } }, "params": { @@ -3034,7 +3034,7 @@ }, { "code": "pipe.prompt_empty", - "title": "A pipe's prompt is empty", + "title": "A step's prompt came out empty", "domain": "input", "retry": "no", "raised_as": "problem", @@ -3047,7 +3047,7 @@ }, "other": { "kind": "change_input", - "detail": "Pass this to the caller, whose inputs leave the prompt of pipe '{pipe}' empty." + "detail": "Pass this to the caller, whose inputs leave the prompt of the step '{pipe}' empty." } }, "params": { @@ -3070,7 +3070,7 @@ }, { "code": "pipe.stack_overflow", - "title": "The run nested pipes deeper than the limit", + "title": "The run nested its steps deeper than the limit", "domain": "method", "retry": "no", "raised_as": "problem", @@ -3083,7 +3083,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to stop the method's pipes from nesting without end." + "detail": "Ask the method's owner to stop the method's steps from calling each other without end." } }, "params": { @@ -3302,7 +3302,7 @@ }, { "code": "model.output_truncated", - "title": "The answer was cut at the method's token cap", + "title": "The model's answer was cut off at the method's length limit", "domain": "method", "retry": "no", "raised_as": "problem", @@ -3315,7 +3315,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to raise the step's token cap." + "detail": "Ask the method's owner to raise the step's length limit." } }, "params": { @@ -3343,7 +3343,7 @@ }, { "code": "model.output_schema_mismatch", - "title": "The model's structured output did not match its schema", + "title": "The model's answer did not have the expected structure", "domain": "runtime", "retry": "yes", "raised_as": "problem", @@ -4099,7 +4099,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to change the URL pipe '{pipe}' names." + "detail": "Ask the method's owner to change the URL the step '{pipe}' names." } }, "params": { @@ -4209,7 +4209,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to change the path pipe '{pipe}' names." + "detail": "Ask the method's owner to change the path the step '{pipe}' names." } }, "params": { @@ -4233,7 +4233,7 @@ }, { "code": "security.template_refused", - "title": "A template was refused by the template sandbox", + "title": "A template in the method reached for something it may not use", "domain": "method", "retry": "no", "raised_as": "problem", @@ -4246,7 +4246,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to remove '{name}' from the templates of pipe '{pipe}'." + "detail": "Ask the method's owner to remove '{name}' from the templates of the step '{pipe}'." } }, "params": { @@ -4264,7 +4264,7 @@ }, { "code": "security.schema_refused", - "title": "An output schema was refused", + "title": "The method declares an output structure the runtime refuses", "domain": "method", "retry": "no", "raised_as": "problem", @@ -4277,7 +4277,7 @@ }, "other": { "kind": "contact_owner", - "detail": "Ask the method's owner to remove the extensions from the concept '{concept}'." + "detail": "Ask the method's owner to remove the code that the output structure '{concept}' carries." } }, "params": { diff --git a/vendor/mthds-error-registry/source.json b/vendor/mthds-error-registry/source.json index 52741e3..af7a0ed 100644 --- a/vendor/mthds-error-registry/source.json +++ b/vendor/mthds-error-registry/source.json @@ -1,11 +1,11 @@ { "source_repository": "mthds-ai/mthds", "source_directory": "docs/spec/openapi", - "source_ref": "v8.0.0", - "source_commit": "dacacdf5e57fe35ebd86a6b7716250070df008d9", - "registry_version": "8.0.0", + "source_ref": "v8.0.1", + "source_commit": "fdfedc5356ea88a9647a262711dd477c4b1c7a71", + "registry_version": "8.0.1", "files": { - "mthds-error-registry.json": "f64fc9f779cc96fa722b9a9c082c12155e3b8fd4e428048754e8241691eea440", + "mthds-error-registry.json": "d1db2596cd5711ed9c0b28231d4371b2d2fd5fdc49783c590a1b735cd5a0886a", "mthds-error-registry.schema.json": "2923af37db081788fdfaf43d8d0e24a39e68be3ebd43f75ef64a04f87480e723" } } From fe681ef13443366131fdf7e33fd13b6a0e94dc94 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 29 Oct 2026 12:58:12 +0100 Subject: [PATCH 2/2] Follow the MTHDS v8.0.1 cut: standard 8.0.1, Protocol 2.0.1 MTHDS_STANDARD_VERSION moves to 8.0.1 and PROTOCOL_VERSION to 2.0.1, the numbers the standard's v8.0.1 release states, so a manifest asking for >=8.0.1, as the standard's own examples now do, is satisfied. The pinned native set is unchanged in v8.0.1, so an implementation of 8.0.1 still materializes the set pinned before it. Protocol 2.0.1 states the order of a files map's refusals: a key refused by its name collides with nothing and has no entry checks, and a hash entry's bytes are checked last. read_file_map already ran its checks in that order; the method-tree corpus is vendored again from conformance, whose new map cases pin it, and every one of them passes. The VersionInfo blob_store description, docs/runners.md and docs/method-tree.md take the clarified wording. The vendoring script's --ref example names v8.0.1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TF9N1QfNEKCTsX1Q6NCf1 --- CHANGELOG.md | 2 +- docs/method-tree.md | 2 +- docs/runners.md | 2 +- docs/versioning.md | 2 +- mthds/package/manifest/schema.py | 2 +- mthds/protocol/models.py | 4 +- mthds/protocol/protocol.py | 2 +- scripts/vendor_error_registry.py | 2 +- tests/fixtures/method_tree_corpus.json | 42 +++++++++++++++++++ tests/fixtures/method_tree_corpus.source.json | 4 +- tests/unit/test_versions.py | 8 ++-- 11 files changed, 59 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffeaffd..cb29459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Changed -- **Core error registry `8.0.1`**: `mthds.protocol.problem.registry` is generated from the standard's core error registry `8.0.1`, vendored from the standard's `v8.0.1` release, and `CORE_ERROR_REGISTRY_VERSION` moved from `8.0.0` to `8.0.1`. The titles of the codes a run can raise, and the next steps they give a reader other than the method's author, are reworded for someone following a run of a method they did not write: `pipe.output_invalid`, for one, is now titled "A step's result does not have the structure the method declares" where it read "A pipe's output does not fit its concept". No code, domain, retry verdict, step kind or status changes. +- **Standard version `8.0.1`, Protocol `2.0.1` and core error registry `8.0.1` (Breaking)**: the package follows the standard's `v8.0.1` release: `MTHDS_STANDARD_VERSION` moved from `8.0.0` to `8.0.1`, so a manifest's `mthds_version` of `>=8.0.1`, as the standard's own examples now read, passes while an exact `8.0.0` no longer does; `PROTOCOL_VERSION` moved from `2.0.0` to `2.0.1`, the cut that states the order of a files map's refusals, which `read_file_map` already followed; and `CORE_ERROR_REGISTRY_VERSION` moved from `8.0.0` to `8.0.1`, with `mthds.protocol.problem.registry` generated from that release's registry. The titles of the codes a run can raise, and the next steps they give a reader other than the method's author, are reworded for someone following a run of a method they did not write: `pipe.output_invalid`, for one, is now titled "A step's result does not have the structure the method declares" where it read "A pipe's output does not fit its concept". No code, domain, retry verdict, step kind or status changes. - **Dev tooling: the development Python is pinned in `.python-version`**: the Makefile, uv and CI read the version from that file, `PYTHON_VERSION= make install` still builds `.venv` on another supported version, and each job of the CI matrices now fails unless its environment ran the version the job names. Nothing shipped changes. ## [v0.25.0] - 2026-10-18 diff --git a/docs/method-tree.md b/docs/method-tree.md index d6cac4a..e53e423 100644 --- a/docs/method-tree.md +++ b/docs/method-tree.md @@ -60,7 +60,7 @@ The standard bounds a tree path's length and nothing else, so a file's size, a t ## A tree in transit: the file entry -A tree travels as a JSON object mapping each tree path to a file entry: a string holding the file's text, `{"base64": "…"}` in canonical padded Base64, or `{"sha256": "…", "size": …}` naming bytes the receiver already holds. `encode_file_map(contents, held_by_receiver=…)` writes one as a `dict[str, FileEntry]`, each entry a `str`, a `Base64FileEntry` or a `HashFileEntry`, which `pydantic_core.to_json` writes in its wire form; `MethodFiles`, a read-only map of the same entries, is the type of the `files` a protocol client sends (see [Sending a method](./runners.md#sending-a-method-files-inline-or-by-hash) for how `MthdsAPIClient` builds one from a listed tree, inline or by hash). `read_file_map(document, blob_store=…)` reads one from its JSON text as a receiver does, refusing a key given twice in the map or inside an entry object, a key that is not a tree path or that the membership rule could never admit, such as one whose last segment is `inputs.json` or `inputs.toml`, refused as `excluded_name`, colliding keys, a malformed entry, and every hash entry whose bytes the blob store lacks. It reads the text rather than a decoded dictionary because a key given twice and a `size` written as `13.0` are only visible there. Text that is not JSON, holds `NaN` or `Infinity`, an integer longer than Python converts, or nesting deeper than `MAX_JSON_DEPTH` levels is a `TreeSourceError`, never a bare `ValueError` or `RecursionError`. That bound is the receiver's own, set below the depth at which any supported Python's decoder gives up, since that depth varies from one version to the next and Python 3.14's decoder has none, so a document gets the same answer on every supported Python. `Base64FileEntry` and `HashFileEntry` are the strict pydantic models of the object forms. +A tree travels as a JSON object mapping each tree path to a file entry: a string holding the file's text, `{"base64": "…"}` in canonical padded Base64, or `{"sha256": "…", "size": …}` naming bytes the receiver already holds. `encode_file_map(contents, held_by_receiver=…)` writes one as a `dict[str, FileEntry]`, each entry a `str`, a `Base64FileEntry` or a `HashFileEntry`, which `pydantic_core.to_json` writes in its wire form; `MethodFiles`, a read-only map of the same entries, is the type of the `files` a protocol client sends (see [Sending a method](./runners.md#sending-a-method-files-inline-or-by-hash) for how `MthdsAPIClient` builds one from a listed tree, inline or by hash). `read_file_map(document, blob_store=…)` reads one from its JSON text as a receiver does, refusing a key given twice in the map or inside an entry object, a key that is not a tree path or that the membership rule could never admit, such as one whose last segment is `inputs.json` or `inputs.toml`, refused as `excluded_name`, colliding keys, a malformed entry, and every hash entry whose bytes the blob store lacks. The checks run in the order the standard states: a key refused by its name, as no tree path or as one the membership rule could never admit, is refused for that alone, since the collision check and the entry checks read only the keys the map does not refuse so, and the bytes of the hash entries are checked last, so a map is refused as `missing_blob` only when it breaks no other rule, and a sender never uploads blobs for a map refused anyway. It reads the text rather than a decoded dictionary because a key given twice and a `size` written as `13.0` are only visible there. Text that is not JSON, holds `NaN` or `Infinity`, an integer longer than Python converts, or nesting deeper than `MAX_JSON_DEPTH` levels is a `TreeSourceError`, never a bare `ValueError` or `RecursionError`. That bound is the receiver's own, set below the depth at which any supported Python's decoder gives up, since that depth varies from one version to the next and Python 3.14's decoder has none, so a document gets the same answer on every supported Python. `Base64FileEntry` and `HashFileEntry` are the strict pydantic models of the object forms. ## A tree written out diff --git a/docs/runners.md b/docs/runners.md index 0bb8058..5245deb 100644 --- a/docs/runners.md +++ b/docs/runners.md @@ -94,7 +94,7 @@ The two optional blob routes are methods of their own, for a sender that manages - `missing_blobs(blobs, digest=None)` posts a `MissingBlobsRequest` to `/v1/blobs/missing` and answers with a `MissingBlobsResult`: every blob the runner lacks, each with an `UploadTarget` (`method`, `url`, `headers`). A runner that serves no blob routes answers non-2xx, raised as `ApiResponseError`. The route lists only blobs of the request, so an answer naming another, by its hash or its size, is not the route's answer: it raises `ApiResponseError` carrying `client.response_unreadable`, and nothing is uploaded. - `upload_blob(blob, data)` sends the bytes to that target. The target is the runner's choice and may be another server, such as an object store behind a presigned URL, so the request carries exactly the target's headers and nothing of this client's: neither the API key nor the `User-Agent` goes with it. A `412` from a create-only target, one whose headers carry `If-None-Match`, means the bytes are already stored and counts as uploaded. Bytes that are not the named blob, by hash or by size, are refused before anything is sent. Any other non-2xx raises `BlobUploadError` (`mthds.runners.api.exceptions`), a `PipelineRequestError` carrying the problem the target's answer reads as (see [Errors](#errors-every-failure-is-a-problem) below) beside `sha256`, `status`, `status_text`, `response_body` and `upload_url`, the last without its userinfo, query or fragment, since a presigned query is itself the permission to write and a `user:password@` is a credential. For the same reason the message never quotes the target's body, as the TypeScript client never quotes it: an object store's refusal, such as S3's `SignatureDoesNotMatch`, echoes the key id, the signature and the presigned query it checked, so the body reaches neither the message nor the problem, and only `response_body` keeps it. An upload that gets no usable answer, because no connection opened, none came back in time, the connection was lost or the body cannot be decoded, raises `BlobUploadError` too, its `status`, `status_text` and `response_body` `None`, as the TypeScript client's carries no status then. It reads as a request that creates nothing does, `client.unreachable` naming the target as `upload_url` shows it, or `client.timeout`, since a blob is named by its bytes and storing it twice is harmless, so its upload is safe to repeat. A malformed target raises it carrying `client.response_unreadable`, naming the `200` of the `missing_blobs` answer that named it, since no repeat changes it: one that is no URL the client can send to, such as one whose port is no number or whose scheme is neither `http` nor `https`, and one with a header the client cannot write, a value not in ASCII or one holding a line break, which h11 refuses before writing a byte, rather than reading as a retryable `client.unreachable`. Its headers are never quoted, since they may carry credentials, and the refusal of a header is not chained as the error's cause, since h11's words quote the header's value. -A run that names a hash the runner does not hold is answered with a `422` carrying `request.blobs_missing`, raised as `ApiResponseError` with each hash the runner lacks on `exc.problem.params.missing_blobs`. +A run whose files map breaks no other rule of the method tree but names a hash the runner does not hold is answered with a `422` carrying `request.blobs_missing`, raised as `ApiResponseError` with each hash the runner lacks on `exc.problem.params.missing_blobs`. ### Errors: every failure is a problem diff --git a/docs/versioning.md b/docs/versioning.md index 7a2f6c1..915cce7 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -14,7 +14,7 @@ The standard's numbers are **copies of a cut made by the standard**, not decisio Two properties of the rule are worth knowing before you read a version out of this package: -- **The standard version and the protocol version are independent.** The protocol version never tracks the standard version. A release of the standard that leaves the routes alone leaves `PROTOCOL_VERSION` exactly where it is, which is why `2.0.0` and `8.0.0` are as far apart as they are. +- **The standard version and the protocol version are independent.** The protocol version never tracks the standard version. A release of the standard that leaves the routes alone leaves `PROTOCOL_VERSION` exactly where it is, which is why `2.0.1` and `8.0.1` are as far apart as they are. - **The standard version moves on every release of the specification**, including one that changes nothing normative. So the number rising is not by itself evidence that anything you depend on changed; the standard's changelog is. The corollary is that the pinned native set is identified by the standard version in which it *last changed*, and an implementation of version `V` materializes the set pinned at the greatest version less than or equal to `V`. ## `mthds_version` in a manifest is a constraint diff --git a/mthds/package/manifest/schema.py b/mthds/package/manifest/schema.py index 2ee50e0..ce41a44 100644 --- a/mthds/package/manifest/schema.py +++ b/mthds/package/manifest/schema.py @@ -34,7 +34,7 @@ RESERVED_DOMAINS: frozenset[str] = frozenset({"native", "mthds", "pipelex"}) -MTHDS_STANDARD_VERSION: Final[str] = "8.0.0" +MTHDS_STANDARD_VERSION: Final[str] = "8.0.1" """The MTHDS standard version this library implements. This is a copy of a cut made by the standard, and the only place this library diff --git a/mthds/protocol/models.py b/mthds/protocol/models.py index 9cfb277..f6eb2c3 100644 --- a/mthds/protocol/models.py +++ b/mthds/protocol/models.py @@ -220,7 +220,9 @@ class VersionInfo(BaseModel): default=False, description=( "True when the runner keeps a blob store: it serves POST /blobs/missing and resolves hash entries in a files map. " - "False or absent when it does not, in which case it answers any hash entry with a 422 listing missing_blobs." + "False or absent when it does not, in which case it answers a files map that breaks no other rule but holds hash entries " + "with a 422 coded request.blobs_missing, listing their hashes in params.missing_blobs, and serves no upload route, " + "so a sender sends those files inline instead." ), ) diff --git a/mthds/protocol/protocol.py b/mthds/protocol/protocol.py index 37e87a7..88b5d8a 100644 --- a/mthds/protocol/protocol.py +++ b/mthds/protocol/protocol.py @@ -16,7 +16,7 @@ from mthds.protocol.working_memory import WorkingMemoryAbstract -PROTOCOL_VERSION: Final[str] = "2.0.0" +PROTOCOL_VERSION: Final[str] = "2.0.1" """The MTHDS Protocol version this SDK implements. This is a copy of a cut made by the standard, and the only place this library diff --git a/scripts/vendor_error_registry.py b/scripts/vendor_error_registry.py index 09f8965..46f108b 100644 --- a/scripts/vendor_error_registry.py +++ b/scripts/vendor_error_registry.py @@ -94,7 +94,7 @@ def differs_from(checkout: Path, *, ref: str) -> list[str]: def main() -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("mthds", type=Path, help="the path of an mthds checkout") - parser.add_argument("--ref", required=True, help="the ref or tag to vendor from, such as v8.0.0") + parser.add_argument("--ref", required=True, help="the ref or tag to vendor from, such as v8.0.1") parser.add_argument("--check", action="store_true", help="write nothing and fail when the vendored files differ from the ref's") arguments = parser.parse_args() try: diff --git a/tests/fixtures/method_tree_corpus.json b/tests/fixtures/method_tree_corpus.json index 3c46351..700d78c 100644 --- a/tests/fixtures/method_tree_corpus.json +++ b/tests/fixtures/method_tree_corpus.json @@ -7814,6 +7814,48 @@ } } }, + { + "id": "run-inputs-key-with-malformed-entry", + "description": "A key inputs.json whose entry is malformed is refused as excluded_name alone: the entry checks read only the keys that are tree paths the membership rule could admit, so the unpadded Base64 under a refused key is never checked.", + "document": "{\"inputs.json\": {\"base64\": \"e30\"}, \"main.mthds\": \"x\"}", + "blob_store": [], + "expected": { + "refusal": { + "code": "excluded_name", + "names": [ + "inputs.json" + ] + } + } + }, + { + "id": "invalid-key-with-missing-blob", + "description": "A key that is not a tree path, holding a hash entry the blob store does not hold, is refused as invalid_tree_path alone, never as missing_blob: the entry under a refused key is never checked, so its hash is never named.", + "document": "{\"templates\\\\card.html\": {\"sha256\": \"3e9b68b71cc667168399f0bccfb64b2d16314cdc0ed78fac4819aba0dda121a0\", \"size\": 13}, \"main.mthds\": \"x\"}", + "blob_store": [], + "expected": { + "refusal": { + "code": "invalid_tree_path", + "names": [ + "templates\\card.html" + ] + } + } + }, + { + "id": "missing-blob-beside-excluded-key", + "description": "A map holding a key inputs.json beside a hash entry the blob store does not hold, under a key it admits, is refused as excluded_name alone, never as missing_blob: the bytes a hash entry names are checked last, only for a map that earns no other refusal, so a sender never uploads blobs for a map refused anyway.", + "document": "{\"inputs.json\": \"{}\\n\", \"card.html\": {\"sha256\": \"3e9b68b71cc667168399f0bccfb64b2d16314cdc0ed78fac4819aba0dda121a0\", \"size\": 13}, \"main.mthds\": \"x\"}", + "blob_store": [], + "expected": { + "refusal": { + "code": "excluded_name", + "names": [ + "inputs.json" + ] + } + } + }, { "id": "case-collision-keys", "description": "Two keys equal once case-folded are refused, naming both.", diff --git a/tests/fixtures/method_tree_corpus.source.json b/tests/fixtures/method_tree_corpus.source.json index e3b22ce..a0f5f28 100644 --- a/tests/fixtures/method_tree_corpus.source.json +++ b/tests/fixtures/method_tree_corpus.source.json @@ -1,8 +1,8 @@ { "source_repository": "Pipelex/conformance", "source_path": "tests/method_tree/fixtures/method-tree-corpus.json", - "source_commit": "ea617a9312ba3c72e81f880a5622a2b54aff74e8", + "source_commit": "9a6b7b2ccdd58771c00a640dff4308aece6dd05a", "files": { - "method_tree_corpus.json": "ce5df88181adb28668c2ebf6750f750e4c2b96d259168e679e4aacc5fb0038c8" + "method_tree_corpus.json": "2186906cd72fd78ce807e7b2d494d1e11b0e39a03f2d93cd1b95a43ebad6a064" } } diff --git a/tests/unit/test_versions.py b/tests/unit/test_versions.py index 4096cbc..4022d56 100644 --- a/tests/unit/test_versions.py +++ b/tests/unit/test_versions.py @@ -12,8 +12,8 @@ def test_constants_match_the_standards_cut(self): # Pinned deliberately: these are copies of a cut made in the standard # (https://mthds.ai/latest/spec/versioning/), so a value moving here without a # cut moving there is exactly what this asserts against. - assert MTHDS_STANDARD_VERSION == "8.0.0" - assert PROTOCOL_VERSION == "2.0.0" + assert MTHDS_STANDARD_VERSION == "8.0.1" + assert PROTOCOL_VERSION == "2.0.1" assert CORE_ERROR_REGISTRY_VERSION == "8.0.1" assert is_valid_semver(MTHDS_STANDARD_VERSION) assert is_valid_semver(PROTOCOL_VERSION) @@ -23,7 +23,8 @@ def test_constants_match_the_standards_cut(self): ("topic", "constraint", "is_satisfied"), [ ("the constraint every manifest written before the 2.0.0 cut carries", ">=1.0.0", True), - ("an exact pin on the current standard", "8.0.0", True), + ("an exact pin on the current standard", "8.0.1", True), + ("the constraint the standard's own manifest example carries", ">=8.0.1", True), ("a package needing the crate-key wire form", ">=2.1.0", True), ("a package needing the verdict natives", ">=3.0.0", True), ("a package needing the binding step", ">=4.0.0", True), @@ -39,6 +40,7 @@ def test_constants_match_the_standards_cut(self): ("the same compound range spelled without one", ">=8.0.0,<9.0.0", True), ("a caret range over the superseded major", "^7.0.0", False), ("an exact pin on a superseded cut", "7.0.0", False), + ("an exact pin on the superseded patch of the current minor", "8.0.0", False), ("a tilde range over a superseded minor", "~7.0.0", False), ("a wildcard over the superseded major", "7.*", False), ("a package that predates the cut and says so", "<8.0.0", False),