Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .orly/orly.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"orly_version": "0.14.6",
"orly_version": "0.14.7",
"packs": [
"domain.auth",
"domain.changelog",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

---

Expand Down
7 changes: 7 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
25 changes: 17 additions & 8 deletions docs/REST_API_DESIGN_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
}
```

Expand All @@ -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=<ts_ms>&created_before=<ts_ms>`. Bracket grammar (`?created_at[gte]=...`) is forbidden.
- **Time ranges:** `?created_after=<RFC 3339>&created_before=<RFC 3339>`, 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=<resource_id>&limit=<int>`;
Expand Down Expand Up @@ -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.

<!-- oracle-packs:start product.agentsfleet -->
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.
<!-- oracle-packs:end -->

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.

Expand Down
6 changes: 3 additions & 3 deletions docs/RULE_ENFORCEMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 9 additions & 4 deletions docs/SCHEMA_CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading