@terpjs/spec 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/catalog/backend/declared_read_only_routes_do_not_write.json +26 -0
- package/catalog/backend/schemas_avoid_positional_tuples.json +27 -0
- package/corpus/backend/declared_read_only_routes_do_not_write/compliant-01/modules/notes/router.py +4 -0
- package/corpus/backend/declared_read_only_routes_do_not_write/compliant-02/modules/notes/router.py +8 -0
- package/corpus/backend/declared_read_only_routes_do_not_write/violation-01/expected-findings.json +7 -0
- package/corpus/backend/declared_read_only_routes_do_not_write/violation-01/modules/notes/router.py +4 -0
- package/corpus/backend/declared_read_only_routes_do_not_write/violation-02/expected-findings.json +7 -0
- package/corpus/backend/declared_read_only_routes_do_not_write/violation-02/modules/notes/router.py +5 -0
- package/corpus/backend/schemas_avoid_positional_tuples/compliant-01/modules/notes/schemas.py +6 -0
- package/corpus/backend/schemas_avoid_positional_tuples/violation-01/modules/notes/schemas.py +2 -0
- package/corpus/backend/schemas_avoid_positional_tuples/violation-02/modules/notes/schemas.py +2 -0
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.23.0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "backend/declared_read_only_routes_do_not_write",
|
|
3
|
+
"surface": "backend",
|
|
4
|
+
"title": "A route that declares itself read-only must not mutate",
|
|
5
|
+
"intent": "Write authority is derived from the HTTP method, which is right for almost every route and blind to one: the handler that uses an unsafe method because its input is a body, not because it writes — validating a candidate document, previewing an import, costing a plan. Undeclared, such a route is pure only by the absence of a write: a guarantee made of missing code, which holds until an edit adds a line and which no rule and no reviewer is prompted to check. Declaring the intent makes it enforceable in both layers: the build-time rule refuses a declared handler that calls a mutating service method, and the runtime binder marks the request read-only so the write chokepoint refuses a write the rule cannot see statically (through a helper, a subscriber, a capability). Fix a violation by removing the write — putting it behind its own route — or by removing the declaration, whichever the route was meant to be; never both, because a declared handler that writes leaves the platform holding two answers to the same promise. Authorization is deliberately unchanged: a declared route is still authorized at the write tier, because declaring purity narrows what the handler may do, never what the caller must hold.",
|
|
6
|
+
"layer": "static-bespoke",
|
|
7
|
+
"enforcement": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "build-time",
|
|
10
|
+
"tool": "terp.arch",
|
|
11
|
+
"ref": "check_declared_read_only_routes_do_not_write"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"kind": "runtime",
|
|
15
|
+
"tool": "terp.core",
|
|
16
|
+
"ref": "build_read_only_request_binder"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"opt_out": "# arch-allow-declared-read-only-routes-do-not-write: <reason>",
|
|
20
|
+
"runtime": {
|
|
21
|
+
"applicability": "required"
|
|
22
|
+
},
|
|
23
|
+
"reference": "Mutating BaseService calls (create/update/delete/_save/_remove) inside a handler decorated with terp.core.read_only are flagged; create_app binds a declared route's request read-only (build_read_only_request_binder).",
|
|
24
|
+
"guide_topic": "module",
|
|
25
|
+
"corpus": true
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "backend/schemas_avoid_positional_tuples",
|
|
3
|
+
"surface": "backend",
|
|
4
|
+
"title": "A schema field never crosses the wire as a positional tuple",
|
|
5
|
+
"intent": "A fixed-length tuple annotation (tuple[str, str], list[tuple[str, int]], tuple[str, ...]) on a schema a client can see or send serialises into the contract as an array whose element types are positional (prefixItems, or the list form of items). Client generators do not agree on that shape: one emits the positional form and another the widened element array, so the two descriptions of the same field are structurally unrelated and the app cannot type its own calls against its own API — the failure surfaces at the call site as an opaque generic-instantiation mismatch, far from the field that caused it, and only with error truncation disabled. A tuple is also a poor contract in its own right: the positions carry meaning that no name records. Name the shape instead — a nested model with named fields when the positions differ in meaning, or a homogeneous sequence (list[str]) when they do not.",
|
|
6
|
+
"layer": "static-bespoke",
|
|
7
|
+
"enforcement": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "build-time",
|
|
10
|
+
"tool": "terp.arch",
|
|
11
|
+
"ref": "check_schemas_avoid_positional_tuples"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"kind": "runtime",
|
|
15
|
+
"tool": "terp.core",
|
|
16
|
+
"ref": "_reject_positional_tuple_schemas"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"runtime": {
|
|
20
|
+
"applicability": "required",
|
|
21
|
+
"rationale": "The invariant survives into the running app: the generated OpenAPI document is the artifact the client is built from, so the boot-time contract validation walks every component schema and refuses a positional array shape (prefixItems, or items as a list) fail-closed. This catches the vector the source check cannot see — a tuple reaching the contract through a type alias, a generic parameter, or a custom __get_pydantic_core_schema__ — and both halves name the same fix."
|
|
22
|
+
},
|
|
23
|
+
"reference": "Schema fields annotated with a nested BaseSchema model or a homogeneous list[...]; tuple[...] annotations on BaseSchema subclasses and on any class used as a route body or response_model are refused.",
|
|
24
|
+
"opt_out": "# arch-allow-schemas-avoid-positional-tuples: <reason>",
|
|
25
|
+
"guide_topic": "module",
|
|
26
|
+
"corpus": true
|
|
27
|
+
}
|
package/corpus/backend/declared_read_only_routes_do_not_write/compliant-02/modules/notes/router.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
@router.post('/preview', response_model=NotePreview)
|
|
2
|
+
@read_only
|
|
3
|
+
def preview_import(payload) -> NotePreview:
|
|
4
|
+
existing = service.list(skip=0, limit=10)
|
|
5
|
+
return NotePreview(rows=payload.rows, existing=existing)
|
|
6
|
+
@router.post('/', response_model=NoteRead)
|
|
7
|
+
def create_note(payload) -> NoteRead:
|
|
8
|
+
return service.create(payload)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "The Terp Standard — stack-neutral rule catalog, violation corpus, finding format, and refused-surface declaration (ADRs 0080/0081; packaged per ADR 0082, published per ADR 0086). Data only: consumers resolve the spec root via require.resolve('@terpjs/spec/package.json').",
|
|
5
5
|
"files": [
|
|
6
6
|
"VERSION",
|