@terpjs/spec 0.24.0 → 0.26.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/README.md +15 -4
- package/VERSION +1 -1
- package/catalog/backend/no_manual_lease_columns.json +22 -0
- package/catalog/frontend/layout-contract.json +2 -2
- package/corpus/backend/no_manual_lease_columns/compliant-01/modules/requests/models.py +13 -0
- package/corpus/backend/no_manual_lease_columns/compliant-01/modules/requests/service.py +44 -0
- package/corpus/backend/no_manual_lease_columns/compliant-02/modules/requests/schemas.py +15 -0
- package/corpus/backend/no_manual_lease_columns/violation-01/modules/requests/models.py +19 -0
- package/corpus/backend/no_manual_lease_columns/violation-02/modules/pipelines/models.py +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,17 +67,28 @@ The artifacts:
|
|
|
67
67
|
extensions, deep-import segments). The portable prohibition rules cite it
|
|
68
68
|
structurally (the `restricted_surface` catalog field); which sanctioned
|
|
69
69
|
component answers each primitive is per-stack configuration.
|
|
70
|
-
7. **The
|
|
70
|
+
7. **The layout declaration** (`layout-declaration.schema.json`) — the
|
|
71
|
+
stack-neutral, normative schema for the document an app checks in to declare
|
|
72
|
+
which slot-typed page contract it opts into, which palette it starts on, and
|
|
73
|
+
how its application shell is shaped (content measure, density, navigation
|
|
74
|
+
placement, the navigation groups a module's items name by id, and the mark
|
|
75
|
+
the app is recognised by). One document, so a build-time checker and the running app read the
|
|
76
|
+
same bytes and neither can hold a different answer — and so a tool that edits
|
|
77
|
+
files rather than code can read and rewrite those choices. Naming the contract
|
|
78
|
+
and naming a palette are the app's to do and their values are per-stack, hence
|
|
79
|
+
plain strings; the shell's vocabulary is fixed normatively, and the reference
|
|
80
|
+
stack's resolver is parity-tested against it.
|
|
81
|
+
8. **The residual ratchet** (`corpus/RESIDUALS.json`) — the statically-erased
|
|
71
82
|
or renamed forms deliberately outside the corpus contract, per rule, as
|
|
72
83
|
shrink-only data (see "Detector boundaries" below).
|
|
73
|
-
|
|
84
|
+
9. **The scorecard format** (`scorecard.schema.json`) — the machine-readable
|
|
74
85
|
certification summary a conformant checker emits (spec version, per-rule
|
|
75
86
|
verdicts over the corpus, residuals claimed), so "certified against spec
|
|
76
87
|
X.Y.Z" is a verifiable artifact instead of a claim.
|
|
77
|
-
|
|
88
|
+
10. **The changelog** (`CHANGELOG.md`) — the change history keyed to `VERSION`
|
|
78
89
|
(the top entry must match, held by the spec suite), so a checker certified
|
|
79
90
|
against an earlier version can see exactly what changed since.
|
|
80
|
-
|
|
91
|
+
11. **The rule pages** (`docs/rules/`) — plain-language documentation generated
|
|
81
92
|
from the catalog (`tools/generate_rule_docs.py`; regenerate-and-compare
|
|
82
93
|
parity in the spec suite, so the pages cannot drift from the data).
|
|
83
94
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.26.0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "backend/no_manual_lease_columns",
|
|
3
|
+
"surface": "backend",
|
|
4
|
+
"title": "Application tables do not re-derive leases; expiring custody of work is a platform primitive",
|
|
5
|
+
"intent": "Exclusive, time-bounded custody of a unit of work belongs to the platform, not to each table that needs it. An application table must not declare its own lease bookkeeping \u2014 a holder column paired with an expiry, a heartbeat stamp, or an equivalent claim deadline \u2014 because the hand-rolled form reliably omits the part that makes a lease safe. Expiry alone establishes that a holder may have died; it does not prevent that holder, if it merely paused, from waking after its deadline and completing work a successor has already taken over. A conformant platform supplies custody that is fenced (a monotonic grant token every write is matched against, so a superseded holder is refused rather than trusted), taken atomically with the state change it guards, renewable by a heartbeat that fails closed when the grant is lost, and recoverable \u2014 an expired grant must trigger the owning domain's declared recovery, so work a crashed holder abandoned returns to a retryable state instead of requiring manual repair. Platform-owned delivery infrastructure that claims batches of its own rows, and needs no domain recovery, is a reviewed exception rather than an application pattern.",
|
|
6
|
+
"layer": "static-portable",
|
|
7
|
+
"enforcement": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "build-time",
|
|
10
|
+
"tool": "terp.arch",
|
|
11
|
+
"ref": "check_no_manual_lease_columns"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"reference": "LeaseResource names the leased row or domain mutex; hold_lease / acquire_lease take it through the configured LeaseStore, LeaseGuard.heartbeat renews it and raises when the grant was lost, and register_lease_reaper declares the per-kind recovery an expiry triggers. terp-cap-leases keeps the grants in the app's own database so a claim commits with the row change it protects.",
|
|
15
|
+
"opt_out": "# arch-allow-no-manual-lease-columns: <reason>",
|
|
16
|
+
"runtime": {
|
|
17
|
+
"applicability": "not-applicable",
|
|
18
|
+
"rationale": "Column presence is observable at runtime but not attributable. Composition shares one model-metadata registry across every installed distribution, and the platform's own durable-delivery table legitimately declares exactly these columns for its batch claim \u2014 so a running app cannot tell an application's hand-rolled lease from the framework's sanctioned one (the same non-attribution as the import-form egress and background-runtime rules). Nor can a module manifest close the gap: it carries routers and jobs, never a model reference to attribute a column through. The lease seam's own fail-closed controls \u2014 the fence that refuses a superseded holder's write, the heartbeat that raises rather than returning a flag, and the boot guard that refuses a store whose grants die with their holder \u2014 protect the sanctioned path; they cannot see a table that never joined it. The paired protection is therefore constructive rather than preventive: a maintained primitive that is strictly better than the hand-rolled shape, offered where the hand-rolled shape used to be the only option."
|
|
19
|
+
},
|
|
20
|
+
"guide_topic": "leases",
|
|
21
|
+
"corpus": true
|
|
22
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "frontend/layout-contract",
|
|
3
3
|
"surface": "frontend",
|
|
4
4
|
"title": "An opted-in app's archetype body slots accept only the contract's components",
|
|
5
|
-
"intent": "With a checked-in layout
|
|
5
|
+
"intent": "With a checked-in layout declaration, each page archetype's body slot is constrained to the contract's sanctioned components, so screens stay structurally consistent and the failure message tells an agent exactly how to build the screen. Paired with the fail-closed runtime check on the rendered slot. The declaration is the single source for the opt-in, for the palette the app opens on, and for the shell's own shape down to the navigation groups a module's items name by id and the mark the app is recognised by (layout-declaration.schema.json): a build-time checker and the running app read the same document, so neither can enforce a contract the other does not, and a key declared both in the document and in the app's own code is refused rather than resolved by an invisible precedence.",
|
|
6
6
|
"layer": "static-bespoke",
|
|
7
7
|
"enforcement": [
|
|
8
8
|
{
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"opt_out": "// terp-allow-layout-contract: <reason>",
|
|
21
|
-
"reference": "frontend/layout-contract.json
|
|
21
|
+
"reference": "frontend/layout-contract.json, the document layout-declaration.schema.json describes (ADR 0079); the app imports it and the reference stack's bootstrap resolves it, refusing an unknown key, a value outside its enum, and any key declared twice. The per-stack values: contracts are the keys of LAYOUT_CONTRACTS in @terpjs/react-core, and defaultTheme names one of the palettes @terpjs/contract compiles or the reserved system. Neither list is written out here: the contract ids and the palette names both live in @terpjs/react-core's published layout.manifest.json, with the values filled in for the release an app is pinned to, and every copy of them in that stack is gated against it. A fourth copy in this field would be the one nothing checks, and it would sit in the very field this schema points a reader at for the truth. shell.navGroups is groupNav's group table in @terpjs/react-core, whose NavItem.group references it; shell.brand names paths Vite serves from the app's own frontend/public, rendered into AppShell's logo and logoDark slots. verifySlotChildren in @terpjs/react-core is the runtime DOM check.",
|
|
22
22
|
"runtime": {
|
|
23
23
|
"applicability": "required"
|
|
24
24
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from sqlmodel import Field
|
|
4
|
+
from terp.core import BaseTable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RunRequest(BaseTable, table=True):
|
|
8
|
+
"""The row records its own lifecycle; custody is the platform's, keyed on the row."""
|
|
9
|
+
|
|
10
|
+
__tablename__ = "run_request"
|
|
11
|
+
|
|
12
|
+
connection_id: uuid.UUID = Field(index=True)
|
|
13
|
+
status: str = Field(max_length=16, index=True)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from terp.core import (
|
|
2
|
+
AuditAction,
|
|
3
|
+
BaseService,
|
|
4
|
+
LeaseResource,
|
|
5
|
+
hold_lease,
|
|
6
|
+
register_lease_reaper,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from app.modules.requests.models import RunRequest
|
|
10
|
+
from app.modules.requests.schemas import RunRequestCreate, RunRequestUpdate
|
|
11
|
+
|
|
12
|
+
CLAIMED = "claimed"
|
|
13
|
+
QUEUED = "queued"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RunRequestService(BaseService[RunRequest, RunRequestCreate, RunRequestUpdate]):
|
|
17
|
+
model = RunRequest
|
|
18
|
+
|
|
19
|
+
def __init__(self, holder: str) -> None:
|
|
20
|
+
self._holder = holder
|
|
21
|
+
|
|
22
|
+
def _after_write(self, session, entity, action):
|
|
23
|
+
"""Take the lease inside the write that claims the row, so the two agree.
|
|
24
|
+
|
|
25
|
+
A resource somebody else holds raises here, before the write commits, so the row
|
|
26
|
+
never reaches ``claimed`` at all — there is no compensating update to forget.
|
|
27
|
+
"""
|
|
28
|
+
if entity.status == CLAIMED:
|
|
29
|
+
hold_lease(
|
|
30
|
+
session,
|
|
31
|
+
LeaseResource.for_row(entity),
|
|
32
|
+
holder=self._holder,
|
|
33
|
+
ttl_seconds=60,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def requeue_stale_request(session, lease) -> None:
|
|
38
|
+
"""The recovery an expired lease triggers: put the abandoned row back in the queue."""
|
|
39
|
+
service = RunRequestService(holder="reaper")
|
|
40
|
+
row = service.get(session, lease.resource.key)
|
|
41
|
+
service.update(session, row.id, RunRequestUpdate(status=QUEUED, version=row.version))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
register_lease_reaper("run_request", requeue_stale_request)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from terp.core import BaseSchema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LeaseRead(BaseSchema):
|
|
7
|
+
"""A read DTO may surface who holds a lease and until when — it declares no column.
|
|
8
|
+
|
|
9
|
+
Only a persisted column on a table model is refused; showing an operator what is
|
|
10
|
+
stuck is exactly what the primitive is for.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
holder: str | None
|
|
14
|
+
locked_until: datetime | None
|
|
15
|
+
heartbeat_at: datetime | None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from sqlmodel import Field
|
|
4
|
+
from terp.core import BaseTable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RunRequest(BaseTable, table=True):
|
|
8
|
+
"""A queue row that re-derives custody on itself: a holder plus a deadline.
|
|
9
|
+
|
|
10
|
+
Nothing here can refuse a holder that merely paused: it wakes past its deadline,
|
|
11
|
+
finds its own name still in ``locked_by``, and finishes over the successor that
|
|
12
|
+
already took the work.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__tablename__ = "run_request"
|
|
16
|
+
|
|
17
|
+
status: str = Field(max_length=16, index=True)
|
|
18
|
+
locked_by: str | None = Field(default=None, max_length=128)
|
|
19
|
+
locked_until: datetime | None = None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from sqlmodel import Field
|
|
4
|
+
from terp.core import BaseTable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PipelineRun(BaseTable, table=True):
|
|
8
|
+
"""The same defect in its other common spelling: a heartbeat stamp on the row.
|
|
9
|
+
|
|
10
|
+
A reader still cannot tell "working" from "died", because nothing declares how long
|
|
11
|
+
a gap in the heartbeat is allowed to be, and nothing walks the run back when it is.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__tablename__ = "pipeline_run"
|
|
15
|
+
|
|
16
|
+
status: str = Field(max_length=16, index=True)
|
|
17
|
+
heartbeat_at: datetime | None = None
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|