From 806affb6d30b753b0a799ba76cffea997eae8d05 Mon Sep 17 00:00:00 2001 From: Kishore Kumar Date: Sat, 17 Oct 2026 23:45:47 +0530 Subject: [PATCH 1/2] chore(release): orly 0.14.7 API instants become RFC 3339 strings in UTC (OpenAPI date-time) and epoch integers no longer represent them. agentsfleet's schema stores instants as TIMESTAMPTZ(3), stamps lifecycle columns with now(), and reads the clock after a lock wait in locking validity checks. Co-Authored-By: Claude Opus 5.5 --- .orly/orly.json | 2 +- README.md | 2 +- docs/CHANGELOG.md | 7 +++++++ docs/REST_API_DESIGN_GUIDELINES.md | 25 +++++++++++++++++-------- docs/RULE_ENFORCEMENT.md | 6 +++--- docs/SCHEMA_CONVENTIONS.md | 13 +++++++++---- package.json | 2 +- 7 files changed, 39 insertions(+), 18 deletions(-) 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..fe856da 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 in a new field. Replacing an existing integer field is a type change (§9) and needs a recorded compatibility or versioning decision first. +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", From 201318692725504d0975154e49a383d27ec04a07 Mon Sep 17 00:00:00 2001 From: Kishore Kumar Date: Sat, 17 Oct 2026 23:50:36 +0530 Subject: [PATCH 2/2] docs(rest): epoch integers never represent an instant Drops the clause asking for a compatibility or versioning decision before replacing an existing integer field: agentsfleet tears its database down and ships every client with the change, so there is nothing to stay compatible with. Co-Authored-By: Claude Opus 5.5 --- docs/REST_API_DESIGN_GUIDELINES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/REST_API_DESIGN_GUIDELINES.md b/docs/REST_API_DESIGN_GUIDELINES.md index fe856da..c23a5f0 100644 --- a/docs/REST_API_DESIGN_GUIDELINES.md +++ b/docs/REST_API_DESIGN_GUIDELINES.md @@ -326,7 +326,7 @@ If you find yourself wanting another status code, check RFC 7231/7807 — don't 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 in a new field. Replacing an existing integer field is a type change (§9) and needs a recorded compatibility or versioning decision first. +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.