diff --git a/.orly/orly.json b/.orly/orly.json index 5af4fce..b7e2366 100644 --- a/.orly/orly.json +++ b/.orly/orly.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "orly_version": "0.14.6", + "orly_version": "0.14.7", "packs": [ "domain.auth", "domain.changelog", diff --git a/README.md b/README.md index da1fc9a..c7c04a7 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ TypeSafe's Jev model supplies runtime advice through explicit `orly judge` reque See [releases](https://github.com/agentsfleet/orly/releases) for published versions. The install commands below fetch the published package. -The release workflow publishes 0.14.6 after its version change merges to `main`. +The release workflow publishes 0.14.7 after its version change merges to `main`. --- diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d44e818..82121cb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.14.7 — 2026-10-17 + +API instants become RFC 3339 strings, and agentsfleet's schema stores them as `TIMESTAMPTZ(3)` under a clock rule that reads the time after a lock wait. + +- **REST guide** — `_at` fields are RFC 3339 strings in UTC, OpenAPI `format: date-time`; epoch integers no longer represent instants, and filters state their boundaries. +- **Schema conventions** — timestamps are `TIMESTAMPTZ(3)`; lifecycle stamps come from `now()`, locking validity checks lock first and then read `clock_timestamp()`, and sweeps compare against `statement_timestamp()`. + ## 0.14.6 — 2026-10-17 agentsfleet's schema conventions no longer require migrate to refuse an older migrator's ledger. diff --git a/docs/REST_API_DESIGN_GUIDELINES.md b/docs/REST_API_DESIGN_GUIDELINES.md index 2062047..c23a5f0 100644 --- a/docs/REST_API_DESIGN_GUIDELINES.md +++ b/docs/REST_API_DESIGN_GUIDELINES.md @@ -35,7 +35,7 @@ exception; an unchecked item cannot silently become a pass. - [ ] **List envelope** is exactly `{items, total: int|null, next_cursor: string|null}` — no synonyms (§3) - [ ] **Bulk endpoints** use `207` with the canonical per-item shape (§3) - [ ] **Null vs omit** — absent optionals omitted, `null` reserved for "explicitly cleared" (§3) -- [ ] **Datetime fields** are int64 epoch ms with `_at` suffix (NOT ISO 8601) (§4) +- [ ] **Datetime fields** are RFC 3339 strings in UTC (`Z`) with `_at` suffix; no epoch integers (§4) - [ ] **Duration fields** are integers with `_ms` or `_seconds` suffix; no bare `timeout`/`ttl`/`duration` (§4) - [ ] **Status codes** — 409 includes `current_state`; 412 includes `etag`; 429 includes `Retry-After` + `X-RateLimit-*` (§4) - [ ] **ETag/`If-Match`** wired for any resource with realistic concurrent edits (§4) @@ -120,7 +120,7 @@ If the parent ID is in the path, never repeat it in the body. - **Plural for collections, singular for items** - **`_id` suffix** for identifiers (`product_id`, `agent_id`) — never bare `id` except as the resource's own primary key field -- **`_at` suffix** for datetimes; values are int64 epoch milliseconds (§4) +- **`_at` suffix** for datetimes; values are RFC 3339 strings in UTC (§4) - **Unit suffix** for durations: `_ms` or `_seconds` (§4) — never bare `timeout`/`ttl`/`interval`/`duration`/`expiration` - **Adjectives before nouns** (`completed_items`, not `items_completed`) - **No `is_` prefix** on booleans (`enabled`, not `is_enabled`) @@ -173,7 +173,7 @@ Endpoints that can't complete inside the request window MUST follow this convent 1. The kicking POST returns `202 Accepted` with `Location: /v1/operations/{operation_id}` and a body containing `operation_id` and `status: "pending"`. 2. `GET /v1/operations/{operation_id}` returns: ```json - { "operation_id": "...", "done": false, "status": "running", "started_at": 1735689600000 } + { "operation_id": "...", "done": false, "status": "running", "started_at": "2026-10-17T18:06:49.123Z" } ``` When `done: true`, the body carries either `result` (the final resource, inline) or `error` (an RFC 7807 envelope per §5). 3. Terminal states are `succeeded`, `failed`, `cancelled`. No others. @@ -213,7 +213,7 @@ The resource itself, no envelope: { "id": "01HZQ...", "name": "platform-ops", - "created_at": 1735689600000 + "created_at": "2026-10-17T18:06:49.123Z" } ``` @@ -227,7 +227,7 @@ GET /products?status=active&sort=-created_at&starting_after=01HZQ...&limit=50 - **Filtering grammar — exactly one form per case:** - **Equality:** `?status=active` (single value), `?status=active,paused` (multi-value, comma-separated, single key). - - **Time ranges:** `?created_after=&created_before=`. Bracket grammar (`?created_at[gte]=...`) is forbidden. + - **Time ranges:** `?created_after=&created_before=`, both exclusive; an endpoint that differs states its boundaries in the parameter description. Bracket grammar (`?created_at[gte]=...`) is forbidden. - **No boolean explosions.** Don't add `?include_x=true&include_y=true` — use `?include=x,y` with a documented enum of legal values, OR don't expose a knob. - **Sorting:** `sort=field` ascending; `sort=-field` descending. Single sort key per request — no multi-key. - **Pagination — keyset.** Request: `?starting_after=&limit=`; @@ -318,13 +318,22 @@ If you find yourself wanting another status code, check RFC 7231/7807 — don't ### Datetime fields -**Integer milliseconds since Unix epoch (UTC), serialized as JSON number, OpenAPI `type: integer, format: int64`.** Field name uses the `_at` suffix. +**RFC 3339 strings, serialized as JSON strings, OpenAPI `type: string, format: date-time`.** Field name uses the `_at` suffix. ```json -"created_at": 1735689600000 +"created_at": "2026-10-17T18:06:49.123Z" ``` -NOT ISO 8601 strings, NOT seconds, NOT floats, NOT a string-encoded integer. This matches the codebase (`EXTRACT(EPOCH FROM created_at) * 1000` in handlers; `i64` in response structs; `format: int64` in `components/schemas.yaml`). Anyone proposing ISO 8601 string datetimes for a new endpoint is wrong — match the existing convention or amend this rule first. +1. Responses MUST emit UTC with the `Z` designator. +2. Requests MAY carry any valid numeric UTC offset. The server MUST normalize an accepted instant to UTC. +3. Epoch integers MUST NOT represent an instant. +4. An invalid or out-of-range instant MUST return `400` with a field-specific `detail` (§5). +5. Timestamp filters MUST document whether each boundary is inclusive or exclusive. An opaque pagination cursor MUST carry the complete ordering key at the database's precision. +6. Shared validation MUST enforce the documented input range and precision. + + +For `agentsfleet`, wire types hold `jiff::Timestamp` and use its standard serialization; no handler converts an instant by hand. Postgres stores instants as `TIMESTAMPTZ(3)`, so the API, the cursor and the database agree on millisecond precision. + When the field has not yet been set (e.g. an unrevoked key's `revoked_at`), serialize as JSON `null` and declare `nullable: true` in the schema. diff --git a/docs/RULE_ENFORCEMENT.md b/docs/RULE_ENFORCEMENT.md index 1253773..89cee40 100644 --- a/docs/RULE_ENFORCEMENT.md +++ b/docs/RULE_ENFORCEMENT.md @@ -7,14 +7,14 @@ regenerates this file and fails on any difference. To update it, run | Rule document | enforced by | judged | acknowledged | classified | trigger | |---|---|--:|--:|--:|---| | `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 10 | 13 | 30/30 | mechanical | -| `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/69 | mechanical | -| `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/12 | mechanical | +| `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/75 | mechanical | +| `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/13 | mechanical | | `docs/DOCUMENTATION_RULES.md` | — | 0 | 0 | 0/22 | latent | | `docs/LIFECYCLE_PATTERNS.md` | — | 0 | 0 | 0/22 | mechanical | | `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/3 | latent | | `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/4 | uncited | | `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/115 | mechanical | -| **corpus** | | | | **30/277** | | +| **corpus** | | | | **30/284** | | ## How to read this diff --git a/docs/SCHEMA_CONVENTIONS.md b/docs/SCHEMA_CONVENTIONS.md index 2aae353..bc0acf6 100644 --- a/docs/SCHEMA_CONVENTIONS.md +++ b/docs/SCHEMA_CONVENTIONS.md @@ -93,16 +93,21 @@ Every root a user or operator can delete — draft, user, Library card, fleet, w ## Timestamps -- **Type:** `BIGINT NOT NULL` — milliseconds since the Unix epoch, from the application clock (`afd_core::clock::UnixMillis`). -- **Never** `TIMESTAMPTZ`, `TIMESTAMP` or `DEFAULT now()`. +- **Type:** `TIMESTAMPTZ(3) NOT NULL` (nullable for a NULL marker), so the database, the Rust `jiff::Timestamp` and the API agree on millisecond precision. Never `BIGINT` epoch integers or `TIMESTAMP` without a time zone. +- **Server lifecycle stamps** (`created_at`, `updated_at`) are written by the statement as `now()`: one database clock, one instant per transaction. No column `DEFAULT`; every write names its stamp in its `.sql` file. +- **Deadlines** are computed in SQL as `clock_timestamp() + $ttl::interval`, with the TTL bound as an exact interval. +- **A locking validity check** ("is this lease still live?") locks the target rows first, then reads `clock_timestamp()` once in a `MATERIALIZED` step that depends on the locked rows. A clock read in the `WHERE` or select list of a locking statement can run before the lock wait and accept an expired row. +- **A non-locking read or `SKIP LOCKED` sweep** compares against `statement_timestamp()`, which an index can use; `clock_timestamp()` cannot be an index bound. +- **`now()` and `CURRENT_TIMESTAMP` never decide an expiry**, because both are fixed at transaction start. +- A time supplied by another system is stored as given in its own column, apart from the received or processed stamp. ## Standard Columns | Column | Type | Required | Notes | |--------|------|----------|-------| | `id` | `UUID PRIMARY KEY` | Yes | UUIDv7, application-minted | -| `created_at` | `BIGINT NOT NULL` | Yes | Set once at INSERT | -| `updated_at` | `BIGINT NOT NULL` | If the row ever changes | Set at INSERT and every UPDATE | +| `created_at` | `TIMESTAMPTZ(3) NOT NULL` | Yes | `now()` at INSERT | +| `updated_at` | `TIMESTAMPTZ(3) NOT NULL` | If the row ever changes | `now()` at INSERT and every UPDATE | ## Audit Pattern diff --git a/package.json b/package.json index 4639fdb..762faad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentsfleet/orly", - "version": "0.14.6", + "version": "0.14.7", "description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.", "license": "MIT", "type": "module",