From 89eed55c30b2e4237f673307f6b02c44dd32321c Mon Sep 17 00:00:00 2001 From: Anton Kundenko Date: Sat, 11 Jul 2026 10:44:37 +0200 Subject: [PATCH 01/14] v2.4.0 (#327) From eb66eed863b52d4fc992f69f96c1be1d1fc042c2 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 14 Jul 2026 20:55:53 +0200 Subject: [PATCH 02/14] feat(query): support live inserts into parted tables Add immutable live-tail growth with explicit partition keys, shared FILE-domain symbol handling, atomic symbol rebinding, adversarial coverage, documentation, and a runnable rollover example. --- docs/docs/language/functions.md | 23 +- docs/docs/namespaces/db.md | 70 +- docs/docs/reference/all-functions.md | 4 +- examples/rfl/parted_live_tail.rfl | 97 ++ src/ops/query.c | 1080 ++++++++++++++++++- test/test_lang.c | 1453 ++++++++++++++++++++++++++ 6 files changed, 2717 insertions(+), 10 deletions(-) create mode 100644 examples/rfl/parted_live_tail.rfl diff --git a/docs/docs/language/functions.md b/docs/docs/language/functions.md index a35f3ef9..f41add19 100644 --- a/docs/docs/language/functions.md +++ b/docs/docs/language/functions.md @@ -238,7 +238,7 @@ These are **special forms** that bridge to the Rayforce DAG executor. |---|---|---| | `select` | variadic, special | Query table with optional filter, projection, grouping, and aggregation | | `update` | variadic, special | Add or modify columns in a table | -| `insert` | variadic, special | Append a row to a table, append to a vector/list, or insert at position(s) | +| `insert` | variadic, special | Append to a flat table/collection, or grow the live tail of a parted table | | `upsert` | variadic, special | Insert or update rows (by key) | ```lisp @@ -258,12 +258,20 @@ These are **special forms** that bridge to the Rayforce DAG executor. notional: (* price size)}) ``` -`insert` overloads on arity. With two arguments it appends; with three arguments it inserts at a position (or positions) given by the second argument. The first argument is a quoted symbol for in-place mutation, e.g. `'v`, or any expression that evaluates to a table/vector/list. +`insert` has two table forms: + +- `(insert target rows)` is the ordinary two-argument form. It returns a new flat table, or rebinds and returns a quoted target symbol such as `'trades`. The row payload can be a list, table, or dictionary, as described by the target's physical columns. +- `(insert parted partition-key rows)` grows the in-memory live tail of a parted table and returns a fresh logical view, leaving `parted` unchanged. Quote a symbol — `(insert 'parted partition-key rows)` — to rebind that symbol to the fresh view and return the symbol. A validated zero-row batch is a no-op and returns the existing view without rebinding. This is distinct from the three-argument positional form for vectors and lists, where the second argument is an insertion index. ```lisp ; Append a row to a table (insert 'trades (list 'AAPL 150.0 100 12)) +; Append two rows to the live 2024.01.16 partition of a parted table. +; `date` is virtual, so the payload contains only physical data columns. +(insert 'trades 2024.01.16 + (list ['AAPL 'MSFT] [151.0 410.0] [200 75] [13 14])) + ; Vector / list operations (set v (til 5)) ; [0 1 2 3 4] (insert 'v 99) ; append: [0 1 2 3 4 99] @@ -275,6 +283,17 @@ These are **special forms** that bridge to the Rayforce DAG executor. Indices are *pre-insertion* positions in `[0, count]`; `idx == count` is equivalent to append. Vector positional inserts of a same-typed vector splice that vector in. List positional inserts always add the value as a single slot — use `concat` to splice. Multi-insert is stable on duplicate indices, preserving input order. Typed-null atoms (`0Nl`, `0Nf`, …) carry their null flag through — the inserted slot is marked null, not zero. +For a parted target, `rows` must match the physical data-column schema exactly in name and concrete vector type. Do **not** include the virtual `date` or `part` column. A list maps values in physical column order; a table or dictionary may reorder columns, but must contain every physical name exactly once. Atoms append one row. Equal-length vectors append a batch, and atom values alongside them broadcast across that batch. Generic `null` is accepted for sentinel-nullable columns; `SYM` and `STR` map it to their empty value because those types have no distinct null, while `BOOL` and `U8` reject it because they are non-nullable. + +The partition key must equal the current last key, which grows that live segment, or be strictly later, which starts a new live segment. Earlier keys — including an existing historical partition — are immutable and cannot be inserted into. Existing partition metadata must already be strictly increasing in logical key order; use zero-padded integer directory names when lexical directory order would otherwise disagree with numeric order. Every historical physical segment must be present. A missing segment in the last partition can be repaired only by a non-empty same-key append; it blocks an empty insert or a move to a later key. `BOOL`/`U8` cannot be null-backfilled when that missing segment already represents existing rows, but a missing zero-row segment needs no backfill. + +A non-empty functional insert returns a new logical table view; the quoted-symbol form publishes that view by rebinding the symbol. Historical mmap segments are retained without copying, while a segment receiving rows becomes heap-backed. Queries and values that already retained the previous table continue to see that snapshot; subsequent resolution of a rebound target symbol sees the newly appended rows. A non-empty same-key append rebuilds partition metadata and copies the active segment; a later key materializes a new tail instead. Batch incoming rows to avoid repeatedly copying a growing intraday segment. + +!!! warning "Live-tail insert is not persistence" + Parted `insert` changes memory only. It does not modify partition directories, column files, `.d`, or `.sym`; process exit loses an unpersisted tail. Persist the completed physical partition explicitly at rollover, then reload the parted table if it should return to a fully mmap-backed view. See [`.db.parted.get`](../namespaces/db.md#db-parted-get) for the production pattern. + +`upsert` is not supported on parted tables. Materialize a flat table or use an application-level update strategy when key-based replacement is required. + ## Joins Rayforce supports equi-joins, outer joins, anti-joins, and time-series-aware joins. diff --git a/docs/docs/namespaces/db.md b/docs/docs/namespaces/db.md index 58e0f6e3..376d59e0 100644 --- a/docs/docs/namespaces/db.md +++ b/docs/docs/namespaces/db.md @@ -5,7 +5,7 @@ Persist and reload tables. Rayforce stores tables in two on-disk shapes: - **Splayed**: one directory per table, one file per column, plus a `.d` schema file and a `.sym` symbol-table file. The standard layout for a single-table dataset. - **Partitioned (parted)**: a database root containing one subdirectory per partition (date or other numeric/dotted name); each partition contains splayed-style table directories. A single shared `.sym` file sits at the root. The query optimizer can prune partitions when predicates select on the virtual `MAPCOMMON` partition column. -The `get` builtins memory-map every column file — load is constant-time regardless of dataset size. `set` writes a table's columns to a splayed directory. +The `get` builtins memory-map every column file — load is constant-time regardless of dataset size. `set` writes a table's columns to a splayed directory. A loaded parted table can also grow an in-memory live tail with `insert`; that operation is deliberately separate from persistence. !!! note "Restricted under `-U`" `.db.splayed.set` and `.db.parted.fill` are `RAY_FN_RESTRICTED` (they write to disk). The `get`/`tables` builtins are read-only and unrestricted. @@ -61,8 +61,10 @@ Returns a single logical table assembled from every partition directory under `d ```lisp (set dbroot "/tmp/rayforce-parted-db") (set symfile (format "%/.sym" dbroot)) -(set jan15 (table [sym price qty] (list [AAPL GOOG] [150.5 2800.0] [100 50]))) -(set jan16 (table [sym price qty] (list [MSFT] [410.0] [75]))) +(set jan15 (table ['sym 'price 'qty] + (list ['AAPL 'GOOG] [150.5 2800.0] [100 50]))) +(set jan16 (table ['sym 'price 'qty] + (list ['MSFT] [410.0] [75]))) (.db.splayed.set (format "%/2024.01.15/trades" dbroot) jan15 symfile) (.db.splayed.set (format "%/2024.01.16/trades" dbroot) jan16 symfile) (set trades (.db.parted.get dbroot 'trades)) @@ -73,6 +75,68 @@ Returns a single logical table assembled from every partition directory under `d Errors: `domain` (arity != 2 or `tbl_name` invalid), `type` (root not a string or name not a sym), `name` (sym ID unknown). +### In-memory live-tail inserts + +Use `(insert parted partition-key rows)` to build a fresh logical view containing immutable history and a growing current partition; the source value remains unchanged. Quote a bound symbol, as in `(insert 'parted partition-key rows)`, to rebind it to that fresh view. A validated zero-row batch returns the existing view without rebinding or creating a partition. The source is normally a table returned by `.db.parted.get`. + +The row payload describes the physical splayed schema only: omit the virtual `date` or `part` column, and supply exactly one matching value per physical column. A list follows physical column order. A table or dictionary may reorder its payload, but must contain every physical column name exactly once. Atoms append one row; equal-length vectors append a batch, with atom values broadcast across that batch. Concrete vector types must match exactly. Generic `null` is accepted for sentinel-nullable columns; it becomes the empty value for `SYM`/`STR`, while non-nullable `BOOL`/`U8` reject it. + +The key may equal the table's last partition key, growing the current segment, or be strictly later, starting a new current segment. Inserts into any earlier or non-last partition are rejected; historical partitions are immutable. The loaded partition keys must already be strictly increasing in their logical type. In particular, zero-pad integer directory names if their lexical order would otherwise differ from numeric order. All historical physical segments must be present. A missing segment in the last partition can be repaired only by a non-empty same-key append; until repaired it blocks advancing the key. Missing `BOOL`/`U8` cannot be null-backfilled when the partition already has rows, but a zero-row segment needs no backfill. + +For a non-empty insert, the quoted-symbol form rebinds the target to the new logical view. Existing historical mmap segments are retained, while a segment receiving live rows becomes heap-backed. Queries and values that already retained the previous table remain stable snapshots; queries that resolve the symbol after the rebind see the new rows. Same-key growth rebuilds partition metadata and copies only the active segment; a later key builds a new tail. Batch incoming rows to avoid repeatedly copying a growing intraday segment. + +!!! warning "Memory only — no implicit durability" + Live-tail insert does **not** create or change any on-disk partition, column, `.d`, or `.sym` file. An unpersisted tail is lost when the process exits. `upsert` is not supported on parted tables. + +All `SYM` segments in the parted table share the FILE domain loaded from the database's `.sym`. Existing rows store stable positions in that domain. A live insert resolves existing symbols there and appends novel symbols to an in-memory extension shared by every symbol column and partition; it never rewrites historical positions. Live insert alone persists neither the new vocabulary nor its rows. Explicit `.db.splayed.set` rollover writes the enlarged `.sym` first; if a later column write fails, unused vocabulary entries can remain durable and the affected physical partition must be repaired or rewritten before reload. + +A typical production cycle loads history once, batches rows into the live tail during the day, then explicitly persists the completed physical partition at rollover. Serialize rollover with ingestion so the projected snapshot is stable: + +```lisp +(set dbroot "/data/market") +(set symfile (format "%/.sym" dbroot)) +(set trades (.db.parted.get dbroot 'trades)) + +;; Named batches match physical columns by name and may reorder them. +;; `date` is virtual and therefore absent from the payload. +(insert 'trades 2024.01.16 + {qty: [200 75] + sym: ['AAPL 'MSFT] + price: [151.0 410.0]}) + +;; A positional list follows physical order: [sym price qty]. +;; The scalar price broadcasts across the two-row vector batch. +(insert 'trades 2024.01.16 + (list ['AAPL 'GOOG] 151.25 [50 25])) + +;; Disk history and the heap-backed current day query as one table. +(select {from: trades where: (== date 2024.01.16)}) + +;; Rollover: stop/serialize ingestion, project away the virtual column, +;; persist the complete day explicitly, and reopen mmap-backed history. +(set closed-day + (select {from: trades + where: (== date 2024.01.16) + sym: sym price: price qty: qty})) +(.db.splayed.set + (format "%/2024.01.16/trades" dbroot) closed-day symfile) +(set trades (.db.parted.get dbroot 'trades)) + +;; The next strictly later key starts the new live tail. +(insert 'trades 2024.01.17 (list 'AAPL 152.0 100)) + +;; Several later memory-only dates may coexist. After advancing to the 18th, +;; only that last key may grow; another insert into the 17th is rejected. +(insert 'trades 2024.01.18 (list 'MSFT 412.0 40)) +``` + +A complete, repeatable version is available in +[`examples/rfl/parted_live_tail.rfl`](https://github.com/RayforceDB/rayforce/blob/master/examples/rfl/parted_live_tail.rfl). It creates a small database under `/tmp`, demonstrates both batch forms and unified queries, performs rollover/reload, creates multiple live dates, and cleans up afterward: + +```bash +./rayforce examples/rfl/parted_live_tail.rfl +``` + ## `.db.parted.tables` { #db-parted-tables } Signature: `(.db.parted.tables "db_root")`. diff --git a/docs/docs/reference/all-functions.md b/docs/docs/reference/all-functions.md index 20fab102..de933e63 100644 --- a/docs/docs/reference/all-functions.md +++ b/docs/docs/reference/all-functions.md @@ -444,8 +444,8 @@ Special forms that bridge to the Rayforce DAG executor for high-performance colu | `select` | variadic | special | Query table with optional filter, projection, grouping, and aggregation | `(select {from: t a: a})` | | `window` | variadic | special | Partitioned window query. `frame:` is `'whole`, `'running`, or a positive trailing row count | `(window {from: t part: [sym] order: [time] frame: 5 funcs: {avg5: (avg price)}})` | | `update` | variadic | special, restricted | Add or modify columns in a table (mutates in-place) | `(update {from: t b: (* a 2)})` | -| `insert` | variadic | special, restricted | Insert rows into a table | `(insert t {x: 10 y: 20})` | -| `upsert` | variadic | special, restricted | Insert or update rows by key match (target, key, row) | `(upsert t 'x {x: 10 y: 20})` | +| `insert` | variadic | special, restricted | Append rows, or grow a parted table's in-memory live tail with an explicit partition key | `(insert 't 2024.01.16 rows)` | +| `upsert` | variadic | special, restricted | Insert or update flat-table rows by key match (target, key, row) | `(upsert t 'x {x: 10 y: 20})` | ```lisp ; Select with filter and projection diff --git a/examples/rfl/parted_live_tail.rfl b/examples/rfl/parted_live_tail.rfl new file mode 100644 index 00000000..01935694 --- /dev/null +++ b/examples/rfl/parted_live_tail.rfl @@ -0,0 +1,97 @@ +;; parted_live_tail.rfl - query mmap history plus an in-memory live tail +;; Usage: ./rayforce examples/rfl/parted_live_tail.rfl + +(set dbroot "/tmp/rayforce_parted_live_tail") +(set symfile (format "%/.sym" dbroot)) + +;; Build a repeatable two-day historical fixture. +(.sys.exec "rm -rf /tmp/rayforce_parted_live_tail") + +(set day12 + (table ['ticker 'ts 'price 'qty] + (list ['AAPL 'MSFT] + [2026.07.12D09:30:00.000000000 + 2026.07.12D09:30:01.000000000] + [209.50 505.20] + [100 40]))) + +(set day13 + (table ['ticker 'ts 'price 'qty] + (list ['AAPL 'MSFT] + [2026.07.13D09:30:00.000000000 + 2026.07.13D09:30:01.000000000] + [210.10 506.40] + [80 60]))) + +(.db.splayed.set + (format "%/2026.07.12/trades" dbroot) day12 symfile) +(.db.splayed.set + (format "%/2026.07.13/trades" dbroot) day13 symfile) + +;; Historical partitions are mmap-backed after this load. +(set trades (.db.parted.get dbroot 'trades)) +(println "historical rows=%" (count trades)) + +;; A named batch may put physical columns in any order. `date` is virtual and +;; must not appear in the payload. NVDA extends only the in-memory .sym domain. +(insert 'trades 2026.07.14 + {qty: [100 50] + price: [211.25 172.40] + ticker: ['AAPL 'NVDA] + ts: [2026.07.14D09:30:00.000000000 + 2026.07.14D09:30:01.000000000]}) + +;; A positional list follows physical order: [ticker ts price qty]. +;; Scalars broadcast when another physical column establishes the batch size. +(insert 'trades 2026.07.14 + (list ['AAPL 'MSFT] + 2026.07.14D09:31:00.000000000 + [211.30 507.80] + [75 25])) + +;; History and the live tail query as one logical table. +(show + (select {from: trades + where: (>= date 2026.07.13) + ticker: ticker + date: date + ts: ts + price: price + qty: qty})) + +(show + (select {from: trades + by: {date: date ticker: ticker} + rows: (count qty) + volume: (sum qty)})) + +;; Rollover is explicit: serialize ingestion, project away the virtual date, +;; persist the complete current partition, and reopen mmap-backed history. +(set closed-day + (select {from: trades + where: (== date 2026.07.14) + ticker: ticker + ts: ts + price: price + qty: qty})) + +(.db.splayed.set + (format "%/2026.07.14/trades" dbroot) closed-day symfile) +(set trades (.db.parted.get dbroot 'trades)) +(println "rows after rollover=%" (count trades)) + +;; More than one later in-memory date is supported. Only the last key can grow +;; once ingestion advances; inserting into 2026.07.15 after this is rejected. +(insert 'trades 2026.07.15 + (list 'AAPL 2026.07.15D09:30:00.000000000 212.10 100)) +(insert 'trades 2026.07.16 + (list 'MSFT 2026.07.16D09:30:00.000000000 509.20 50)) + +(show + (select {from: trades + by: {date: date} + rows: (count qty)})) + +;; Keep the example repeatable. Remove this line to inspect the persisted data. +(.sys.exec "rm -rf /tmp/rayforce_parted_live_tail") +(exit 0) diff --git a/src/ops/query.c b/src/ops/query.c index 7a43fecc..458fa1c8 100644 --- a/src/ops/query.c +++ b/src/ops/query.c @@ -41,6 +41,7 @@ #include "ops/temporal.h" #include "core/profile.h" #include "table/sym.h" +#include "table/domain.h" #include "table/dict.h" #include "mem/heap.h" #include "mem/sys.h" @@ -11904,7 +11905,1004 @@ ray_t* ray_update(ray_t** args, int64_t n) { DICT_VIEW_CLOSE(upda); return result; } -/* (insert table (list val1 val2 ...)) — append a row to a table */ +/* A `.db.parted.get` table is a deliberately narrow shape: one leading + * MAPCOMMON virtual column followed by one PARTED wrapper per physical + * column. Keep detection separate from validation so arity-2 insert and + * upsert can reject parted values before any wrapper is mistaken for flat + * vector storage. */ +static bool table_has_parted_columns(ray_t* tbl) { + if (!tbl || RAY_IS_ERR(tbl) || tbl->type != RAY_TABLE) return false; + int64_t ncols = ray_table_ncols(tbl); + for (int64_t c = 0; c < ncols; c++) { + ray_t* col = ray_table_get_col_idx(tbl, c); + if (col && (col->type == RAY_MAPCOMMON || RAY_IS_PARTED(col->type))) + return true; + } + return false; +} + +typedef struct { + ray_t* mapcommon; /* borrowed */ + ray_t* key_values; /* borrowed */ + ray_t* row_counts; /* borrowed */ + int64_t ncols; + int64_t ndata; + int64_t nparts; + int64_t total_rows; + bool active_missing; + int8_t key_type; + struct ray_sym_domain_s* sym_dom; /* borrowed, shared by every data SYM */ + ray_t* sym_rep; /* borrowed SYM segment for domain adoption */ +} parted_insert_view_t; + +static int byte_string_cmp(const char* a, size_t an, + const char* b, size_t bn) { + size_t n = an < bn ? an : bn; + int cmp = n ? memcmp(a, b, n) : 0; + if (cmp < 0) return -1; + if (cmp > 0) return 1; + return (an > bn) - (an < bn); +} + +/* Compare two MAPCOMMON cells under their logical key type. */ +static int parted_key_cells_cmp(ray_t* keys, int64_t a, int64_t b, + bool* valid) { + *valid = true; + if (keys->type == RAY_DATE) { + int32_t av = ((int32_t*)ray_data(keys))[a]; + int32_t bv = ((int32_t*)ray_data(keys))[b]; + if (av == NULL_I32 || bv == NULL_I32) { *valid = false; return 0; } + return (av > bv) - (av < bv); + } + if (keys->type == RAY_I64) { + int64_t av = ((int64_t*)ray_data(keys))[a]; + int64_t bv = ((int64_t*)ray_data(keys))[b]; + if (av == NULL_I64 || bv == NULL_I64) { *valid = false; return 0; } + return (av > bv) - (av < bv); + } + if (keys->type == RAY_SYM) { + ray_t* av = ray_sym_vec_cell(keys, a); + ray_t* bv = ray_sym_vec_cell(keys, b); + if (!av || !bv || ray_str_len(av) == 0 || ray_str_len(bv) == 0) { + *valid = false; + return 0; + } + return byte_string_cmp(ray_str_ptr(av), ray_str_len(av), + ray_str_ptr(bv), ray_str_len(bv)); + } + *valid = false; + return 0; +} + +/* Compare an explicit runtime-domain key atom with one MAPCOMMON cell. */ +static int parted_key_atom_cmp(ray_t* key, ray_t* keys, int64_t cell, + bool* valid) { + *valid = true; + if (keys->type == RAY_DATE) { + int32_t cv = ((int32_t*)ray_data(keys))[cell]; + if (cv == NULL_I32 || RAY_ATOM_IS_NULL(key)) { *valid = false; return 0; } + return (key->i32 > cv) - (key->i32 < cv); + } + if (keys->type == RAY_I64) { + int64_t cv = ((int64_t*)ray_data(keys))[cell]; + if (cv == NULL_I64 || RAY_ATOM_IS_NULL(key)) { *valid = false; return 0; } + return (key->i64 > cv) - (key->i64 < cv); + } + if (keys->type == RAY_SYM) { + ray_t* ka = ray_sym_str(key->i64); + ray_t* cv = ray_sym_vec_cell(keys, cell); + if (!ka || !cv || ray_str_len(ka) == 0 || ray_str_len(cv) == 0) { + *valid = false; + return 0; + } + return byte_string_cmp(ray_str_ptr(ka), ray_str_len(ka), + ray_str_ptr(cv), ray_str_len(cv)); + } + *valid = false; + return 0; +} + +static bool parted_segment_attrs_valid(int8_t base, uint8_t attrs) { + uint8_t allowed = RAY_ATTR_HAS_INDEX | RAY_ATTR_SORTED; + if (base == RAY_SYM) allowed |= RAY_SYM_W_MASK; + if (base == RAY_I32 || base == RAY_I64) + allowed |= RAY_ATTR_HAS_LINK; + if (base == RAY_I16 || base == RAY_I32 || base == RAY_I64 || + base == RAY_F32 || base == RAY_F64 || base == RAY_DATE || + base == RAY_TIME || base == RAY_TIMESTAMP || base == RAY_GUID) + allowed |= RAY_ATTR_HAS_NULLS; + return (attrs & (uint8_t)~allowed) == 0; +} + +/* Validate the complete canonical representation before looking at payload + * values. A NULL segment is tolerated only in the current active partition, + * where a same-key non-empty append can materialise it. Missing historical + * segments are unqueryable by segmented execution and must never survive into + * a newly returned live-tail snapshot. */ +static ray_t* validate_parted_insert_target(ray_t* tbl, + parted_insert_view_t* out) { + memset(out, 0, sizeof(*out)); + if (tbl->len != 2 || tbl->attrs != 0) + return ray_error("corrupt", "insert: parted target has non-canonical table metadata"); + ray_t** table_slots = (ray_t**)ray_data(tbl); + ray_t* schema = table_slots[0]; + ray_t* columns = table_slots[1]; + if (!schema || RAY_IS_ERR(schema) || schema->type != RAY_I64 || + schema->attrs != 0 || schema->len < 0 || + !columns || RAY_IS_ERR(columns) || columns->type != RAY_LIST || + columns->attrs != 0 || columns->len != schema->len) + return ray_error("corrupt", "insert: parted target has a malformed table container"); + int64_t ncols = ray_table_ncols(tbl); + if (ncols < 2) + return ray_error("type", "insert: parted target must have a MAPCOMMON column and at least one physical column"); + + ray_t* mc = ray_table_get_col_idx(tbl, 0); + if (!mc || mc->type != RAY_MAPCOMMON || mc->len != 2 || + mc->attrs > RAY_MC_I64) + return ray_error("type", "insert: three-argument table form requires a canonical .db.parted.get table"); + + ray_t** mp = (ray_t**)ray_data(mc); + ray_t* keys = mp[0]; + ray_t* counts = mp[1]; + int8_t key_type = mc->attrs == RAY_MC_DATE ? RAY_DATE + : mc->attrs == RAY_MC_I64 ? RAY_I64 + : RAY_SYM; + uint8_t key_attrs = key_type == RAY_SYM ? RAY_SYM_W64 : 0; + if (!keys || RAY_IS_ERR(keys) || keys->type != key_type || + keys->attrs != key_attrs || + !counts || RAY_IS_ERR(counts) || counts->type != RAY_I64 || + counts->attrs != 0 || + keys->len <= 0 || counts->len != keys->len) + return ray_error("corrupt", "insert: malformed MAPCOMMON partition metadata"); + if (key_type == RAY_SYM && + ray_sym_vec_domain(keys) != ray_sym_runtime_domain()) + return ray_error("corrupt", "insert: MAPCOMMON symbol keys must use the runtime domain"); + + /* Schema names must be unambiguous before exact named payload gathering. */ + for (int64_t c = 0; c < ncols; c++) { + int64_t name = ray_table_col_name(tbl, c); + if (name < 0) + return ray_error("corrupt", "insert: parted target has an invalid column name at index %lld", (long long)c); + for (int64_t d = c + 1; d < ncols; d++) + if (ray_table_col_name(tbl, d) == name) + return ray_error("corrupt", "insert: parted target has duplicate column names"); + } + + int64_t nparts = keys->len; + int64_t* rc = (int64_t*)ray_data(counts); + int64_t total = 0; + for (int64_t p = 0; p < nparts; p++) { + if (rc[p] < 0 || rc[p] == NULL_I64 || total > INT64_MAX - rc[p]) + return ray_error("corrupt", "insert: invalid MAPCOMMON row count at partition %lld", (long long)p); + total += rc[p]; + if (p > 0) { + bool valid = false; + int cmp = parted_key_cells_cmp(keys, p - 1, p, &valid); + if (!valid || cmp >= 0) + return ray_error("corrupt", "insert: MAPCOMMON partition keys are not strictly increasing"); + } else if (key_type == RAY_DATE) { + if (((int32_t*)ray_data(keys))[0] == NULL_I32) + return ray_error("corrupt", "insert: MAPCOMMON contains a null partition key"); + } else if (key_type == RAY_I64) { + if (((int64_t*)ray_data(keys))[0] == NULL_I64) + return ray_error("corrupt", "insert: MAPCOMMON contains a null partition key"); + } else { + ray_t* first = ray_sym_vec_cell(keys, 0); + if (!first || ray_str_len(first) == 0) + return ray_error("corrupt", "insert: MAPCOMMON contains an invalid partition key"); + } + } + + struct ray_sym_domain_s* common_dom = NULL; + ray_t* common_rep = NULL; + bool active_missing = false; + for (int64_t c = 1; c < ncols; c++) { + ray_t* wrapper = ray_table_get_col_idx(tbl, c); + if (!wrapper || !RAY_IS_PARTED(wrapper->type) || + wrapper->len != nparts || wrapper->attrs != 0) + return ray_error("corrupt", "insert: physical column %lld is not a canonical PARTED wrapper", (long long)(c - 1)); + int8_t base = (int8_t)RAY_PARTED_BASETYPE(wrapper->type); + if (base < RAY_BOOL || base > RAY_STR) + return ray_error("type", "insert: unsupported parted base type in physical column %lld", (long long)(c - 1)); + + ray_t** segs = (ray_t**)ray_data(wrapper); + /* ray_read_parted derives every wrapper from partition zero. Requiring + * that representative distinguishes this canonical shape from a table + * assembled manually out of wrapper-like objects. */ + if (!segs[0]) + return ray_error("corrupt", "insert: physical column %lld has no first-partition segment", (long long)(c - 1)); + for (int64_t p = 0; p < nparts; p++) { + ray_t* seg = segs[p]; + if (!seg) { + if (p != nparts - 1) + return ray_error("corrupt", "insert: physical column %lld is missing historical partition %lld", + (long long)(c - 1), (long long)p); + active_missing = true; + continue; + } + if (RAY_IS_ERR(seg) || seg->type != base || + !parted_segment_attrs_valid(base, seg->attrs) || + seg->len != rc[p]) + return ray_error("corrupt", "insert: segment metadata/length/type mismatch in physical column %lld partition %lld", + (long long)(c - 1), (long long)p); + if (seg->attrs & RAY_ATTR_HAS_INDEX) { + ray_t* index = seg->index; + if (!index || (uintptr_t)index <= 31 || + RAY_IS_ERR(index) || index->type != RAY_INDEX) + return ray_error("corrupt", "insert: physical column %lld partition %lld has malformed index metadata", + (long long)(c - 1), (long long)p); + } + if (base == RAY_SYM) { + struct ray_sym_domain_s* dom = ray_sym_vec_domain(seg); + if (dom == ray_sym_runtime_domain() || !ray_sym_domain_path(dom)) + return ray_error("corrupt", "insert: parted SYM segments must use a FILE domain"); + if (!common_dom) { common_dom = dom; common_rep = seg; } + else if (common_dom != dom) + return ray_error("corrupt", "insert: parted SYM segments do not share one FILE domain"); + } + } + } + + if (common_dom) { + ray_t* empty = ray_sym_domain_str(common_dom, 0); + if (ray_sym_domain_count(common_dom) <= 0 || !empty || ray_str_len(empty) != 0) + return ray_error("corrupt", "insert: parted FILE symbol domain lacks the reserved empty symbol"); + } + + out->mapcommon = mc; + out->key_values = keys; + out->row_counts = counts; + out->ncols = ncols; + out->ndata = ncols - 1; + out->nparts = nparts; + out->total_rows = total; + out->active_missing = active_missing; + out->key_type = key_type; + out->sym_dom = common_dom; + out->sym_rep = common_rep; + return NULL; +} + +static ray_t* decide_parted_insert_partition(parted_insert_view_t* v, + ray_t* key, + bool* new_partition) { + if (!key || RAY_IS_ERR(key) || RAY_IS_NULL(key) || + key->type != -v->key_type || RAY_ATOM_IS_NULL(key)) + return ray_error("type", "insert: partition key must be a non-null %s atom, got %s", + ray_type_name(-v->key_type), + key ? ray_type_name(key->type) : "null"); + bool valid = false; + int cmp = parted_key_atom_cmp(key, v->key_values, v->nparts - 1, &valid); + if (!valid) + return ray_error("domain", "insert: partition key is not representable in the target key domain"); + if (cmp < 0) + return ray_error("domain", "insert: partition key is historical/out of order; only the last or a later partition may grow"); + *new_partition = cmp > 0; + return NULL; +} + +static ray_t* parted_values_append(ray_t* vals, ray_t* value, bool owned) { + if (!value || RAY_IS_ERR(value)) { + if (owned && value && !RAY_IS_ERR(value)) ray_release(value); + ray_release(vals); + return value && RAY_IS_ERR(value) ? value + : ray_error("type", "insert: physical payload contains a missing value"); + } + ray_t* next = ray_list_append(vals, value); + if (owned) ray_release(value); + if (!next || RAY_IS_ERR(next)) { + ray_release(vals); + return next ? next : ray_error("oom", NULL); + } + return next; +} + +/* Normalize LIST/TABLE/DICT payloads into physical target-column order. The + * returned list owns one ref per value; all named forms are exact and unique. */ +static ray_t* normalize_parted_insert_rows(ray_t* tbl, + parted_insert_view_t* v, + ray_t* rows) { + if (!rows || RAY_IS_ERR(rows) || + (rows->type != RAY_LIST && rows->type != RAY_TABLE && + rows->type != RAY_DICT)) + return ray_error("type", "insert: parted rows must be a list, table or dict, got %s", + rows ? ray_type_name(rows->type) : "null"); + + ray_t* vals = ray_list_new(v->ndata); + if (!vals || RAY_IS_ERR(vals)) return vals ? vals : ray_error("oom", NULL); + + if (rows->type == RAY_LIST) { + if (rows->len != v->ndata) { + ray_release(vals); + return ray_error("domain", "insert: parted row list has %lld values, expected %lld physical columns", + (long long)rows->len, (long long)v->ndata); + } + ray_t** items = (ray_t**)ray_data(rows); + for (int64_t c = 0; c < v->ndata; c++) { + vals = parted_values_append(vals, items[c], false); + if (!vals || RAY_IS_ERR(vals)) return vals; + } + return vals; + } + + if (rows->type == RAY_TABLE) { + int64_t src_n = ray_table_ncols(rows); + if (src_n != v->ndata) { + ray_release(vals); + return ray_error("domain", "insert: parted row table has %lld columns, expected exactly %lld physical columns", + (long long)src_n, (long long)v->ndata); + } + for (int64_t sc = 0; sc < src_n; sc++) { + int64_t name = ray_table_col_name(rows, sc); + int64_t src_matches = 0, target_matches = 0; + for (int64_t i = 0; i < src_n; i++) + if (ray_table_col_name(rows, i) == name) src_matches++; + for (int64_t i = 1; i < v->ncols; i++) + if (ray_table_col_name(tbl, i) == name) target_matches++; + if (src_matches != 1 || target_matches != 1) { + ray_release(vals); + return ray_error("value", "insert: parted row table schema must exactly and uniquely match physical columns"); + } + } + for (int64_t c = 1; c < v->ncols; c++) { + ray_t* col = ray_table_get_col(rows, ray_table_col_name(tbl, c)); + vals = parted_values_append(vals, col, false); + if (!vals || RAY_IS_ERR(vals)) return vals; + } + return vals; + } + + ray_t* keys = ray_dict_keys(rows); + ray_t* dvals = ray_dict_vals(rows); + if (!keys || keys->type != RAY_SYM || !dvals || + (dvals->type != RAY_LIST && !ray_is_vec(dvals)) || + keys->len != v->ndata || dvals->len != keys->len) { + ray_release(vals); + return ray_error("type", "insert: parted row dict must have one symbol key/value per physical column"); + } + for (int64_t d = 0; d < keys->len; d++) { + int64_t name = sym_cell_runtime_id(keys, d); + int64_t dict_matches = 0, target_matches = 0; + for (int64_t i = 0; i < keys->len; i++) + if (sym_cell_runtime_id(keys, i) == name) dict_matches++; + for (int64_t i = 1; i < v->ncols; i++) + if (ray_table_col_name(tbl, i) == name) target_matches++; + if (name < 0 || dict_matches != 1 || target_matches != 1) { + ray_release(vals); + return ray_error("value", "insert: parted row dict schema must exactly and uniquely match physical columns"); + } + } + for (int64_t c = 1; c < v->ncols; c++) { + int64_t want = ray_table_col_name(tbl, c); + int64_t found = -1; + for (int64_t d = 0; d < keys->len; d++) + if (sym_cell_runtime_id(keys, d) == want) { found = d; break; } + if (found < 0) { + ray_release(vals); + return ray_error("value", "insert: parted row dict is missing a physical column"); + } + if (dvals->type == RAY_LIST) { + ray_t* item = ((ray_t**)ray_data(dvals))[found]; + vals = parted_values_append(vals, item, false); + } else { + int owned = 0; + ray_t* item = collection_elem(dvals, found, &owned); + vals = parted_values_append(vals, item, owned != 0); + } + if (!vals || RAY_IS_ERR(vals)) return vals; + } + return vals; +} + +static bool parted_value_is_scalar(ray_t* v) { + return v && !RAY_IS_ERR(v) && (RAY_IS_NULL(v) || v->type < 0); +} + +static bool parted_source_vec_compatible(int8_t dst, int8_t src) { + return src == dst; +} + +static ray_t* validate_parted_payload_atom(int8_t dst, ray_t* atom, + int64_t physical_col) { + if (!atom || RAY_IS_ERR(atom) || + (!RAY_IS_NULL(atom) && atom->type >= 0)) + return ray_error("type", "insert: physical column %lld payload cells must be atoms", + (long long)physical_col); + if (RAY_IS_NULL(atom)) { + if (dst == RAY_BOOL || dst == RAY_U8) + return ray_error("type", "insert: physical column %lld (%s) has no null representation", + (long long)physical_col, ray_type_name(dst)); + return NULL; + } + if (atom->type != -dst) + return ray_error("type", "insert: value type must exactly match physical column %lld (%s), got %s", + (long long)physical_col, ray_type_name(dst), + ray_type_name(atom->type)); + if (dst == RAY_GUID && + (!atom->obj || RAY_IS_ERR(atom->obj) || + atom->obj->type != RAY_U8 || atom->obj->len != 16)) + return ray_error("corrupt", "insert: GUID payload atom must own exactly 16 U8 bytes"); + if (RAY_ATOM_IS_NULL(atom)) { + if (dst == RAY_BOOL || dst == RAY_U8) + return ray_error("type", "insert: physical column %lld (%s) has no null representation", + (long long)physical_col, ray_type_name(dst)); + return NULL; + } + if (dst == RAY_SYM && !ray_sym_str(atom->i64)) + return ray_error("domain", "insert: SYM payload atom has an invalid runtime id"); + if ((dst == RAY_F32 && !isfinite((float)atom->f64)) || + (dst == RAY_F64 && !isfinite(atom->f64))) + return ray_error("domain", "insert: non-finite float payload must use the typed-null representation"); + if (dst >= RAY_BOOL && dst <= RAY_STR) return NULL; + return ray_error("type", "insert: unsupported physical column %lld (%s), got %s", + (long long)physical_col, ray_type_name(dst), + ray_type_name(atom->type)); +} + +/* Scalar physical columns broadcast. Every collection column establishes the + * same batch length, including zero. */ +static ray_t* validate_parted_payloads(ray_t* tbl, + parted_insert_view_t* v, + ray_t* vals, + int64_t* out_batch) { + ray_t** pv = (ray_t**)ray_data(vals); + bool have_collection = false; + int64_t batch = 1; + for (int64_t c = 0; c < v->ndata; c++) { + ray_t* payload = pv[c]; + if (parted_value_is_scalar(payload)) continue; + if (!payload || RAY_IS_ERR(payload) || !ray_is_vec(payload)) + return ray_error("type", "insert: physical column %lld payload must be an atom or an exactly typed vector", + (long long)c); + if (payload->len < 0) + return ray_error("corrupt", "insert: physical column %lld payload has a negative vector length", + (long long)c); + if (!have_collection) { batch = payload->len; have_collection = true; } + else if (payload->len != batch) + return ray_error("length", "insert: all non-scalar physical payload columns must have the same length"); + } + + for (int64_t c = 0; c < v->ndata; c++) { + ray_t* wrapper = ray_table_get_col_idx(tbl, c + 1); + int8_t dst = (int8_t)RAY_PARTED_BASETYPE(wrapper->type); + ray_t* payload = pv[c]; + if (!parted_value_is_scalar(payload) && + !parted_source_vec_compatible(dst, payload->type)) + return ray_error("type", "insert: physical column %lld vector type must exactly match %s", + (long long)c, ray_type_name(dst)); + + /* Exact vector types need no per-cell boxing. SYM still needs a + * direct domain-resolvability scan (ray_sym_vec_cell deliberately + * avoids building the FILE->runtime LUT), while floats reject + * infinities but retain NaN as their typed-null representation. */ + if (!parted_value_is_scalar(payload)) { + ray_t* owner = (payload->attrs & RAY_ATTR_SLICE) + ? payload->slice_parent : payload; + if ((dst == RAY_BOOL || dst == RAY_U8) && + (!owner || (owner->attrs & RAY_ATTR_HAS_NULLS))) + return ray_error("type", "insert: physical column %lld (%s) has no null representation", + (long long)c, ray_type_name(dst)); + if (dst == RAY_SYM) { + for (int64_t r = 0; r < payload->len; r++) + if (!ray_sym_vec_cell(payload, r)) + return ray_error("domain", "insert: physical column %lld SYM vector contains an invalid domain position", + (long long)c); + } else if (dst == RAY_F32) { + const float* d = (const float*)ray_data(payload); + for (int64_t r = 0; r < payload->len; r++) + if (!isfinite(d[r]) && !isnan(d[r])) + return ray_error("domain", "insert: physical column %lld F32 vector contains infinity", + (long long)c); + } else if (dst == RAY_F64) { + const double* d = (const double*)ray_data(payload); + for (int64_t r = 0; r < payload->len; r++) + if (!isfinite(d[r]) && !isnan(d[r])) + return ray_error("domain", "insert: physical column %lld F64 vector contains infinity", + (long long)c); + } + continue; + } + + ray_t* err = validate_parted_payload_atom(dst, payload, c); + if (err) return err; + } + *out_batch = batch; + return NULL; +} + +/* Append a prevalidated non-SYM atom, preserving every concrete splayable + * physical type (notably F32, which legacy flat insert does not handle). */ +static ray_t* append_parted_atom(ray_t* vec, int8_t dst, ray_t* atom) { + if (RAY_IS_NULL(atom) || RAY_ATOM_IS_NULL(atom)) { + if (dst == RAY_STR) + return ray_str_vec_append(vec, "", 0); + uint8_t zero[16] = {0}; + ray_t* next = ray_vec_append(vec, zero); + if (!next || RAY_IS_ERR(next)) return next; + if (dst != RAY_BOOL && dst != RAY_U8 && dst != RAY_SYM) + ray_vec_set_null(next, next->len - 1, true); + return next; + } + + switch (dst) { + case RAY_BOOL: { uint8_t x = atom->b8; return ray_vec_append(vec, &x); } + case RAY_U8: { uint8_t x = atom->u8; return ray_vec_append(vec, &x); } + case RAY_I16: { int16_t x = atom->i16; return ray_vec_append(vec, &x); } + case RAY_I32: { int32_t x = atom->i32; return ray_vec_append(vec, &x); } + case RAY_I64: { int64_t x = atom->i64; return ray_vec_append(vec, &x); } + case RAY_F32: { float x = (float)atom->f64; return ray_vec_append(vec, &x); } + case RAY_F64: { double x = atom->f64; return ray_vec_append(vec, &x); } + case RAY_DATE: { int32_t x = atom->i32; return ray_vec_append(vec, &x); } + case RAY_TIME: { int32_t x = atom->i32; return ray_vec_append(vec, &x); } + case RAY_TIMESTAMP: { int64_t x = atom->i64; return ray_vec_append(vec, &x); } + case RAY_GUID: return ray_vec_append(vec, ray_data(atom->obj)); + case RAY_STR: return ray_str_vec_append(vec, ray_str_ptr(atom), ray_str_len(atom)); + default: return ray_error("type", "insert: unsupported parted append type %s", ray_type_name(dst)); + } +} + +/* Concat copies sentinel payloads faithfully, but deliberately trusts the + * source HAS_NULLS gate. Finalize the private segment from its bytes so raw + * API vectors cannot publish an unmarked null. GUID uses an all-zero + * sentinel and is outside par_finalize_nulls' numeric/temporal scope. */ +static void finalize_parted_segment_nulls(ray_t* vec) { + par_finalize_nulls(vec); + if (vec->type != RAY_GUID) return; + const uint8_t* d = (const uint8_t*)ray_data(vec); + for (int64_t r = 0; r < vec->len; r++) { + bool all_zero = true; + for (int b = 0; b < 16; b++) { + if (d[(size_t)r * 16u + (size_t)b] != 0) { + all_zero = false; + break; + } + } + if (all_zero) { + vec->attrs |= RAY_ATTR_HAS_NULLS; + return; + } + } +} + +static ray_t* build_parted_null_prefix(int8_t base, int64_t len) { + ray_t* prefix = ray_vec_new(base, len); + if (!prefix || RAY_IS_ERR(prefix)) + return prefix ? prefix : ray_error("oom", NULL); + for (int64_t r = 0; r < len; r++) { + ray_t* next = append_parted_atom(prefix, base, RAY_NULL_OBJ); + if (!next || RAY_IS_ERR(next)) { + ray_release(prefix); + return next ? next : ray_error("oom", NULL); + } + prefix = next; + } + return prefix; +} + +static ray_t* build_parted_nonsym_segment(int8_t base, + ray_t* old_seg, + int64_t old_len, + ray_t* metadata_seg, + ray_t* payload, + int64_t batch) { + if (!old_seg && old_len > 0 && (base == RAY_BOOL || base == RAY_U8)) + return ray_error("type", "insert: cannot backfill a missing active %s segment because the type has no null representation", + ray_type_name(base)); + + ray_t* owned_prefix = NULL; + ray_t* prefix = old_seg; + if (!prefix && old_len > 0) { + owned_prefix = build_parted_null_prefix(base, old_len); + if (!owned_prefix || RAY_IS_ERR(owned_prefix)) + return owned_prefix ? owned_prefix : ray_error("oom", NULL); + prefix = owned_prefix; + } + + ray_t* result = NULL; + if (parted_value_is_scalar(payload)) { + ray_t* tail = ray_vec_new(base, batch); + if (!tail || RAY_IS_ERR(tail)) + result = tail ? tail : ray_error("oom", NULL); + for (int64_t r = 0; r < batch; r++) { + if (!tail || RAY_IS_ERR(tail)) break; + ray_t* next = append_parted_atom(tail, base, payload); + if (!next || RAY_IS_ERR(next)) { + ray_release(tail); + tail = NULL; + result = next ? next : ray_error("oom", NULL); + break; + } + tail = next; + } + if (tail && !RAY_IS_ERR(tail)) { + if (prefix) { + /* Never append directly to an mmap (or any source) segment. */ + result = ray_vec_concat(prefix, tail); + ray_release(tail); + } else { + result = tail; + } + } + } else if (prefix) { + /* Exact typed vector: one concat directly into the completed active + * segment, with no per-cell boxing and no intermediate payload copy. */ + result = ray_vec_concat(prefix, payload); + } else { + /* A brand-new partition still needs to materialise slices/mmaps and + * deep-copy STR storage rather than publish the caller's vector. */ + ray_t* empty = ray_vec_new(base, 0); + if (!empty || RAY_IS_ERR(empty)) { + result = empty ? empty : ray_error("oom", NULL); + } else { + result = ray_vec_concat(empty, payload); + ray_release(empty); + } + } + + if (owned_prefix) ray_release(owned_prefix); + if (!result || RAY_IS_ERR(result)) return result ? result : ray_error("oom", NULL); + finalize_parted_segment_nulls(result); + + /* Link identity is semantic column metadata. The changed heap segment has + * no index, but it keeps a representative I32/I64 link target. */ + if ((base == RAY_I32 || base == RAY_I64) && metadata_seg && + (metadata_seg->attrs & RAY_ATTR_HAS_LINK)) { + result->attrs |= RAY_ATTR_HAS_LINK; + result->link_target = metadata_seg->link_target; + } + return result; +} + +/* Allocate and copy the old prefix of a W64 FILE-domain SYM segment. Appended + * positions are intentionally zero until the late domain-intern commit pass. */ +static ray_t* build_parted_sym_skeleton(parted_insert_view_t* v, + ray_t* old_seg, + int64_t old_len, + int64_t batch) { + if (!v->sym_dom || !v->sym_rep) + return ray_error("corrupt", "insert: no shared FILE domain representative for SYM column"); + if (old_len > INT64_MAX - batch) return ray_error("oom", NULL); + int64_t total = old_len + batch; + ray_t* out = ray_sym_vec_new(RAY_SYM_W64, total); + if (!out || RAY_IS_ERR(out)) return out ? out : ray_error("oom", NULL); + ray_sym_vec_adopt_domain(out, v->sym_rep); + if (total) memset(ray_data(out), 0, (size_t)total * sizeof(int64_t)); + if (old_seg) { + int64_t domain_n = ray_sym_domain_count(v->sym_dom); + for (int64_t r = 0; r < old_len; r++) { + int64_t pos = ray_read_sym(ray_data(old_seg), r, + old_seg->type, old_seg->attrs); + if (pos < 0 || pos >= domain_n) { + ray_release(out); + return ray_error("corrupt", "insert: active SYM segment contains an out-of-domain position"); + } + ((int64_t*)ray_data(out))[r] = pos; + } + } + out->len = total; + return out; +} + +static ray_t* parted_wrapper_metadata(ray_t* wrapper, int64_t active) { + ray_t** segs = (ray_t**)ray_data(wrapper); + if (active < wrapper->len && segs[active]) return segs[active]; + int64_t start = active < wrapper->len ? active - 1 : wrapper->len - 1; + for (int64_t p = start; p >= 0; p--) + if (segs[p]) return segs[p]; + return NULL; +} + +static ray_t* build_parted_mapcommon(parted_insert_view_t* v, + ray_t* key, + bool new_partition, + int64_t batch) { + if (new_partition && v->nparts == INT64_MAX) + return ray_error("oom", NULL); + int64_t new_n = v->nparts + (new_partition ? 1 : 0); + ray_t* keys = ray_vec_new(v->key_type, new_n); + ray_t* counts = ray_vec_new(RAY_I64, new_n); + if (!keys || RAY_IS_ERR(keys) || !counts || RAY_IS_ERR(counts)) { + ray_t* ret = keys && RAY_IS_ERR(keys) ? keys + : counts && RAY_IS_ERR(counts) ? counts : NULL; + if (keys && keys != ret) { + if (RAY_IS_ERR(keys)) ray_error_free(keys); + else ray_release(keys); + } + if (counts && counts != ret) { + if (RAY_IS_ERR(counts)) ray_error_free(counts); + else ray_release(counts); + } + return ret ? ret : ray_error("oom", NULL); + } + + for (int64_t p = 0; p < v->nparts; p++) { + ray_t* next; + if (v->key_type == RAY_DATE) { + int32_t x = ((int32_t*)ray_data(v->key_values))[p]; + next = ray_vec_append(keys, &x); + } else { + int64_t x = v->key_type == RAY_SYM + ? ray_read_sym(ray_data(v->key_values), p, + RAY_SYM, v->key_values->attrs) + : ((int64_t*)ray_data(v->key_values))[p]; + next = ray_vec_append(keys, &x); + } + if (!next || RAY_IS_ERR(next)) { + ray_release(keys); ray_release(counts); + return next ? next : ray_error("oom", NULL); + } + keys = next; + + int64_t rc = ((int64_t*)ray_data(v->row_counts))[p]; + if (!new_partition && p == v->nparts - 1) rc += batch; + next = ray_vec_append(counts, &rc); + if (!next || RAY_IS_ERR(next)) { + ray_release(keys); ray_release(counts); + return next ? next : ray_error("oom", NULL); + } + counts = next; + } + if (new_partition) { + ray_t* next; + if (v->key_type == RAY_DATE) { + int32_t x = key->i32; + next = ray_vec_append(keys, &x); + } else { + int64_t x = key->i64; + next = ray_vec_append(keys, &x); + } + if (!next || RAY_IS_ERR(next)) { + ray_release(keys); ray_release(counts); + return next ? next : ray_error("oom", NULL); + } + keys = next; + next = ray_vec_append(counts, &batch); + if (!next || RAY_IS_ERR(next)) { + ray_release(keys); ray_release(counts); + return next ? next : ray_error("oom", NULL); + } + counts = next; + } + + ray_t* mc = ray_alloc(2 * sizeof(ray_t*)); + if (!mc || RAY_IS_ERR(mc)) { + ray_release(keys); ray_release(counts); + return mc ? mc : ray_error("oom", NULL); + } + mc->type = RAY_MAPCOMMON; + mc->len = 2; + mc->attrs = v->mapcommon->attrs; + memset(mc->aux, 0, 16); + ((ray_t**)ray_data(mc))[0] = keys; /* transfer ownership */ + ((ray_t**)ray_data(mc))[1] = counts; + return mc; +} + +/* Borrow the logical bytes of a prevalidated SYM payload cell without + * allocating a runtime atom for typed vectors. */ +static bool parted_sym_payload_bytes(ray_t* payload, int64_t row, + const char** out_s, size_t* out_n) { + ray_t* atom = NULL; + if (parted_value_is_scalar(payload)) { + atom = payload; + } else if (payload->type == RAY_LIST) { + atom = ((ray_t**)ray_data(payload))[row]; + } else if (payload->type == RAY_SYM) { + ray_t* s = ray_sym_vec_cell(payload, row); + if (!s) return false; + *out_s = ray_str_ptr(s); + *out_n = ray_str_len(s); + return true; + } else { + return false; + } + if (RAY_IS_NULL(atom)) { + *out_s = ""; *out_n = 0; return true; + } + if (!atom || atom->type != -RAY_SYM) return false; + ray_t* s = ray_sym_str(atom->i64); + if (!s) return false; + *out_s = ray_str_ptr(s); + *out_n = ray_str_len(s); + return true; +} + +/* Domain growth is append-only and currently has no rollback primitive. This + * pass therefore runs only after the complete private result (including every + * allocation and every non-SYM conversion) has been built. An OOM may leave + * unused vocabulary entries in memory, but no row/table/env/disk state is + * published. */ +static ray_t* intern_parted_payload_symbols(ray_t* tbl, + parted_insert_view_t* v, + ray_t* vals, + int64_t batch) { + ray_t** pv = (ray_t**)ray_data(vals); + for (int64_t c = 0; c < v->ndata; c++) { + ray_t* wrapper = ray_table_get_col_idx(tbl, c + 1); + if (RAY_PARTED_BASETYPE(wrapper->type) != RAY_SYM) continue; + ray_t* payload = pv[c]; + int64_t n = parted_value_is_scalar(payload) ? (batch ? 1 : 0) : batch; + for (int64_t r = 0; r < n; r++) { + const char* s = NULL; size_t slen = 0; + if (!parted_sym_payload_bytes(payload, r, &s, &slen)) + return ray_error("corrupt", "insert: could not resolve a SYM payload cell"); + if (ray_sym_domain_intern(v->sym_dom, s, slen) < 0) + return ray_error("oom", "insert: could not extend the parted FILE symbol domain"); + } + } + return NULL; +} + +static ray_t* fill_parted_payload_symbols(ray_t* tbl, + parted_insert_view_t* v, + ray_t* vals, + ray_t* staged, + int64_t old_len, + int64_t batch) { + ray_t** pv = (ray_t**)ray_data(vals); + ray_t** sv = (ray_t**)ray_data(staged); + for (int64_t c = 0; c < v->ndata; c++) { + ray_t* wrapper = ray_table_get_col_idx(tbl, c + 1); + if (RAY_PARTED_BASETYPE(wrapper->type) != RAY_SYM) continue; + ray_t* payload = pv[c]; + ray_t* seg = sv[c]; + for (int64_t r = 0; r < batch; r++) { + const char* s = NULL; size_t slen = 0; + int64_t src_r = parted_value_is_scalar(payload) ? 0 : r; + if (!parted_sym_payload_bytes(payload, src_r, &s, &slen)) + return ray_error("corrupt", "insert: could not resolve a staged SYM payload cell"); + int64_t pos = ray_sym_domain_find(v->sym_dom, s, slen); + if (pos < 0) + return ray_error("corrupt", "insert: staged SYM is absent after domain intern"); + ray_write_sym(ray_data(seg), old_len + r, (uint64_t)pos, + RAY_SYM, seg->attrs); + } + } + return NULL; +} + +static ray_t* insert_parted_rows(ray_t* tbl, ray_t* key, ray_t* rows) { + parted_insert_view_t v; + ray_t* err = validate_parted_insert_target(tbl, &v); + if (err) return err; + + bool new_partition = false; + err = decide_parted_insert_partition(&v, key, &new_partition); + if (err) return err; + if (new_partition && v.active_missing) + return ray_error("domain", "insert: cannot advance the partition key while the current partition has missing physical segments"); + + ray_t* vals = normalize_parted_insert_rows(tbl, &v, rows); + if (!vals || RAY_IS_ERR(vals)) return vals ? vals : ray_error("oom", NULL); + int64_t batch = 0; + err = validate_parted_payloads(tbl, &v, vals, &batch); + if (err) { ray_release(vals); return err; } + + /* Empty batch is a validated no-op and never creates an empty partition. + * It cannot repair a missing active segment, so reject that shape instead + * of returning the same unqueryable table. */ + if (batch == 0) { + ray_release(vals); + if (v.active_missing) + return ray_error("domain", "insert: an empty batch cannot materialize missing active physical segments"); + ray_retain(tbl); + return tbl; + } + + if (v.total_rows > INT64_MAX - batch) { + ray_release(vals); + return ray_error("oom", NULL); + } + + int64_t active = new_partition ? v.nparts : v.nparts - 1; + int64_t old_len = new_partition ? 0 + : ((int64_t*)ray_data(v.row_counts))[v.nparts - 1]; + if (old_len > INT64_MAX - batch) { + ray_release(vals); + return ray_error("oom", NULL); + } + if (new_partition && + (v.nparts == INT64_MAX || + (uint64_t)v.nparts + 1u > SIZE_MAX / sizeof(ray_t*))) { + ray_release(vals); + return ray_error("oom", NULL); + } + + ray_t* staged = ray_list_new(v.ndata); + if (!staged || RAY_IS_ERR(staged)) { + ray_release(vals); + return staged ? staged : ray_error("oom", NULL); + } + ray_t** pv = (ray_t**)ray_data(vals); + for (int64_t c = 0; c < v.ndata; c++) { + ray_t* wrapper = ray_table_get_col_idx(tbl, c + 1); + ray_t** segs = (ray_t**)ray_data(wrapper); + int8_t base = (int8_t)RAY_PARTED_BASETYPE(wrapper->type); + ray_t* old_seg = new_partition ? NULL : segs[v.nparts - 1]; + ray_t* new_seg = base == RAY_SYM + ? build_parted_sym_skeleton(&v, old_seg, old_len, batch) + : build_parted_nonsym_segment(base, old_seg, old_len, + parted_wrapper_metadata(wrapper, active), + pv[c], batch); + if (!new_seg || RAY_IS_ERR(new_seg)) { + ray_release(staged); ray_release(vals); + return new_seg ? new_seg : ray_error("oom", NULL); + } + ray_t* next = ray_list_append(staged, new_seg); + ray_release(new_seg); + if (!next || RAY_IS_ERR(next)) { + ray_release(staged); ray_release(vals); + return next ? next : ray_error("oom", NULL); + } + staged = next; + } + + ray_t* mc = build_parted_mapcommon(&v, key, new_partition, batch); + if (!mc || RAY_IS_ERR(mc)) { + ray_release(staged); ray_release(vals); + return mc ? mc : ray_error("oom", NULL); + } + ray_t* result = ray_table_new(v.ncols); + if (!result || RAY_IS_ERR(result)) { + ray_release(mc); ray_release(staged); ray_release(vals); + return result ? result : ray_error("oom", NULL); + } + result = ray_table_add_col(result, ray_table_col_name(tbl, 0), mc); + ray_release(mc); + if (!result || RAY_IS_ERR(result)) { + ray_release(staged); ray_release(vals); + return result ? result : ray_error("oom", NULL); + } + + int64_t new_nparts = v.nparts + (new_partition ? 1 : 0); + if ((uint64_t)new_nparts > SIZE_MAX / sizeof(ray_t*)) { + ray_release(result); ray_release(staged); ray_release(vals); + return ray_error("oom", NULL); + } + ray_t** staged_segs = (ray_t**)ray_data(staged); + for (int64_t c = 0; c < v.ndata; c++) { + ray_t* old_wrapper = ray_table_get_col_idx(tbl, c + 1); + ray_t** old_segs = (ray_t**)ray_data(old_wrapper); + ray_t* wrapper = ray_alloc((size_t)new_nparts * sizeof(ray_t*)); + if (!wrapper || RAY_IS_ERR(wrapper)) { + ray_release(result); ray_release(staged); ray_release(vals); + return wrapper ? wrapper : ray_error("oom", NULL); + } + wrapper->type = old_wrapper->type; + wrapper->len = new_nparts; + wrapper->attrs = 0; + memset(wrapper->aux, 0, 16); + ray_t** ws = (ray_t**)ray_data(wrapper); + memset(ws, 0, (size_t)new_nparts * sizeof(ray_t*)); + for (int64_t p = 0; p < v.nparts; p++) { + if (!new_partition && p == v.nparts - 1) continue; + ws[p] = old_segs[p]; + if (ws[p]) ray_retain(ws[p]); + } + ws[active] = staged_segs[c]; + ray_retain(ws[active]); + + result = ray_table_add_col(result, ray_table_col_name(tbl, c + 1), wrapper); + ray_release(wrapper); + if (!result || RAY_IS_ERR(result)) { + ray_release(staged); ray_release(vals); + return result ? result : ray_error("oom", NULL); + } + } + + /* All allocations and all fallible non-SYM conversions are complete. */ + err = intern_parted_payload_symbols(tbl, &v, vals, batch); + if (!err) + err = fill_parted_payload_symbols(tbl, &v, vals, staged, + old_len, batch); + ray_release(staged); + ray_release(vals); + if (err) { ray_release(result); return err; } + return result; +} + +/* (insert table (list val1 val2 ...)) — append a row to a table + * (insert parted partition-key rows) — grow the explicit live tail */ ray_t* ray_insert_fn(ray_t** args, int64_t n) { return ray_insert(args, n); } @@ -12118,8 +13116,76 @@ ray_t* ray_insert(ray_t** args, int64_t n) { return result; } - /* Table target: arity-3 positional row insert is not implemented. */ - if (n != 2) { ray_release(tbl); return ray_error("nyi", NULL); } + /* Table arity-3 is reserved for explicit live-tail growth on the exact + * representation returned by `.db.parted.get`. */ + if (n == 3) { + if (!table_has_parted_columns(tbl)) { + ray_release(tbl); + return ray_error("type", "insert: three-argument table form requires a canonical .db.parted.get table"); + } + if (inplace_sym >= 0 && ray_sym_is_reserved(inplace_sym) && + !ray_sym_is_ipc_hook(inplace_sym)) { + ray_release(tbl); + return ray_error("reserved", "insert: cannot rebind a reserved target"); + } + ray_t* partition_key = already_eval + ? (ray_retain(args[1]), args[1]) + : ray_eval(args[1]); + if (!partition_key || RAY_IS_ERR(partition_key)) { + ray_release(tbl); + return partition_key ? partition_key + : ray_error("type", "insert: partition key evaluation failed"); + } + ray_t* rows = already_eval + ? (ray_retain(args[2]), args[2]) + : ray_eval(args[2]); + if (!rows || RAY_IS_ERR(rows)) { + ray_release(tbl); ray_release(partition_key); + return rows ? rows : ray_error("type", "insert: rows evaluation failed"); + } + /* Preallocate the success value before domain growth/publication. */ + ray_t* success = NULL; + if (inplace_sym >= 0) { + success = ray_sym(inplace_sym); + if (!success || RAY_IS_ERR(success)) { + ray_release(rows); + ray_release(partition_key); + ray_release(tbl); + return success ? success : ray_error("oom", NULL); + } + } + ray_t* result = insert_parted_rows(tbl, partition_key, rows); + ray_release(rows); + ray_release(partition_key); + if (inplace_sym >= 0 && result && !RAY_IS_ERR(result)) { + /* Empty batches return the original table and require no rebind. + * Otherwise publish only after the complete private result exists, + * and surface dotted-path OOM/type failures instead of + * falsely returning the target symbol as success. */ + ray_err_t bind_err = result == tbl ? RAY_OK + : ray_env_set(inplace_sym, result); + ray_release(result); + ray_release(tbl); + if (bind_err != RAY_OK) { + ray_release(success); + return ray_error(ray_err_code_str(bind_err), + "insert: could not rebind the parted target"); + } + return success; + } + if (success) ray_release(success); + ray_release(tbl); + return result; + } + if (n != 2) { + ray_release(tbl); + return ray_error("arity", "insert: table insert takes 2 args, or 3 for a parted live tail, got %lld", + (long long)n); + } + if (table_has_parted_columns(tbl)) { + ray_release(tbl); + return ray_error("arity", "insert: parted table insert requires target, partition key and physical rows"); + } /* Evaluate the row argument (skip if already evaluated) */ ray_t* row = already_eval ? (ray_retain(args[1]), args[1]) : ray_eval(args[1]); @@ -12377,6 +13443,14 @@ ray_t* ray_upsert(ray_t** args, int64_t n) { } } + /* PARTED wrappers are segmented pointer arrays, not flat row storage. + * Reject before evaluating key/row expressions so the legacy match scan + * can never interpret wrapper pointers as scalar cells. */ + if (tbl->type == RAY_TABLE && table_has_parted_columns(tbl)) { + ray_release(tbl); + return ray_error("nyi", "upsert: parted tables are not supported; use insert with an explicit partition key"); + } + ray_t* key_sym = already_eval ? (ray_retain(args[1]), args[1]) : ray_eval(args[1]); if (!key_sym || RAY_IS_ERR(key_sym)) { ray_release(tbl); return key_sym ? key_sym : ray_error("type", "upsert: key evaluation failed"); } diff --git a/test/test_lang.c b/test/test_lang.c index 9f27d53e..fbdb1f42 100644 --- a/test/test_lang.c +++ b/test/test_lang.c @@ -28,10 +28,15 @@ #include #include #include "mem/heap.h" +#include "store/part.h" +#include "store/splay.h" +#include "table/domain.h" #include "table/sym.h" #include "lang/internal.h" #include #include +#include +#include #include #include @@ -118,6 +123,32 @@ extern ray_runtime_t *__RUNTIME; ray_error_free(_le); \ } while(0) +/* ASSERT_ER_CODE: also pin the semantic rejection path. This is useful for + * safety guards where merely receiving some downstream schema error would be + * a false positive (for example, PARTED upsert must be rejected up front). */ +#define ASSERT_ER_CODE(expr, expected_code) do { \ + ray_t* _le = ray_eval_str(expr); \ + if (!RAY_IS_ERR(_le)) { \ + ray_t* _s = _le ? ray_fmt(_le, 0) : NULL; \ + fprintf(stderr, " %s:%d: expected %s error, got: %.*s\n -- expr: %s\n", \ + __FILE__, __LINE__, expected_code, \ + (int)(_s ? ray_str_len(_s) : 0), \ + _s ? ray_str_ptr(_s) : "", expr); \ + if (_s) ray_release(_s); \ + if (_le) ray_release(_le); \ + FAIL("explicit MUNIT_FAIL"); \ + } \ + const char* _ec = ray_err_code(_le); \ + int _code_ok = _ec && strcmp(_ec, expected_code) == 0; \ + if (!_code_ok) { \ + fprintf(stderr, " %s:%d: expected error code %s, got %s\n -- expr: %s\n", \ + __FILE__, __LINE__, expected_code, _ec ? _ec : "null", expr); \ + ray_error_free(_le); \ + FAIL("explicit MUNIT_FAIL"); \ + } \ + ray_error_free(_le); \ +} while(0) + /* ---- Setup / Teardown ---- */ static void lang_setup(void) { @@ -2519,6 +2550,300 @@ static test_result_t test_eval_select_empty_const(void) { PASS(); } +/* ---- Helpers: real splayed fixture for partition-aware insert --------- */ + +static void lang_parted_insert_rm_rf(const char* root) { + DIR* dir = opendir(root); + if (!dir) { + (void)unlink(root); + return; + } + + struct dirent* ent; + while ((ent = readdir(dir)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) + continue; + char child[1200]; + int n = snprintf(child, sizeof(child), "%s/%s", root, ent->d_name); + if (n <= 0 || (size_t)n >= sizeof(child)) continue; + struct stat st; + if (lstat(child, &st) != 0) continue; + if (S_ISDIR(st.st_mode)) lang_parted_insert_rm_rf(child); + else (void)unlink(child); + } + closedir(dir); + (void)rmdir(root); +} + +static bool lang_parted_insert_save_table(const char* src, const char* dir, + const char* sym_path) { + ray_t* tbl = ray_eval_str(src); + if (!tbl || RAY_IS_ERR(tbl)) { + if (tbl) ray_error_free(tbl); + return false; + } + ray_err_t err = ray_splay_save(tbl, dir, sym_path); + ray_release(tbl); + return err == RAY_OK; +} + +/* Two immutable mmap-backed days with one shared FILE-domain vocabulary. + * Inserts into the canonical parted view are intentionally memory-only. */ +static bool lang_parted_insert_fixture(const char* root) { + char day1[1024], day2[1024], sym_path[1024]; + int n1 = snprintf(day1, sizeof(day1), "%s/2024.01.01/trades", root); + int n2 = snprintf(day2, sizeof(day2), "%s/2024.01.02/trades", root); + int ns = snprintf(sym_path, sizeof(sym_path), "%s/.sym", root); + if (n1 <= 0 || (size_t)n1 >= sizeof(day1) || + n2 <= 0 || (size_t)n2 >= sizeof(day2) || + ns <= 0 || (size_t)ns >= sizeof(sym_path)) + return false; + + if (!lang_parted_insert_save_table( + "(table ['id 'ticker 'note 'qty] " + " (list [1 2] ['alpha 'beta] " + " [\"one\" \"two\"] [10 0Nl]))", + day1, sym_path)) + return false; + + return lang_parted_insert_save_table( + "(table ['id 'ticker 'note 'qty] " + " (list [3 4] ['alpha 'beta] " + " [\"three\" \"four\"] [30 40]))", + day2, sym_path); +} + +/* Two SYM columns sharing one FILE vocabulary across both disk partitions. */ +static bool lang_parted_insert_sym_fixture(const char* root) { + char day1[1024], day2[1024], sym_path[1024]; + int n1 = snprintf(day1, sizeof(day1), "%s/2024.01.01/trades", root); + int n2 = snprintf(day2, sizeof(day2), "%s/2024.01.02/trades", root); + int ns = snprintf(sym_path, sizeof(sym_path), "%s/.sym", root); + if (n1 <= 0 || (size_t)n1 >= sizeof(day1) || + n2 <= 0 || (size_t)n2 >= sizeof(day2) || + ns <= 0 || (size_t)ns >= sizeof(sym_path)) + return false; + + if (!lang_parted_insert_save_table( + "(table ['id 'ticker 'venue] " + " (list [1 2] ['alpha 'beta] ['xnys 'xnas]))", + day1, sym_path)) + return false; + + return lang_parted_insert_save_table( + "(table ['id 'ticker 'venue] " + " (list [3 4] ['alpha 'beta] ['xnas 'xnys]))", + day2, sym_path); +} + +/* STRL stores its little-endian entry count immediately after the magic. */ +static bool lang_parted_insert_symfile_count(const char* path, int64_t* out) { + if (!path || !out) return false; + int fd = open(path, O_RDONLY); + if (fd < 0) return false; + uint8_t hdr[12]; + size_t got = 0; + while (got < sizeof(hdr)) { + ssize_t n = read(fd, hdr + got, sizeof(hdr) - got); + if (n <= 0) { + (void)close(fd); + return false; + } + got += (size_t)n; + } + (void)close(fd); + + uint64_t count = 0; + for (int i = 0; i < 8; i++) + count |= (uint64_t)hdr[4 + i] << (unsigned)(8 * i); + if (count > INT64_MAX) return false; + *out = (int64_t)count; + return true; +} + +static ray_t* lang_parted_insert_col(ray_t* tbl, const char* name) { + return ray_table_get_col(tbl, ray_sym_intern(name, strlen(name))); +} + +static ray_t* lang_parted_insert_counts(ray_t* tbl) { + ray_t* mc = ray_table_get_col_idx(tbl, 0); + if (!mc || mc->type != RAY_MAPCOMMON || mc->len != 2) return NULL; + return ((ray_t**)ray_data(mc))[1]; +} + +enum { + LANG_WIDE_BOOL, LANG_WIDE_U8, LANG_WIDE_I16, LANG_WIDE_I32, + LANG_WIDE_F32, LANG_WIDE_F64, LANG_WIDE_DATE, LANG_WIDE_TIME, + LANG_WIDE_TIMESTAMP, LANG_WIDE_GUID, LANG_WIDE_NCOLS +}; + +static const int8_t lang_parted_wide_types[LANG_WIDE_NCOLS] = { + RAY_BOOL, RAY_U8, RAY_I16, RAY_I32, RAY_F32, RAY_F64, + RAY_DATE, RAY_TIME, RAY_TIMESTAMP, RAY_GUID +}; + +static const char* const lang_parted_wide_names[LANG_WIDE_NCOLS] = { + "b", "u8", "i16", "i32", "f32", "f64", "d", "tm", "ts", "g" +}; + +static const char* const lang_parted_wide_payload_names[LANG_WIDE_NCOLS] = { + "wide_bv", "wide_u8v", "wide_i16v", "wide_i32v", "wide_f32v", + "wide_f64v", "wide_dv", "wide_tmv", "wide_tsv", "wide_gv" +}; + +static const char* const lang_parted_wide_zero_names[LANG_WIDE_NCOLS] = { + "wide_zb", "wide_zu8", "wide_zi16", "wide_zi32", "wide_zf32", + "wide_zf64", "wide_zd", "wide_ztm", "wide_zts", "wide_zg" +}; + +static ray_t* lang_parted_wide_fixture_vec(int8_t type, int variant) { + ray_t* v = ray_vec_new(type, 1); + if (!v || RAY_IS_ERR(v)) return v; + v->len = 1; + switch (type) { + case RAY_BOOL: ((uint8_t*)ray_data(v))[0] = (uint8_t)(variant & 1); break; + case RAY_U8: ((uint8_t*)ray_data(v))[0] = (uint8_t)(10 + variant); break; + case RAY_I16: ((int16_t*)ray_data(v))[0] = (int16_t)(100 + variant); break; + case RAY_I32: ((int32_t*)ray_data(v))[0] = 1000 + variant; break; + case RAY_F32: ((float*)ray_data(v))[0] = (float)variant + 0.25f; break; + case RAY_F64: ((double*)ray_data(v))[0] = (double)variant + 0.5; break; + case RAY_DATE: ((int32_t*)ray_data(v))[0] = 8765 + variant; break; + case RAY_TIME: ((int32_t*)ray_data(v))[0] = variant * 1000; break; + case RAY_TIMESTAMP: + ((int64_t*)ray_data(v))[0] = (int64_t)variant * 1000000000LL; + break; + case RAY_GUID: { + uint8_t* p = (uint8_t*)ray_data(v); + for (int i = 0; i < 16; i++) p[i] = (uint8_t)(variant * 16 + i + 1); + break; + } + default: ray_release(v); return ray_error("type", NULL); + } + return v; +} + +static ray_t* lang_parted_wide_table(int variant) { + ray_t* tbl = ray_table_new(LANG_WIDE_NCOLS); + if (!tbl || RAY_IS_ERR(tbl)) return tbl; + for (int i = 0; i < LANG_WIDE_NCOLS; i++) { + ray_t* col = lang_parted_wide_fixture_vec(lang_parted_wide_types[i], + variant); + if (!col || RAY_IS_ERR(col)) { + ray_release(tbl); + return col ? col : ray_error("oom", NULL); + } + int64_t name = ray_sym_intern(lang_parted_wide_names[i], + strlen(lang_parted_wide_names[i])); + tbl = ray_table_add_col(tbl, name, col); + ray_release(col); + if (!tbl || RAY_IS_ERR(tbl)) return tbl; + } + return tbl; +} + +static bool lang_parted_wide_fixture(const char* root) { + char day1[1024], day2[1024]; + int n1 = snprintf(day1, sizeof(day1), "%s/2024.01.01/wide", root); + int n2 = snprintf(day2, sizeof(day2), "%s/2024.01.02/wide", root); + if (n1 <= 0 || (size_t)n1 >= sizeof(day1) || + n2 <= 0 || (size_t)n2 >= sizeof(day2)) + return false; + + ray_t* t1 = lang_parted_wide_table(1); + if (!t1 || RAY_IS_ERR(t1)) { + if (t1 && RAY_IS_ERR(t1)) ray_error_free(t1); + return false; + } + ray_err_t err = ray_splay_save(t1, day1, NULL); + ray_release(t1); + if (err != RAY_OK) return false; + + ray_t* t2 = lang_parted_wide_table(2); + if (!t2 || RAY_IS_ERR(t2)) { + if (t2 && RAY_IS_ERR(t2)) ray_error_free(t2); + return false; + } + err = ray_splay_save(t2, day2, NULL); + ray_release(t2); + return err == RAY_OK; +} + +static ray_t* lang_parted_wide_payload_vec(int8_t type, bool empty) { + ray_t* v = ray_vec_new(type, empty ? 0 : 2); + if (!v || RAY_IS_ERR(v) || empty) return v; + v->len = 2; + switch (type) { + case RAY_BOOL: { + uint8_t* d = (uint8_t*)ray_data(v); d[0] = 1; d[1] = 0; break; + } + case RAY_U8: { + uint8_t* d = (uint8_t*)ray_data(v); d[0] = 7; d[1] = 8; break; + } + case RAY_I16: { + int16_t* d = (int16_t*)ray_data(v); d[0] = -123; d[1] = 456; break; + } + case RAY_I32: { + int32_t* d = (int32_t*)ray_data(v); d[0] = 123456; d[1] = -654321; break; + } + case RAY_F32: { + float* d = (float*)ray_data(v); d[0] = 1.5f; d[1] = 0.0f; + ray_vec_set_null(v, 1, true); break; + } + case RAY_F64: { + double* d = (double*)ray_data(v); d[0] = 2.5; d[1] = -3.5; break; + } + case RAY_DATE: { + int32_t* d = (int32_t*)ray_data(v); d[0] = 9132; d[1] = 9133; break; + } + case RAY_TIME: { + int32_t* d = (int32_t*)ray_data(v); + d[0] = 3723004; d[1] = 18367008; break; + } + case RAY_TIMESTAMP: { + int64_t* d = (int64_t*)ray_data(v); + d[0] = 1111111111LL; d[1] = 2222222222LL; break; + } + case RAY_GUID: { + uint8_t* d = (uint8_t*)ray_data(v); + for (int i = 0; i < 16; i++) d[i] = (uint8_t)(0xA0 + i); + memset(d + 16, 0, 16); + ray_vec_set_null(v, 1, true); + break; + } + default: ray_release(v); return ray_error("type", NULL); + } + return v; +} + +static bool lang_parted_wide_bind_payloads(void) { + for (int i = 0; i < LANG_WIDE_NCOLS; i++) { + ray_t* payload = lang_parted_wide_payload_vec(lang_parted_wide_types[i], + false); + if (!payload || RAY_IS_ERR(payload)) { + if (payload && RAY_IS_ERR(payload)) ray_error_free(payload); + return false; + } + int64_t id = ray_sym_intern(lang_parted_wide_payload_names[i], + strlen(lang_parted_wide_payload_names[i])); + ray_err_t err = ray_env_bind_flat(id, payload); + ray_release(payload); + if (err != RAY_OK) return false; + + ray_t* zero = lang_parted_wide_payload_vec(lang_parted_wide_types[i], + true); + if (!zero || RAY_IS_ERR(zero)) { + if (zero && RAY_IS_ERR(zero)) ray_error_free(zero); + return false; + } + id = ray_sym_intern(lang_parted_wide_zero_names[i], + strlen(lang_parted_wide_zero_names[i])); + err = ray_env_bind_flat(id, zero); + ray_release(zero); + if (err != RAY_OK) return false; + } + return true; +} + /* ---- Test: insert ---- */ static test_result_t test_eval_insert(void) { ray_t* result = ray_eval_str( @@ -2699,6 +3024,1129 @@ static test_result_t test_eval_insert_positional_errors(void) { PASS(); } +/* ---- Test: insert into a canonical parted table ----------------------- */ + +static test_result_t test_eval_insert_parted_e2e_impl(const char* root) { + TEST_ASSERT(lang_parted_insert_fixture(root), "create parted fixture"); + + char src[1400]; + int n = snprintf(src, sizeof(src), + "(set p (.db.parted.get \"%s\" 'trades))", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(src), "format parted get"); + ray_t* setup = ray_eval_str(src); + TEST_ASSERT_NOT_NULL(setup); + TEST_ASSERT_FALSE(RAY_IS_ERR(setup)); + ray_release(setup); + + char sym_path[1200], disk_id_path[1200]; + struct stat sym_before, sym_after, disk_id_before, disk_id_after; + n = snprintf(sym_path, sizeof(sym_path), "%s/.sym", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(sym_path), "format sym path"); + TEST_ASSERT(stat(sym_path, &sym_before) == 0, "stat fixture symfile"); + n = snprintf(disk_id_path, sizeof(disk_id_path), + "%s/2024.01.02/trades/id", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(disk_id_path), + "format current disk column path"); + TEST_ASSERT(stat(disk_id_path, &disk_id_before) == 0, + "stat current disk column"); + + ray_t* before = ray_eval_str("p"); + TEST_ASSERT_NOT_NULL(before); + TEST_ASSERT_FALSE(RAY_IS_ERR(before)); + TEST_ASSERT_EQ_I(before->type, RAY_TABLE); + TEST_ASSERT_EQ_I(ray_table_nrows(before), 4); + TEST_ASSERT_EQ_I(ray_table_ncols(before), 5); + + ray_t* mc = ray_table_get_col_idx(before, 0); + TEST_ASSERT_NOT_NULL(mc); + TEST_ASSERT_EQ_I(mc->type, RAY_MAPCOMMON); + TEST_ASSERT_EQ_U(mc->attrs, RAY_MC_DATE); + ray_t* counts = lang_parted_insert_counts(before); + TEST_ASSERT_NOT_NULL(counts); + TEST_ASSERT_EQ_I(counts->len, 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[0], 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[1], 2); + + const char* physical[] = { "id", "ticker", "note", "qty" }; + ray_t* hist_seg[4]; + ray_t* old_tail[4]; + for (size_t i = 0; i < 4; i++) { + ray_t* col = lang_parted_insert_col(before, physical[i]); + TEST_ASSERT_NOT_NULL(col); + TEST_ASSERT_TRUE(RAY_IS_PARTED(col->type)); + TEST_ASSERT_EQ_I(col->len, 2); + ray_t** segs = (ray_t**)ray_data(col); + hist_seg[i] = segs[0]; + old_tail[i] = segs[1]; + TEST_ASSERT_EQ_I(segs[0]->len, 2); + TEST_ASSERT_EQ_I(segs[1]->len, 2); + TEST_ASSERT_EQ_U(segs[0]->mmod, 1); + TEST_ASSERT_EQ_U(segs[1]->mmod, 1); + } + + ray_t* base_sym_col = lang_parted_insert_col(before, "ticker"); + ray_t** base_sym_segs = (ray_t**)ray_data(base_sym_col); + struct ray_sym_domain_s* file_dom = ray_sym_vec_domain(base_sym_segs[0]); + TEST_ASSERT_NOT_NULL(file_dom); + TEST_ASSERT(file_dom != ray_sym_runtime_domain(), + "historical ticker uses FILE domain"); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(base_sym_segs[1]), file_dom); + + /* Functional form returns a fresh snapshot and leaves `p` unchanged. */ + ray_t* inserted = ray_eval_str( + "(set f (insert p 2024.01.02 " + " (list 5 'gamma \"five\" 50)))"); + TEST_ASSERT_NOT_NULL(inserted); + TEST_ASSERT_FALSE(RAY_IS_ERR(inserted)); + ray_release(inserted); + + ray_t* functional = ray_eval_str("f"); + TEST_ASSERT_NOT_NULL(functional); + TEST_ASSERT_FALSE(RAY_IS_ERR(functional)); + TEST_ASSERT_EQ_I(ray_table_nrows(functional), 5); + counts = lang_parted_insert_counts(functional); + TEST_ASSERT_NOT_NULL(counts); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[0], 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[1], 3); + for (size_t i = 0; i < 4; i++) { + ray_t* col = lang_parted_insert_col(functional, physical[i]); + TEST_ASSERT_NOT_NULL(col); + ray_t** segs = (ray_t**)ray_data(col); + TEST_ASSERT_EQ_PTR(segs[0], hist_seg[i]); + TEST_ASSERT(segs[1] != old_tail[i], "tail copied before append"); + TEST_ASSERT_EQ_I(segs[1]->len, 3); + TEST_ASSERT_EQ_U(segs[1]->mmod, 0); + } + ray_t** functional_sym = (ray_t**)ray_data( + lang_parted_insert_col(functional, "ticker")); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(functional_sym[1]), file_dom); + ray_t* gamma = ray_sym_vec_cell(functional_sym[1], 2); + TEST_ASSERT_NOT_NULL(gamma); + TEST_ASSERT_EQ_U(ray_str_len(gamma), 5); + TEST_ASSERT_MEM_EQ(5, ray_str_ptr(gamma), "gamma"); + + ASSERT_EQ("(count p)", "4"); + ASSERT_EQ("(count f)", "5"); + ASSERT_EQ("(at (select {id: id from: f where: (== ticker 'gamma)}) 'id)", + "[5]"); + + /* Symbol target returns the symbol and rebinds only that name. The + * exact DICT schema is order-independent and its atoms broadcast. */ + ASSERT_EQ("(insert 'p 2024.01.02 " + " {qty: 0Nl note: \"six\" ticker: 'delta id: 6})", + "'p"); + ASSERT_EQ("(count p)", "5"); + ASSERT_EQ("(count f)", "5"); + + /* Retain a snapshot whose active segment is already heap-backed. The + * next insert must replace that segment, never mutate it through COW. */ + ray_t* mid_set = ray_eval_str("(set mid p)"); + TEST_ASSERT_NOT_NULL(mid_set); + TEST_ASSERT_FALSE(RAY_IS_ERR(mid_set)); + ray_release(mid_set); + + ASSERT_EQ("(insert 'p 2024.01.02 " + " {note: \"bulk\" qty: [70 80] " + " ticker: 'alpha id: [7 8]})", + "'p"); + ASSERT_EQ("(count mid)", "5"); + ASSERT_EQ("(count (at (select {id: id from: mid where: (>= id 7)}) 'id))", + "0"); + + /* A greater key creates a new heap tail. TABLE payload schemas are + * name-based, so deliberately reverse the physical-column order. */ + ASSERT_EQ("(insert 'p 2024.01.03 " + " (table ['note 'qty 'ticker 'id] " + " (list [\"nine\" \"ten\"] [90 0Nl] " + " ['gamma 'beta] [9 10])))", + "'p"); + + ASSERT_EQ("(count p)", "9"); + ASSERT_EQ("(sum (at p 'qty))", "320"); + ASSERT_EQ("(at (select {id: id from: p where: (== ticker 'gamma)}) 'id)", + "[9]"); + ASSERT_EQ("(at (select {from: p by: date c: (count id)}) 'c)", + "[2 5 2]"); + ASSERT_EQ("(do (set pg (select {from: p by: ticker c: (count id)})) " + " (at (select {c: c from: pg where: (== ticker 'alpha)}) 'c))", + "[4]"); + + ray_t* live = ray_eval_str("p"); + TEST_ASSERT_NOT_NULL(live); + TEST_ASSERT_FALSE(RAY_IS_ERR(live)); + ray_t* mid = ray_eval_str("mid"); + TEST_ASSERT_NOT_NULL(mid); + TEST_ASSERT_FALSE(RAY_IS_ERR(mid)); + TEST_ASSERT_EQ_I(ray_table_nrows(live), 9); + TEST_ASSERT_EQ_I(ray_table_nrows(mid), 5); + TEST_ASSERT_EQ_I(ray_table_nrows(before), 4); + TEST_ASSERT_EQ_I(ray_table_nrows(functional), 5); + + for (size_t i = 0; i < 4; i++) { + ray_t** mid_segs = (ray_t**)ray_data( + lang_parted_insert_col(mid, physical[i])); + ray_t** live_segs_cmp = (ray_t**)ray_data( + lang_parted_insert_col(live, physical[i])); + TEST_ASSERT_EQ_PTR(mid_segs[0], hist_seg[i]); + TEST_ASSERT(mid_segs[1] != live_segs_cmp[1], + "heap-backed retained snapshot was mutated"); + TEST_ASSERT_EQ_I(mid_segs[1]->len, 3); + TEST_ASSERT_EQ_I(live_segs_cmp[1]->len, 5); + } + + mc = ray_table_get_col_idx(live, 0); + TEST_ASSERT_NOT_NULL(mc); + ray_t** mc_parts = (ray_t**)ray_data(mc); + TEST_ASSERT_EQ_I(mc_parts[0]->len, 3); + TEST_ASSERT_EQ_I(mc_parts[1]->len, 3); + int64_t* live_counts = (int64_t*)ray_data(mc_parts[1]); + TEST_ASSERT_EQ_I(live_counts[0], 2); + TEST_ASSERT_EQ_I(live_counts[1], 5); + TEST_ASSERT_EQ_I(live_counts[2], 2); + + for (size_t i = 0; i < 4; i++) { + ray_t* col = lang_parted_insert_col(live, physical[i]); + TEST_ASSERT_NOT_NULL(col); + TEST_ASSERT_TRUE(RAY_IS_PARTED(col->type)); + TEST_ASSERT_EQ_I(col->len, 3); + ray_t** segs = (ray_t**)ray_data(col); + TEST_ASSERT_EQ_PTR(segs[0], hist_seg[i]); + TEST_ASSERT_EQ_I(segs[0]->len, 2); + TEST_ASSERT_EQ_U(segs[0]->mmod, 1); + TEST_ASSERT_EQ_I(segs[1]->len, 5); + TEST_ASSERT_EQ_U(segs[1]->mmod, 0); + TEST_ASSERT_EQ_I(segs[2]->len, 2); + TEST_ASSERT_EQ_U(segs[2]->mmod, 0); + } + + ray_t** live_sym = (ray_t**)ray_data( + lang_parted_insert_col(live, "ticker")); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(live_sym[0]), file_dom); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(live_sym[1]), file_dom); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(live_sym[2]), file_dom); + ray_t* delta = ray_sym_vec_cell(live_sym[1], 2); + gamma = ray_sym_vec_cell(live_sym[2], 0); + TEST_ASSERT_NOT_NULL(delta); + TEST_ASSERT_NOT_NULL(gamma); + TEST_ASSERT_EQ_U(ray_str_len(delta), 5); + TEST_ASSERT_MEM_EQ(5, ray_str_ptr(delta), "delta"); + TEST_ASSERT_EQ_U(ray_str_len(gamma), 5); + TEST_ASSERT_MEM_EQ(5, ray_str_ptr(gamma), "gamma"); + + ray_t** live_note = (ray_t**)ray_data( + lang_parted_insert_col(live, "note")); + size_t slen = 0; + const char* sval = ray_str_vec_get(live_note[1], 2, &slen); + TEST_ASSERT_NOT_NULL(sval); + TEST_ASSERT_EQ_U(slen, 3); + TEST_ASSERT_MEM_EQ(3, sval, "six"); + sval = ray_str_vec_get(live_note[1], 3, &slen); + TEST_ASSERT_NOT_NULL(sval); + TEST_ASSERT_EQ_U(slen, 4); + TEST_ASSERT_MEM_EQ(4, sval, "bulk"); + + ray_t** live_qty = (ray_t**)ray_data( + lang_parted_insert_col(live, "qty")); + TEST_ASSERT_TRUE(ray_vec_is_null(live_qty[0], 1)); + TEST_ASSERT_TRUE(ray_vec_is_null(live_qty[1], 2)); + TEST_ASSERT_TRUE(ray_vec_is_null(live_qty[2], 1)); + + /* A positional LIST batch is column-oriented. Scalar physical values + * broadcast across the vector-established batch length. Building a + * fourth partition functionally also proves that more than one heap-only + * date can coexist without changing the published source binding. */ + ray_t* future_set = ray_eval_str( + "(set future (insert p 2024.01.04 " + " (list [11 12] ['epsilon 'alpha] " + " \"future\" [110 120])))"); + TEST_ASSERT_NOT_NULL(future_set); + TEST_ASSERT_FALSE(RAY_IS_ERR(future_set)); + ray_release(future_set); + + ASSERT_EQ("(count p)", "9"); + ASSERT_EQ("(count future)", "11"); + ASSERT_EQ("(at (select {from: future by: date c: (count id)}) 'c)", + "[2 5 2 2]"); + ASSERT_EQ("(at (select {id: id from: future " + " where: (== ticker 'epsilon)}) 'id)", + "[11]"); + + ray_t* future = ray_eval_str("future"); + TEST_ASSERT_NOT_NULL(future); + TEST_ASSERT_FALSE(RAY_IS_ERR(future)); + ray_t** future_sym = (ray_t**)ray_data( + lang_parted_insert_col(future, "ticker")); + TEST_ASSERT_EQ_I(lang_parted_insert_col(future, "ticker")->len, 4); + for (int64_t pidx = 0; pidx < 4; pidx++) + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(future_sym[pidx]), file_dom); + TEST_ASSERT_EQ_PTR(future_sym[0], live_sym[0]); + TEST_ASSERT_EQ_PTR(future_sym[1], live_sym[1]); + TEST_ASSERT_EQ_PTR(future_sym[2], live_sym[2]); + TEST_ASSERT_EQ_U(future_sym[0]->mmod, 1); + TEST_ASSERT_EQ_U(future_sym[1]->mmod, 0); + TEST_ASSERT_EQ_U(future_sym[2]->mmod, 0); + TEST_ASSERT_EQ_U(future_sym[3]->mmod, 0); + + ray_t** future_note = (ray_t**)ray_data( + lang_parted_insert_col(future, "note")); + TEST_ASSERT_EQ_I(future_note[3]->len, 2); + sval = ray_str_vec_get(future_note[3], 0, &slen); + TEST_ASSERT_NOT_NULL(sval); + TEST_ASSERT_EQ_U(slen, 6); + TEST_ASSERT_MEM_EQ(6, sval, "future"); + sval = ray_str_vec_get(future_note[3], 1, &slen); + TEST_ASSERT_NOT_NULL(sval); + TEST_ASSERT_EQ_U(slen, 6); + TEST_ASSERT_MEM_EQ(6, sval, "future"); + + /* Once 2024.01.04 exists, even the heap-backed 2024.01.03 partition is + * immutable. A rejected quoted insert must preserve the exact binding. */ + ray_t* older_err = ray_eval_str( + "(insert 'future 2024.01.03 (list 13 'alpha \"older\" 130))"); + TEST_ASSERT_NOT_NULL(older_err); + TEST_ASSERT_TRUE(RAY_IS_ERR(older_err)); + TEST_ASSERT_EQ_I(ray_err_from_obj(older_err), RAY_ERR_DOMAIN); + ray_error_free(older_err); + ray_t* future_after = ray_eval_str("future"); + TEST_ASSERT_NOT_NULL(future_after); + TEST_ASSERT_FALSE(RAY_IS_ERR(future_after)); + TEST_ASSERT_EQ_PTR(future_after, future); + TEST_ASSERT_EQ_I(ray_table_nrows(future_after), 11); + ray_release(future_after); + + /* Live tails are not persisted: no third directory is created and a + * fresh canonical read still observes the original 2+2 disk rows. */ + char day3[1200], day4[1200]; + n = snprintf(day3, sizeof(day3), "%s/2024.01.03", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(day3), "format day3 path"); + TEST_ASSERT(access(day3, F_OK) != 0, "greater-key insert created a partition"); + n = snprintf(day4, sizeof(day4), "%s/2024.01.04", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(day4), "format day4 path"); + TEST_ASSERT(access(day4, F_OK) != 0, + "functional greater-key insert created a partition"); + TEST_ASSERT(stat(sym_path, &sym_after) == 0, "stat symfile after inserts"); + TEST_ASSERT_EQ_I(sym_after.st_size, sym_before.st_size); + TEST_ASSERT(stat(disk_id_path, &disk_id_after) == 0, + "stat current disk column after inserts"); + TEST_ASSERT_EQ_I(disk_id_after.st_size, disk_id_before.st_size); + + ray_t* cold = ray_read_parted(root, "trades"); + TEST_ASSERT_NOT_NULL(cold); + TEST_ASSERT_FALSE(RAY_IS_ERR(cold)); + TEST_ASSERT_EQ_I(ray_table_nrows(cold), 4); + counts = lang_parted_insert_counts(cold); + TEST_ASSERT_NOT_NULL(counts); + TEST_ASSERT_EQ_I(counts->len, 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[0], 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[1], 2); + ray_t* cold_id = lang_parted_insert_col(cold, "id"); + TEST_ASSERT_NOT_NULL(cold_id); + ray_t** cold_id_segs = (ray_t**)ray_data(cold_id); + TEST_ASSERT_EQ_U(cold_id_segs[0]->mmod, 1); + TEST_ASSERT_EQ_U(cold_id_segs[1]->mmod, 1); + + ray_release(cold); + ray_release(future); + ray_release(live); + ray_release(mid); + ray_release(functional); + ray_release(before); + PASS(); +} + +static test_result_t test_eval_insert_parted_e2e(void) { + char root[512]; + int n = snprintf(root, sizeof(root), + "/tmp/rayforce_lang_parted_insert_e2e_%ld", + (long)getpid()); + if (n <= 0 || (size_t)n >= sizeof(root)) + return (test_result_t){ TEST_FAIL, "parted fixture path overflow" }; + lang_parted_insert_rm_rf(root); + test_result_t result = test_eval_insert_parted_e2e_impl(root); + lang_parted_insert_rm_rf(root); + return result; +} + +/* ---- Test: live FILE-domain symbols persist only at explicit rollover - */ + +static test_result_t test_eval_insert_parted_rollover_impl(const char* root) { + TEST_ASSERT(lang_parted_insert_sym_fixture(root), + "create shared-symbol parted fixture"); + + char src[3200], sym_path[1200], day3[1200]; + int n = snprintf(src, sizeof(src), + "(set p (.db.parted.get \"%s\" 'trades))", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(src), "format parted get"); + ray_t* setup = ray_eval_str(src); + TEST_ASSERT_NOT_NULL(setup); + TEST_ASSERT_FALSE(RAY_IS_ERR(setup)); + ray_release(setup); + + n = snprintf(sym_path, sizeof(sym_path), "%s/.sym", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(sym_path), "format sym path"); + n = snprintf(day3, sizeof(day3), "%s/2024.01.03", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(day3), "format day3 path"); + + struct stat sym_before, sym_live, sym_persisted; + TEST_ASSERT(stat(sym_path, &sym_before) == 0, "stat fixture symfile"); + int64_t disk_syms_before = -1, disk_syms_live = -1; + int64_t disk_syms_persisted = -1; + TEST_ASSERT(lang_parted_insert_symfile_count(sym_path, &disk_syms_before), + "read fixture symfile count"); + TEST_ASSERT_EQ_I(disk_syms_before, 5); /* "", alpha, beta, xnys, xnas */ + + ray_t* base = ray_eval_str("p"); + TEST_ASSERT_NOT_NULL(base); + TEST_ASSERT_FALSE(RAY_IS_ERR(base)); + ray_t** base_ticker = (ray_t**)ray_data( + lang_parted_insert_col(base, "ticker")); + ray_t** base_venue = (ray_t**)ray_data( + lang_parted_insert_col(base, "venue")); + struct ray_sym_domain_s* live_dom = ray_sym_vec_domain(base_ticker[0]); + TEST_ASSERT_NOT_NULL(live_dom); + TEST_ASSERT(live_dom != ray_sym_runtime_domain(), + "fixture symbols use a FILE domain"); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(base_ticker[1]), live_dom); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(base_venue[0]), live_dom); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(base_venue[1]), live_dom); + TEST_ASSERT_EQ_I(ray_sym_domain_count(live_dom), disk_syms_before); + + /* New vocabulary entries are reused across two SYM columns and across a + * copied last-disk segment plus a newly-created memory partition. */ + ASSERT_EQ("(insert 'p 2024.01.02 " + " (list [5 6] ['ticker_only 'omni] " + " ['venue_only 'omni]))", + "'p"); + ASSERT_EQ("(insert 'p 2024.01.03 " + " (list [7 8] ['omni 'venue_only] " + " ['omni 'ticker_only]))", + "'p"); + ASSERT_EQ("(at (select {id: id from: p " + " where: (== ticker 'omni)}) 'id)", + "[6 7]"); + ASSERT_EQ("(at (select {id: id from: p " + " where: (== venue 'omni)}) 'id)", + "[6 7]"); + TEST_ASSERT(stat(sym_path, &sym_live) == 0, + "stat symfile with live symbols"); + TEST_ASSERT_EQ_I(sym_live.st_size, sym_before.st_size); + TEST_ASSERT(lang_parted_insert_symfile_count(sym_path, &disk_syms_live), + "read symfile count after live inserts"); + TEST_ASSERT_EQ_I(disk_syms_live, disk_syms_before); + TEST_ASSERT(access(day3, F_OK) != 0, + "live symbol insert created an on-disk partition"); + + ray_t* live = ray_eval_str("p"); + TEST_ASSERT_NOT_NULL(live); + TEST_ASSERT_FALSE(RAY_IS_ERR(live)); + TEST_ASSERT_EQ_I(ray_table_nrows(live), 8); + ray_t* counts = lang_parted_insert_counts(live); + TEST_ASSERT_NOT_NULL(counts); + TEST_ASSERT_EQ_I(counts->len, 3); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[0], 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[1], 4); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[2], 2); + + ray_t* live_ticker_col = lang_parted_insert_col(live, "ticker"); + ray_t* live_venue_col = lang_parted_insert_col(live, "venue"); + TEST_ASSERT_NOT_NULL(live_ticker_col); + TEST_ASSERT_NOT_NULL(live_venue_col); + TEST_ASSERT_EQ_I(live_ticker_col->len, 3); + TEST_ASSERT_EQ_I(live_venue_col->len, 3); + ray_t** live_ticker = (ray_t**)ray_data(live_ticker_col); + ray_t** live_venue = (ray_t**)ray_data(live_venue_col); + for (int64_t pidx = 0; pidx < 3; pidx++) { + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(live_ticker[pidx]), live_dom); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(live_venue[pidx]), live_dom); + } + TEST_ASSERT_EQ_U(live_ticker[0]->mmod, 1); + TEST_ASSERT_EQ_U(live_venue[0]->mmod, 1); + for (int64_t pidx = 1; pidx < 3; pidx++) { + TEST_ASSERT_EQ_U(live_ticker[pidx]->mmod, 0); + TEST_ASSERT_EQ_U(live_venue[pidx]->mmod, 0); + } + TEST_ASSERT_EQ_I(ray_sym_domain_count(live_dom), disk_syms_before + 3); + + int64_t omni_pos = ray_sym_domain_find(live_dom, "omni", 4); + TEST_ASSERT(omni_pos >= 0, "omni missing from live FILE domain"); + TEST_ASSERT_EQ_I(ray_read_sym(ray_data(live_ticker[1]), 3, + RAY_SYM, live_ticker[1]->attrs), omni_pos); + TEST_ASSERT_EQ_I(ray_read_sym(ray_data(live_venue[1]), 3, + RAY_SYM, live_venue[1]->attrs), omni_pos); + TEST_ASSERT_EQ_I(ray_read_sym(ray_data(live_ticker[2]), 0, + RAY_SYM, live_ticker[2]->attrs), omni_pos); + TEST_ASSERT_EQ_I(ray_read_sym(ray_data(live_venue[2]), 0, + RAY_SYM, live_venue[2]->attrs), omni_pos); + ray_t* omni = ray_sym_domain_str(live_dom, omni_pos); + TEST_ASSERT_NOT_NULL(omni); + TEST_ASSERT_EQ_U(ray_str_len(omni), 4); + TEST_ASSERT_MEM_EQ(4, ray_str_ptr(omni), "omni"); + + /* Rollover projects away the virtual partition column. splayed.set + * flushes the enlarged vocabulary before writing columns that reference + * its new positions, then a canonical read sees an mmap-backed day. */ + n = snprintf( + src, sizeof(src), + "(do (set closed " + " (select {from: p where: (== date 2024.01.03) " + " id: id ticker: ticker venue: venue})) " + " (.db.splayed.set \"%s/2024.01.03/trades\" closed \"%s\"))", + root, sym_path); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(src), "format rollover"); + ray_t* saved = ray_eval_str(src); + TEST_ASSERT_NOT_NULL(saved); + TEST_ASSERT_FALSE(RAY_IS_ERR(saved)); + ray_release(saved); + + TEST_ASSERT(stat(sym_path, &sym_persisted) == 0, + "stat persisted symfile"); + TEST_ASSERT(sym_persisted.st_size > sym_before.st_size, + "rollover did not persist the enlarged symbol vocabulary"); + TEST_ASSERT(lang_parted_insert_symfile_count(sym_path, + &disk_syms_persisted), + "read persisted symfile count"); + TEST_ASSERT_EQ_I(disk_syms_persisted, disk_syms_before + 3); + TEST_ASSERT(access(day3, F_OK) == 0, + "rollover did not create the physical partition"); + + /* Drop every holder so the following read reopens the FILE domain from + * disk instead of observing the process cache's already-extended object. */ + ray_release(live); + ray_release(base); + ray_t* deleted = ray_eval_str("(do (del p) (del closed))"); + TEST_ASSERT_NOT_NULL(deleted); + TEST_ASSERT_FALSE(RAY_IS_ERR(deleted)); + ray_release(deleted); + + ray_t* cold = ray_read_parted(root, "trades"); + TEST_ASSERT_NOT_NULL(cold); + TEST_ASSERT_FALSE(RAY_IS_ERR(cold)); + TEST_ASSERT_EQ_I(ray_table_nrows(cold), 6); + counts = lang_parted_insert_counts(cold); + TEST_ASSERT_NOT_NULL(counts); + TEST_ASSERT_EQ_I(counts->len, 3); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[0], 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[1], 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[2], 2); + + ray_t* cold_ticker_col = lang_parted_insert_col(cold, "ticker"); + ray_t* cold_venue_col = lang_parted_insert_col(cold, "venue"); + TEST_ASSERT_NOT_NULL(cold_ticker_col); + TEST_ASSERT_NOT_NULL(cold_venue_col); + TEST_ASSERT_EQ_I(cold_ticker_col->len, 3); + TEST_ASSERT_EQ_I(cold_venue_col->len, 3); + ray_t** cold_ticker = (ray_t**)ray_data(cold_ticker_col); + ray_t** cold_venue = (ray_t**)ray_data(cold_venue_col); + struct ray_sym_domain_s* cold_dom = ray_sym_vec_domain(cold_ticker[0]); + TEST_ASSERT_NOT_NULL(cold_dom); + for (int64_t pidx = 0; pidx < 3; pidx++) { + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(cold_ticker[pidx]), cold_dom); + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(cold_venue[pidx]), cold_dom); + TEST_ASSERT_EQ_U(cold_ticker[pidx]->mmod, 1); + TEST_ASSERT_EQ_U(cold_venue[pidx]->mmod, 1); + } + TEST_ASSERT_EQ_I(ray_sym_domain_count(cold_dom), disk_syms_persisted); + TEST_ASSERT_EQ_I(ray_sym_domain_find(cold_dom, "omni", 4), omni_pos); + + ray_t* ticker0 = ray_sym_vec_cell(cold_ticker[2], 0); + ray_t* ticker1 = ray_sym_vec_cell(cold_ticker[2], 1); + ray_t* venue0 = ray_sym_vec_cell(cold_venue[2], 0); + ray_t* venue1 = ray_sym_vec_cell(cold_venue[2], 1); + TEST_ASSERT_NOT_NULL(ticker0); + TEST_ASSERT_NOT_NULL(ticker1); + TEST_ASSERT_NOT_NULL(venue0); + TEST_ASSERT_NOT_NULL(venue1); + TEST_ASSERT_EQ_U(ray_str_len(ticker0), 4); + TEST_ASSERT_MEM_EQ(4, ray_str_ptr(ticker0), "omni"); + TEST_ASSERT_EQ_U(ray_str_len(ticker1), 10); + TEST_ASSERT_MEM_EQ(10, ray_str_ptr(ticker1), "venue_only"); + TEST_ASSERT_EQ_U(ray_str_len(venue0), 4); + TEST_ASSERT_MEM_EQ(4, ray_str_ptr(venue0), "omni"); + TEST_ASSERT_EQ_U(ray_str_len(venue1), 11); + TEST_ASSERT_MEM_EQ(11, ray_str_ptr(venue1), "ticker_only"); + + ray_t* cold_id_col = lang_parted_insert_col(cold, "id"); + TEST_ASSERT_NOT_NULL(cold_id_col); + ray_t** cold_id = (ray_t**)ray_data(cold_id_col); + TEST_ASSERT_EQ_U(cold_id[2]->mmod, 1); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(cold_id[2]))[0], 7); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(cold_id[2]))[1], 8); + + ray_release(cold); + PASS(); +} + +static test_result_t test_eval_insert_parted_rollover(void) { + char root[512]; + int n = snprintf(root, sizeof(root), + "/tmp/rayforce_lang_parted_insert_rollover_%ld", + (long)getpid()); + if (n <= 0 || (size_t)n >= sizeof(root)) + return (test_result_t){ TEST_FAIL, "parted fixture path overflow" }; + lang_parted_insert_rm_rf(root); + test_result_t result = test_eval_insert_parted_rollover_impl(root); + lang_parted_insert_rm_rf(root); + return result; +} + +/* ---- Test: partition-aware insert rejects ambiguous/unsafe shapes ------ */ + +static test_result_t test_eval_insert_parted_errors_impl(const char* root) { + TEST_ASSERT(lang_parted_insert_fixture(root), "create parted fixture"); + + char src[1400]; + int n = snprintf(src, sizeof(src), + "(set p (.db.parted.get \"%s\" 'trades))", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(src), "format parted get"); + ray_t* setup = ray_eval_str(src); + TEST_ASSERT_NOT_NULL(setup); + TEST_ASSERT_FALSE(RAY_IS_ERR(setup)); + ray_release(setup); + + /* Rebinding a quoted reserved dotted name must fail after target + * resolution, without replacing or mutating the existing binding. */ + static const char reserved_name[] = ".test.parted.insert_rebind"; + int64_t reserved_id = ray_sym_intern(reserved_name, + sizeof(reserved_name) - 1); + ray_t* reserved_source = ray_eval_str("p"); + TEST_ASSERT_NOT_NULL(reserved_source); + TEST_ASSERT_FALSE(RAY_IS_ERR(reserved_source)); + TEST_ASSERT_EQ_I(ray_table_nrows(reserved_source), 4); + TEST_ASSERT_EQ_I(ray_env_bind_flat(reserved_id, reserved_source), RAY_OK); + + ray_t* reserved_err = ray_eval_str( + "(insert '.test.parted.insert_rebind 2024.01.02 " + " (list 5 'alpha \"five\" 50))"); + TEST_ASSERT_NOT_NULL(reserved_err); + TEST_ASSERT_TRUE(RAY_IS_ERR(reserved_err)); + TEST_ASSERT_EQ_I(ray_err_from_obj(reserved_err), RAY_ERR_RESERVED); + ray_error_free(reserved_err); + + ray_t* reserved_bound = ray_env_get(reserved_id); /* borrowed */ + TEST_ASSERT_EQ_PTR(reserved_bound, reserved_source); + TEST_ASSERT_EQ_I(ray_table_nrows(reserved_bound), 4); + TEST_ASSERT_EQ_I(ray_table_nrows(reserved_source), 4); + ASSERT_EQ("(count .test.parted.insert_rebind)", "4"); + ASSERT_EQ("(count p)", "4"); + + /* Exercise a checked dotted rebind failure after successful flat + * lookup: the fully-qualified name resolves directly to the PARTED + * table, but ray_env_set must walk through `live`, which is an atom. */ + static const char dotted_name[] = "live.parted"; + int64_t dotted_id = ray_sym_intern(dotted_name, sizeof(dotted_name) - 1); + int64_t head_id = ray_sym_intern("live", 4); + ray_t* head_atom = ray_i64(7); + TEST_ASSERT_NOT_NULL(head_atom); + TEST_ASSERT_EQ_I(ray_env_bind_flat(dotted_id, reserved_source), RAY_OK); + TEST_ASSERT_EQ_I(ray_env_bind_flat(head_id, head_atom), RAY_OK); + ray_release(head_atom); + + ray_t* dotted_err = ray_eval_str( + "(insert 'live.parted 2024.01.02 " + " (list 5 'alpha \"five\" 50))"); + TEST_ASSERT_NOT_NULL(dotted_err); + TEST_ASSERT_TRUE(RAY_IS_ERR(dotted_err)); + TEST_ASSERT_EQ_I(ray_err_from_obj(dotted_err), RAY_ERR_TYPE); + ray_error_free(dotted_err); + + ray_t* dotted_bound = ray_env_get(dotted_id); /* borrowed */ + TEST_ASSERT_EQ_PTR(dotted_bound, reserved_source); + TEST_ASSERT_EQ_I(ray_table_nrows(dotted_bound), 4); + TEST_ASSERT_EQ_I(ray_table_nrows(reserved_source), 4); + ASSERT_EQ("(count live.parted)", "4"); + ASSERT_EQ("(count p)", "4"); + ray_release(reserved_source); + + /* A PARTED table always needs its explicit partition key. */ + ASSERT_ER_CODE("(insert p (list 5 'alpha \"five\" 50))", "arity"); + ASSERT_ER_CODE("(insert 'p (list 5 'alpha \"five\" 50))", "arity"); + + /* PARTED upsert has no defined cross-segment semantics. */ + ASSERT_ER_CODE("(upsert p 1 (list 5 'alpha \"five\" 50))", "nyi"); + ASSERT_ER_CODE("(upsert 'p 1 (list 5 'alpha \"five\" 50))", "nyi"); + + /* Keys are exact typed atoms and may only equal or extend the tail. */ + ASSERT_ER_CODE("(insert p 2024.01.01 (list 5 'alpha \"five\" 50))", + "domain"); + ASSERT_ER_CODE("(insert 'p 8767 (list 5 'alpha \"five\" 50))", "type"); + ASSERT_ER_CODE("(insert p [2024.01.02] (list 5 'alpha \"five\" 50))", + "type"); + + /* Positional LIST rows contain every physical column exactly once; the + * virtual `date` MAPCOMMON column is never part of the payload. */ + ASSERT_ER("(insert p 2024.01.02 (list 5 'alpha \"five\"))", ""); + ASSERT_ER("(insert p 2024.01.02 " + " (list 2024.01.02 5 'alpha \"five\" 50))", ""); + + /* DICT payloads require the exact unique physical-name set. */ + ASSERT_ER("(insert p 2024.01.02 " + " (dict ['id 'ticker 'note] " + " (list 5 'alpha \"five\")))", ""); + ASSERT_ER("(insert p 2024.01.02 " + " {id: 5 ticker: 'alpha note: \"five\" qty: 50 " + " date: 2024.01.02})", ""); + ASSERT_ER("(insert p 2024.01.02 " + " (dict ['id 'id 'ticker 'note 'qty] " + " (list 5 6 'alpha \"five\" 50)))", ""); + + /* TABLE payloads obey the same exact-schema rule. */ + ASSERT_ER("(insert p 2024.01.02 " + " (table ['id 'ticker 'note] " + " (list [5] ['alpha] [\"five\"])))", ""); + ASSERT_ER("(insert p 2024.01.02 " + " (table ['id 'ticker 'note 'qty 'date] " + " (list [5] ['alpha] [\"five\"] [50] " + " [2024.01.02])))", ""); + ASSERT_ER("(insert p 2024.01.02 " + " (table ['id 'id 'ticker 'note 'qty] " + " (list [5] [6] ['alpha] [\"five\"] [50])))", ""); + + /* Collections determine batch cardinality; atoms broadcast, but two + * different collection lengths and wrong physical types fail loudly. */ + ASSERT_ER_CODE("(insert p 2024.01.02 " + " (list [5 6] ['alpha 'beta 'alpha] \"bulk\" [50 60]))", + "length"); + ASSERT_ER_CODE("(insert p 2024.01.02 (list 5 'alpha \"five\" 'bad))", + "type"); + + /* The arity-3 partition-aware form is reserved for canonical PARTED + * tables; it must not become an accidental positional TABLE insert. */ + ASSERT_ER_CODE("(do (set flat " + " (table ['id 'ticker 'note 'qty] " + " (list [1] ['alpha] [\"one\"] [10]))) " + " (insert flat 2024.01.02 (list 2 'beta \"two\" 20)))", + "type"); + + /* Every failure above is atomic: no rebind, segment growth, or new tail. */ + ASSERT_EQ("(count p)", "4"); + ASSERT_EQ("(at (select {from: p by: date c: (count id)}) 'c)", "[2 2]"); + + ray_t* p = ray_eval_str("p"); + TEST_ASSERT_NOT_NULL(p); + TEST_ASSERT_FALSE(RAY_IS_ERR(p)); + TEST_ASSERT_EQ_I(ray_table_nrows(p), 4); + ray_t* mc = ray_table_get_col_idx(p, 0); + TEST_ASSERT_NOT_NULL(mc); + TEST_ASSERT_EQ_I(mc->type, RAY_MAPCOMMON); + ray_t** mc_parts = (ray_t**)ray_data(mc); + TEST_ASSERT_EQ_I(mc_parts[0]->len, 2); + TEST_ASSERT_EQ_I(mc_parts[1]->len, 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(mc_parts[1]))[0], 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(mc_parts[1]))[1], 2); + for (int64_t c = 1; c < ray_table_ncols(p); c++) { + ray_t* col = ray_table_get_col_idx(p, c); + TEST_ASSERT_TRUE(RAY_IS_PARTED(col->type)); + TEST_ASSERT_EQ_I(col->len, 2); + ray_t** segs = (ray_t**)ray_data(col); + TEST_ASSERT_EQ_I(segs[0]->len, 2); + TEST_ASSERT_EQ_I(segs[1]->len, 2); + TEST_ASSERT_EQ_U(segs[0]->mmod, 1); + TEST_ASSERT_EQ_U(segs[1]->mmod, 1); + } + + char day3[1200]; + n = snprintf(day3, sizeof(day3), "%s/2024.01.03", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(day3), "format day3 path"); + TEST_ASSERT(access(day3, F_OK) != 0, "failed insert touched disk"); + ray_release(p); + PASS(); +} + +static test_result_t test_eval_insert_parted_errors(void) { + char root[512]; + int n = snprintf(root, sizeof(root), + "/tmp/rayforce_lang_parted_insert_errors_%ld", + (long)getpid()); + if (n <= 0 || (size_t)n >= sizeof(root)) + return (test_result_t){ TEST_FAIL, "parted fixture path overflow" }; + lang_parted_insert_rm_rf(root); + test_result_t result = test_eval_insert_parted_errors_impl(root); + lang_parted_insert_rm_rf(root); + return result; +} + +/* ---- Test: SYM keys and lexical-vs-numeric I64 directory ordering ------ */ + +static bool lang_parted_insert_onecol(const char* root, const char* part, + int64_t value) { + char dir[1200], src[256]; + int nd = snprintf(dir, sizeof(dir), "%s/%s/trades", root, part); + int ns = snprintf(src, sizeof(src), + "(table ['id] (list [%lld]))", (long long)value); + if (nd <= 0 || (size_t)nd >= sizeof(dir) || + ns <= 0 || (size_t)ns >= sizeof(src)) + return false; + return lang_parted_insert_save_table(src, dir, NULL); +} + +static test_result_t test_eval_insert_parted_key_types_impl(const char* root) { + char iroot[900], sroot[900], src[1400], next_path[1200]; + int ni = snprintf(iroot, sizeof(iroot), "%s/i64", root); + int ns = snprintf(sroot, sizeof(sroot), "%s/sym", root); + TEST_ASSERT(ni > 0 && (size_t)ni < sizeof(iroot), "format i64 root"); + TEST_ASSERT(ns > 0 && (size_t)ns < sizeof(sroot), "format sym root"); + + /* collect_part_dirs is byte-lexical, so these become [10,2]. The + * insert validator must reject that malformed numeric MAPCOMMON order + * instead of treating 2 as a growable tail after 10. */ + TEST_ASSERT(lang_parted_insert_onecol(iroot, "10", 10), + "save integer partition 10"); + TEST_ASSERT(lang_parted_insert_onecol(iroot, "2", 2), + "save integer partition 2"); + int n = snprintf(src, sizeof(src), + "(set pi (.db.parted.get \"%s\" 'trades))", iroot); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(src), "format i64 parted get"); + ray_t* setup = ray_eval_str(src); + TEST_ASSERT_NOT_NULL(setup); + TEST_ASSERT_FALSE(RAY_IS_ERR(setup)); + ray_release(setup); + ASSERT_ER_CODE("(insert pi 11 (list 11))", "corrupt"); + ASSERT_EQ("(count pi)", "2"); + + /* Opaque directory names use a SYM MAPCOMMON key. Equal-key growth and + * a lexically later key follow the same immutable-tail contract. */ + TEST_ASSERT(lang_parted_insert_onecol(sroot, "1.2", 1), + "save symbol partition 1.2"); + TEST_ASSERT(lang_parted_insert_onecol(sroot, "2.1", 2), + "save symbol partition 2.1"); + n = snprintf(src, sizeof(src), + "(set ps (.db.parted.get \"%s\" 'trades))", sroot); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(src), "format sym parted get"); + setup = ray_eval_str(src); + TEST_ASSERT_NOT_NULL(setup); + TEST_ASSERT_FALSE(RAY_IS_ERR(setup)); + ray_release(setup); + + ray_t* ps0 = ray_eval_str("ps"); + TEST_ASSERT_NOT_NULL(ps0); + TEST_ASSERT_FALSE(RAY_IS_ERR(ps0)); + ray_t* smc = ray_table_get_col_idx(ps0, 0); + TEST_ASSERT_NOT_NULL(smc); + TEST_ASSERT_EQ_I(smc->type, RAY_MAPCOMMON); + TEST_ASSERT_EQ_U(smc->attrs, RAY_MC_SYM); + ray_t* skeys = ((ray_t**)ray_data(smc))[0]; + TEST_ASSERT_EQ_PTR(ray_sym_vec_domain(skeys), ray_sym_runtime_domain()); + ray_t* ids = ray_vec_new(RAY_I64, 2); + TEST_ASSERT_NOT_NULL(ids); + TEST_ASSERT_FALSE(RAY_IS_ERR(ids)); + int64_t id = 3; + ids = ray_vec_append(ids, &id); + id = 4; + ids = ray_vec_append(ids, &id); + ray_t* rows = ray_list_new(1); + TEST_ASSERT_NOT_NULL(rows); + TEST_ASSERT_FALSE(RAY_IS_ERR(rows)); + rows = ray_list_append(rows, ids); + ray_release(ids); + ray_t* key = ray_sym(ray_sym_intern("2.1", 3)); + ray_t* args[3] = { ps0, key, rows }; + ray_t* ps1 = ray_insert(args, 3); + ray_release(key); + ray_release(rows); + TEST_ASSERT_NOT_NULL(ps1); + TEST_ASSERT_FALSE(RAY_IS_ERR(ps1)); + TEST_ASSERT_EQ_I(ray_table_nrows(ps0), 2); + TEST_ASSERT_EQ_I(ray_table_nrows(ps1), 4); + + rows = ray_list_new(1); + TEST_ASSERT_NOT_NULL(rows); + TEST_ASSERT_FALSE(RAY_IS_ERR(rows)); + ray_t* id5 = ray_i64(5); + rows = ray_list_append(rows, id5); + ray_release(id5); + key = ray_sym(ray_sym_intern("3.1", 3)); + ray_t* args2[3] = { ps1, key, rows }; + ray_t* ps2 = ray_insert(args2, 3); + ray_release(key); + ray_release(rows); + TEST_ASSERT_NOT_NULL(ps2); + TEST_ASSERT_FALSE(RAY_IS_ERR(ps2)); + TEST_ASSERT_EQ_I(ray_table_nrows(ps1), 4); + TEST_ASSERT_EQ_I(ray_table_nrows(ps2), 5); + + ray_env_set(ray_sym_intern("ps", 2), ps2); + ray_release(ps2); + ray_release(ps1); + ray_release(ps0); + + ASSERT_EQ("(count ps)", "5"); + ASSERT_EQ("(at (select {from: ps by: part c: (count id)}) 'c)", + "[1 3 1]"); + + n = snprintf(next_path, sizeof(next_path), "%s/3.1", sroot); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(next_path), "format next path"); + TEST_ASSERT(access(next_path, F_OK) != 0, + "symbol-key live partition touched disk"); + PASS(); +} + +static test_result_t test_eval_insert_parted_key_types(void) { + char root[512]; + int n = snprintf(root, sizeof(root), + "/tmp/rayforce_lang_parted_insert_keys_%ld", + (long)getpid()); + if (n <= 0 || (size_t)n >= sizeof(root)) + return (test_result_t){ TEST_FAIL, "parted key fixture path overflow" }; + lang_parted_insert_rm_rf(root); + test_result_t result = test_eval_insert_parted_key_types_impl(root); + lang_parted_insert_rm_rf(root); + return result; +} + +/* ---- Test: PARTED insert preserves every remaining physical type ------- */ + +static test_result_t test_eval_insert_parted_physical_types_impl( + const char* root) { + TEST_ASSERT(lang_parted_wide_fixture(root), "create wide parted fixture"); + TEST_ASSERT(lang_parted_wide_bind_payloads(), "bind typed payload vectors"); + + char src[1400]; + int n = snprintf(src, sizeof(src), + "(set wide (.db.parted.get \"%s\" 'wide))", root); + TEST_ASSERT(n > 0 && (size_t)n < sizeof(src), "format wide parted get"); + ray_t* setup = ray_eval_str(src); + TEST_ASSERT_NOT_NULL(setup); + TEST_ASSERT_FALSE(RAY_IS_ERR(setup)); + ray_release(setup); + + ray_t* source = ray_eval_str("wide"); + TEST_ASSERT_NOT_NULL(source); + TEST_ASSERT_FALSE(RAY_IS_ERR(source)); + TEST_ASSERT_EQ_I(ray_table_nrows(source), 2); + TEST_ASSERT_EQ_I(ray_table_ncols(source), LANG_WIDE_NCOLS + 1); + + ray_t* historical[LANG_WIDE_NCOLS]; + for (int i = 0; i < LANG_WIDE_NCOLS; i++) { + ray_t* wrapper = lang_parted_insert_col(source, + lang_parted_wide_names[i]); + TEST_ASSERT_NOT_NULL(wrapper); + TEST_ASSERT_TRUE(RAY_IS_PARTED(wrapper->type)); + TEST_ASSERT_EQ_I(RAY_PARTED_BASETYPE(wrapper->type), + lang_parted_wide_types[i]); + TEST_ASSERT_EQ_I(wrapper->len, 2); + ray_t** segs = (ray_t**)ray_data(wrapper); + historical[i] = segs[0]; + TEST_ASSERT_EQ_I(segs[0]->len, 1); + TEST_ASSERT_EQ_I(segs[1]->len, 1); + TEST_ASSERT_EQ_U(segs[0]->mmod, 1); + TEST_ASSERT_EQ_U(segs[1]->mmod, 1); + } + + /* A fully validated exact-schema zero-vector batch is pointer-identical + * and must not create the requested later partition. */ + ray_t* zero = ray_eval_str( + "(insert wide 2024.01.03 " + " (list wide_zb wide_zu8 wide_zi16 wide_zi32 wide_zf32 " + " wide_zf64 wide_zd wide_ztm wide_zts wide_zg))"); + TEST_ASSERT_NOT_NULL(zero); + TEST_ASSERT_FALSE(RAY_IS_ERR(zero)); + TEST_ASSERT_EQ_PTR(zero, source); + TEST_ASSERT_EQ_I(ray_table_nrows(zero), 2); + ray_t* zero_mc = ray_table_get_col_idx(zero, 0); + TEST_ASSERT_NOT_NULL(zero_mc); + TEST_ASSERT_EQ_I(((ray_t**)ray_data(zero_mc))[0]->len, 2); + ray_release(zero); + + /* Exact typed vectors append two rows to the mmap-backed active day. */ + ASSERT_EQ( + "(insert 'wide 2024.01.02 " + " (list wide_bv wide_u8v wide_i16v wide_i32v wide_f32v " + " wide_f64v wide_dv wide_tmv wide_tsv wide_gv))", + "'wide"); + + ray_t* live = ray_eval_str("wide"); + TEST_ASSERT_NOT_NULL(live); + TEST_ASSERT_FALSE(RAY_IS_ERR(live)); + TEST_ASSERT_EQ_I(ray_table_nrows(live), 4); + TEST_ASSERT_EQ_I(ray_table_nrows(source), 2); + ray_t* counts = lang_parted_insert_counts(live); + TEST_ASSERT_NOT_NULL(counts); + TEST_ASSERT_EQ_I(counts->len, 2); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[0], 1); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(counts))[1], 3); + + ray_t* tails[LANG_WIDE_NCOLS]; + for (int i = 0; i < LANG_WIDE_NCOLS; i++) { + ray_t* wrapper = lang_parted_insert_col(live, + lang_parted_wide_names[i]); + TEST_ASSERT_NOT_NULL(wrapper); + TEST_ASSERT_TRUE(RAY_IS_PARTED(wrapper->type)); + TEST_ASSERT_EQ_I(RAY_PARTED_BASETYPE(wrapper->type), + lang_parted_wide_types[i]); + ray_t** segs = (ray_t**)ray_data(wrapper); + TEST_ASSERT_EQ_PTR(segs[0], historical[i]); + TEST_ASSERT_EQ_U(segs[0]->mmod, 1); + TEST_ASSERT_EQ_I(segs[0]->len, 1); + TEST_ASSERT_EQ_I(segs[1]->type, lang_parted_wide_types[i]); + TEST_ASSERT_EQ_I(segs[1]->len, 3); + TEST_ASSERT_EQ_U(segs[1]->mmod, 0); + tails[i] = segs[1]; + } + + TEST_ASSERT_EQ_I(((uint8_t*)ray_data(tails[LANG_WIDE_BOOL]))[1], 1); + TEST_ASSERT_EQ_I(((uint8_t*)ray_data(tails[LANG_WIDE_BOOL]))[2], 0); + TEST_ASSERT_EQ_I(((uint8_t*)ray_data(tails[LANG_WIDE_U8]))[1], 7); + TEST_ASSERT_EQ_I(((uint8_t*)ray_data(tails[LANG_WIDE_U8]))[2], 8); + TEST_ASSERT_EQ_I(((int16_t*)ray_data(tails[LANG_WIDE_I16]))[1], -123); + TEST_ASSERT_EQ_I(((int16_t*)ray_data(tails[LANG_WIDE_I16]))[2], 456); + TEST_ASSERT_EQ_I(((int32_t*)ray_data(tails[LANG_WIDE_I32]))[1], 123456); + TEST_ASSERT_EQ_I(((int32_t*)ray_data(tails[LANG_WIDE_I32]))[2], -654321); + TEST_ASSERT_EQ_F(((float*)ray_data(tails[LANG_WIDE_F32]))[1], 1.5f, + 0.00001); + TEST_ASSERT_TRUE(ray_vec_is_null(tails[LANG_WIDE_F32], 2)); + TEST_ASSERT((tails[LANG_WIDE_F32]->attrs & RAY_ATTR_HAS_NULLS) != 0, + "F32 null metadata preserved"); + TEST_ASSERT_EQ_F(((double*)ray_data(tails[LANG_WIDE_F64]))[1], 2.5, + 0.0000001); + TEST_ASSERT_EQ_F(((double*)ray_data(tails[LANG_WIDE_F64]))[2], -3.5, + 0.0000001); + TEST_ASSERT_EQ_I(((int32_t*)ray_data(tails[LANG_WIDE_DATE]))[1], 9132); + TEST_ASSERT_EQ_I(((int32_t*)ray_data(tails[LANG_WIDE_DATE]))[2], 9133); + TEST_ASSERT_EQ_I(((int32_t*)ray_data(tails[LANG_WIDE_TIME]))[1], 3723004); + TEST_ASSERT_EQ_I(((int32_t*)ray_data(tails[LANG_WIDE_TIME]))[2], 18367008); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(tails[LANG_WIDE_TIMESTAMP]))[1], + 1111111111LL); + TEST_ASSERT_EQ_I(((int64_t*)ray_data(tails[LANG_WIDE_TIMESTAMP]))[2], + 2222222222LL); + uint8_t expected_guid[16]; + for (int i = 0; i < 16; i++) expected_guid[i] = (uint8_t)(0xA0 + i); + TEST_ASSERT_MEM_EQ(16, + (uint8_t*)ray_data(tails[LANG_WIDE_GUID]) + 16, + expected_guid); + TEST_ASSERT_TRUE(ray_vec_is_null(tails[LANG_WIDE_GUID], 2)); + TEST_ASSERT((tails[LANG_WIDE_GUID]->attrs & RAY_ATTR_HAS_NULLS) != 0, + "GUID null metadata preserved"); + + /* BOOL and U8 have no null representation. Failed symbol-target + * inserts must leave the exact live table binding untouched. */ + int64_t wide_id = ray_sym_intern("wide", 4); + ray_t* bound_before = ray_env_get(wide_id); /* borrowed */ + TEST_ASSERT_EQ_PTR(bound_before, live); + ASSERT_ER_CODE( + "(insert 'wide 2024.01.02 " + " (list 0N wide_u8v wide_i16v wide_i32v wide_f32v " + " wide_f64v wide_dv wide_tmv wide_tsv wide_gv))", + "type"); + TEST_ASSERT_EQ_PTR(ray_env_get(wide_id), bound_before); + TEST_ASSERT_EQ_I(ray_table_nrows(ray_env_get(wide_id)), 4); + ASSERT_ER_CODE( + "(insert 'wide 2024.01.02 " + " (list wide_bv 0N wide_i16v wide_i32v wide_f32v " + " wide_f64v wide_dv wide_tmv wide_tsv wide_gv))", + "type"); + TEST_ASSERT_EQ_PTR(ray_env_get(wide_id), bound_before); + TEST_ASSERT_EQ_I(ray_table_nrows(ray_env_get(wide_id)), 4); + + /* Manually model a legal ragged canonical active day. A nullable I16 + * gap is null-prefixed on same-key insertion. */ + ray_t* gap = ray_read_parted(root, "wide"); + TEST_ASSERT_NOT_NULL(gap); + TEST_ASSERT_FALSE(RAY_IS_ERR(gap)); + int64_t gap_id = ray_sym_intern("wide_gap", 8); + TEST_ASSERT_EQ_I(ray_env_bind_flat(gap_id, gap), RAY_OK); + ray_t* gap_i16 = lang_parted_insert_col(gap, "i16"); + ray_t** gap_i16_segs = (ray_t**)ray_data(gap_i16); + ray_t* removed_i16 = gap_i16_segs[1]; /* same-key data can repair the gap */ + gap_i16_segs[1] = NULL; + ray_t* gap_out = ray_eval_str( + "(insert wide_gap 2024.01.02 " + " (list wide_bv wide_u8v wide_i16v wide_i32v wide_f32v " + " wide_f64v wide_dv wide_tmv wide_tsv wide_gv))"); + gap_i16_segs[1] = removed_i16; /* transfer ref back before asserting */ + TEST_ASSERT_NOT_NULL(gap_out); + TEST_ASSERT_FALSE(RAY_IS_ERR(gap_out)); + TEST_ASSERT_EQ_I(ray_table_nrows(gap_out), 4); + ray_t* out_i16 = lang_parted_insert_col(gap_out, "i16"); + ray_t* out_i16_tail = ((ray_t**)ray_data(out_i16))[1]; + TEST_ASSERT_EQ_I(out_i16_tail->type, RAY_I16); + TEST_ASSERT_EQ_I(out_i16_tail->len, 3); + TEST_ASSERT_TRUE(ray_vec_is_null(out_i16_tail, 0)); + TEST_ASSERT_EQ_I(((int16_t*)ray_data(out_i16_tail))[1], -123); + TEST_ASSERT_EQ_I(((int16_t*)ray_data(out_i16_tail))[2], 456); + ray_release(gap_out); + + /* Missing active BOOL/U8 segments cannot be null-backfilled. */ + const char* missing_names[] = { "b", "u8" }; + for (size_t i = 0; i < 2; i++) { + ray_t* wrapper = lang_parted_insert_col(gap, missing_names[i]); + ray_t** segs = (ray_t**)ray_data(wrapper); + ray_t* removed = segs[1]; + segs[1] = NULL; + ray_t* err = ray_eval_str( + "(insert wide_gap 2024.01.02 " + " (list wide_bv wide_u8v wide_i16v wide_i32v wide_f32v " + " wide_f64v wide_dv wide_tmv wide_tsv wide_gv))"); + segs[1] = removed; + TEST_ASSERT_NOT_NULL(err); + TEST_ASSERT_TRUE(RAY_IS_ERR(err)); + TEST_ASSERT_EQ_I(ray_err_from_obj(err), RAY_ERR_TYPE); + ray_error_free(err); + TEST_ASSERT_EQ_PTR(ray_env_get(gap_id), gap); + TEST_ASSERT_EQ_I(ray_table_nrows(gap), 2); + } + + /* Any missing active physical segment blocks advancing to a later key. */ + removed_i16 = gap_i16_segs[1]; + gap_i16_segs[1] = NULL; + ray_t* advance_err = ray_eval_str( + "(insert wide_gap 2024.01.03 " + " (list wide_bv wide_u8v wide_i16v wide_i32v wide_f32v " + " wide_f64v wide_dv wide_tmv wide_tsv wide_gv))"); + /* A validated empty batch is normally a no-op, but cannot claim success + * while leaving an active gap unmaterialized. */ + ray_t* empty_gap_err = ray_eval_str( + "(insert wide_gap 2024.01.02 " + " (list wide_zb wide_zu8 wide_zi16 wide_zi32 wide_zf32 " + " wide_zf64 wide_zd wide_ztm wide_zts wide_zg))"); + gap_i16_segs[1] = removed_i16; + TEST_ASSERT_NOT_NULL(advance_err); + TEST_ASSERT_TRUE(RAY_IS_ERR(advance_err)); + TEST_ASSERT_EQ_I(ray_err_from_obj(advance_err), RAY_ERR_DOMAIN); + ray_error_free(advance_err); + TEST_ASSERT_NOT_NULL(empty_gap_err); + TEST_ASSERT_TRUE(RAY_IS_ERR(empty_gap_err)); + TEST_ASSERT_EQ_I(ray_err_from_obj(empty_gap_err), RAY_ERR_DOMAIN); + ray_error_free(empty_gap_err); + TEST_ASSERT_EQ_PTR(ray_env_get(gap_id), gap); + TEST_ASSERT_EQ_I(ray_table_nrows(gap), 2); + TEST_ASSERT_EQ_I(((ray_t**)ray_data(ray_table_get_col_idx(gap, 0)))[0]->len, + 2); + + /* A historical gap can never be repaired by tail growth: accepting it + * would return a table that segmented execution still cannot query. */ + ray_t* removed_history = gap_i16_segs[0]; + gap_i16_segs[0] = NULL; + ray_t* history_err = ray_eval_str( + "(insert wide_gap 2024.01.02 " + " (list wide_bv wide_u8v wide_i16v wide_i32v wide_f32v " + " wide_f64v wide_dv wide_tmv wide_tsv wide_gv))"); + gap_i16_segs[0] = removed_history; + TEST_ASSERT_NOT_NULL(history_err); + TEST_ASSERT_TRUE(RAY_IS_ERR(history_err)); + TEST_ASSERT_EQ_I(ray_err_from_obj(history_err), RAY_ERR_CORRUPT); + ray_error_free(history_err); + TEST_ASSERT_EQ_PTR(ray_env_get(gap_id), gap); + TEST_ASSERT_EQ_I(ray_table_nrows(gap), 2); + + ray_release(gap); + ray_release(live); + ray_release(source); + PASS(); +} + +static test_result_t test_eval_insert_parted_physical_types(void) { + char root[512]; + int n = snprintf(root, sizeof(root), + "/tmp/rayforce_lang_parted_insert_types_%ld", + (long)getpid()); + if (n <= 0 || (size_t)n >= sizeof(root)) + return (test_result_t){ TEST_FAIL, "parted fixture path overflow" }; + lang_parted_insert_rm_rf(root); + test_result_t result = test_eval_insert_parted_physical_types_impl(root); + lang_parted_insert_rm_rf(root); + return result; +} + /* ---- Test: upsert (update existing row) ---- */ static test_result_t test_eval_upsert(void) { /* Upsert by 'name key — row with name=2 exists, update it */ @@ -7101,6 +8549,11 @@ const test_entry_t lang_entries[] = { { "lang/eval/insert_typed_null", test_eval_insert_typed_null, lang_setup, lang_teardown }, { "lang/eval/insert_guid", test_eval_insert_guid, lang_setup, lang_teardown }, { "lang/eval/insert_positional_errors", test_eval_insert_positional_errors, lang_setup, lang_teardown }, + { "lang/eval/insert_parted_e2e", test_eval_insert_parted_e2e, lang_setup, lang_teardown }, + { "lang/eval/insert_parted_rollover", test_eval_insert_parted_rollover, lang_setup, lang_teardown }, + { "lang/eval/insert_parted_errors", test_eval_insert_parted_errors, lang_setup, lang_teardown }, + { "lang/eval/insert_parted_key_types", test_eval_insert_parted_key_types, lang_setup, lang_teardown }, + { "lang/eval/insert_parted_physical_types", test_eval_insert_parted_physical_types, lang_setup, lang_teardown }, { "lang/eval/upsert", test_eval_upsert, lang_setup, lang_teardown }, { "lang/eval/upsert_f64_key", test_eval_upsert_f64_key, lang_setup, lang_teardown }, { "lang/eval/upsert_str_key", test_eval_upsert_str_key, lang_setup, lang_teardown }, From bbdc4196e6e4a2d927cafa0ad372da97aed6b216 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 15 Jul 2026 11:57:38 +0200 Subject: [PATCH 03/14] fix(core): restore total-core -c semantics --- docs/docs/namespaces/sys.md | 2 +- src/app/main.c | 10 ++++----- src/core/pool.c | 37 +++++++++++++++++++++---------- src/core/pool.h | 3 +++ test/test_pool.c | 32 ++++++++++++++++++++++++++- test/test_repl.c | 43 +++++++++++++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 18 deletions(-) diff --git a/docs/docs/namespaces/sys.md b/docs/docs/namespaces/sys.md index c8b3ce7a..2e50c6ef 100644 --- a/docs/docs/namespaces/sys.md +++ b/docs/docs/namespaces/sys.md @@ -31,7 +31,7 @@ Signature: `(.sys.args)`. Returns the process's command-line arguments as a dict |---|---|---|---| | `file` | str | `-f` / positional | Script path; empty if none. | | `port` | i64 | `-p` | IPC listen port; `0` if unset. | -| `cores` | i64 | `-c` | Worker-pool size; `0` = auto. | +| `cores` | i64 | `-c` | Total execution cores, including the main thread; `0` = auto. | | `timeit` | bool | `-t` | Profiler enabled at startup. | | `querylog` | bool | `-Q` | Query-statistics logging enabled at startup. | | `interactive` | bool | `-i` | Force the REPL after a script. | diff --git a/src/app/main.c b/src/app/main.c index 86144ea9..501f84c8 100644 --- a/src/app/main.c +++ b/src/app/main.c @@ -81,7 +81,7 @@ int main(int argc, char** argv) { int interactive = 0; const char* file = NULL; uint16_t port = 0; - int n_cores = -1; /* -1 = leave pool at lazy ncpu-1 default */ + int n_cores = -1; /* total execution cores; -1 = leave pool lazy */ int timeit_init = 0; /* -t N: enable profiler at startup */ int qlog_init = 0; /* -Q N: enable query-statistics logging at startup */ const char* auth_pw = NULL; @@ -92,7 +92,7 @@ int main(int argc, char** argv) { /* Parse args. Supported flags: * -f FILE run script file * -p PORT IPC listen port - * -c N worker-pool size (0 = auto: ncpu - 1) + * -c N total execution cores, main included (0 = auto) * -t N enable timeit at startup (N != 0 turns on) * -Q N enable query-statistics logging (N != 0 turns on) * -i interactive @@ -152,7 +152,7 @@ int main(int argc, char** argv) { " [-u PW | -U PW] [-l BASE | -L BASE] [-m SIZE] [file.rfl] [-- app args]\n" " -f, --file FILE run script file (or pass as a positional arg)\n" " -p, --port PORT listen for IPC clients on PORT\n" - " -c, --cores N worker-pool size (0 = auto: ncpu - 1, default)\n" + " -c, --cores N total execution cores, main included (0 = auto)\n" " -t, --timeit N enable profiler at startup (N != 0)\n" " -i, --interactive start the REPL even after running a file\n" " -u PW set plain auth password\n" @@ -191,9 +191,9 @@ int main(int argc, char** argv) { * (file load, REPL eval, builtins). If -c wasn't given, leave the * pool to its lazy default on first use. */ if (n_cores >= 0) { - ray_err_t err = ray_pool_init((uint32_t)n_cores); + ray_err_t err = ray_pool_init_total((uint32_t)n_cores); if (err != RAY_OK) - fprintf(stderr, "warning: ray_pool_init(%d) failed (%d)\n", + fprintf(stderr, "warning: ray_pool_init_total(%d) failed (%d)\n", n_cores, (int)err); } diff --git a/src/core/pool.c b/src/core/pool.c index ab0176c8..01da0a4f 100644 --- a/src/core/pool.c +++ b/src/core/pool.c @@ -120,7 +120,8 @@ static void worker_loop(void* arg) { * ray_pool_create * -------------------------------------------------------------------------- */ -ray_err_t ray_pool_create(ray_pool_t* pool, uint32_t n_workers) { +static ray_err_t ray_pool_create_impl(ray_pool_t* pool, uint32_t n_workers, + bool auto_size) { /* conc-L7: memset zeroes all fields including the `cancelled` atomic, * which resets any cancellation state from a prior pool instance. */ memset(pool, 0, sizeof(*pool)); @@ -133,14 +134,14 @@ ray_err_t ray_pool_create(ray_pool_t* pool, uint32_t n_workers) { atomic_init(&pool->pending, 0); atomic_init(&pool->cancelled, 0); - if (n_workers == 0) { + if (auto_size) { /* Auto-size to ncpu-1. The RAYFORCE_CORES env var overrides this * default worker count — the test harness sets it (see the Makefile * `test` target) so neither the in-process runtime nor the server * children it spawns via .sys.exec each create ncpu-1 threads on a - * many-core box. An explicit -c (which passes n_workers > 0 through - * ray_pool_init) bypasses this entirely, and a non-test run with the - * var unset keeps the historical ncpu-1 default. */ + * many-core box. An explicit positive -c uses ray_pool_init_total() + * with exact sizing and bypasses this entirely; a non-test run with + * the var unset keeps the historical ncpu-1 default. */ const char* env = getenv("RAYFORCE_CORES"); if (env && *env) { long v = strtol(env, NULL, 10); @@ -221,6 +222,10 @@ ray_err_t ray_pool_create(ray_pool_t* pool, uint32_t n_workers) { return RAY_OK; } +ray_err_t ray_pool_create(ray_pool_t* pool, uint32_t n_workers) { + return ray_pool_create_impl(pool, n_workers, n_workers == 0); +} + /* -------------------------------------------------------------------------- * ray_pool_free * -------------------------------------------------------------------------- */ @@ -506,11 +511,11 @@ ray_pool_t* ray_pool_get(void) { * Public API wrappers (declared in rayforce.h) * -------------------------------------------------------------------------- */ -/* conc-L4: If ray_pool_init() is called when the pool is already initialized - * (state==2), the n_workers parameter is silently ignored and the existing - * pool configuration is preserved. This is by design — the pool is a - * singleton and reconfiguration requires ray_pool_destroy() + ray_pool_init(). */ -ray_err_t ray_pool_init(uint32_t n_workers) { +/* conc-L4: If an initializer is called when the pool is already initialized + * (state==2), its requested size is silently ignored and the existing pool + * configuration is preserved. This is by design — the pool is a singleton + * and reconfiguration requires ray_pool_destroy() followed by initialization. */ +static ray_err_t ray_pool_init_impl(uint32_t n_workers, bool auto_size) { uint32_t expected = 0; if (!atomic_compare_exchange_strong_explicit(&g_pool_init_state, &expected, 1, memory_order_acq_rel, @@ -527,7 +532,7 @@ ray_err_t ray_pool_init(uint32_t n_workers) { } return RAY_OK; /* already initialized or completed during our spin */ } - ray_err_t err = ray_pool_create(&g_pool, n_workers); + ray_err_t err = ray_pool_create_impl(&g_pool, n_workers, auto_size); if (err == RAY_OK) { atomic_store_explicit(&g_pool_init_state, 2, memory_order_release); } else { @@ -536,6 +541,16 @@ ray_err_t ray_pool_init(uint32_t n_workers) { return err; } +ray_err_t ray_pool_init(uint32_t n_workers) { + return ray_pool_init_impl(n_workers, n_workers == 0); +} + +ray_err_t ray_pool_init_total(uint32_t total_workers) { + if (total_workers == 0) + return ray_pool_init_impl(0, true); + return ray_pool_init_impl(total_workers - 1, false); +} + void ray_pool_destroy(void) { uint32_t expected = 2; if (!atomic_compare_exchange_strong_explicit(&g_pool_init_state, &expected, 3, diff --git a/src/core/pool.h b/src/core/pool.h index 41bc0608..2b71c7bf 100644 --- a/src/core/pool.h +++ b/src/core/pool.h @@ -107,6 +107,9 @@ ray_pool_t* ray_pool_get(void); /* Public pool init/destroy (moved from rayforce.h) */ ray_err_t ray_pool_init(uint32_t n_workers); +/* Initialize by total execution participants, including the main thread. + * Pass 0 to auto-detect. Used by the CLI, where -c is a total-core count. */ +ray_err_t ray_pool_init_total(uint32_t total_workers); void ray_pool_destroy(void); #endif /* RAY_POOL_H */ diff --git a/test/test_pool.c b/test/test_pool.c index 812f3693..81e40261 100644 --- a/test/test_pool.c +++ b/test/test_pool.c @@ -661,6 +661,36 @@ static test_result_t test_pool_total_workers(void) { PASS(); } +/* -------------------------------------------------------------------------- + * Test: total-worker initialization used by the CLI. + * + * The pool API normally accepts a background-worker count and reserves worker + * 0 for the calling thread. The CLI's -c contract is different: it counts all + * participants. In particular, total=1 must create an exact zero-background + * pool rather than taking ray_pool_init(0)'s auto-size path. + * -------------------------------------------------------------------------- */ + +static test_result_t test_pool_init_total(void) { + ray_pool_destroy(); + TEST_ASSERT_EQ_I(ray_pool_init_total(1), RAY_OK); + ray_pool_t* pool = ray_pool_get(); + TEST_ASSERT_NOT_NULL(pool); + TEST_ASSERT_EQ_U(pool->n_workers, 0u); + TEST_ASSERT_EQ_U(ray_pool_total_workers(pool), 1u); + + ray_pool_destroy(); + TEST_ASSERT_EQ_I(ray_pool_init_total(4), RAY_OK); + pool = ray_pool_get(); + TEST_ASSERT_NOT_NULL(pool); + TEST_ASSERT_EQ_U(pool->n_workers, 3u); + TEST_ASSERT_EQ_U(ray_pool_total_workers(pool), 4u); + + /* Restore the lazy/default configuration for later tests. */ + ray_pool_destroy(); + TEST_ASSERT_EQ_I(ray_pool_init(0), RAY_OK); + PASS(); +} + /* -------------------------------------------------------------------------- * Test: ray_pool_free(NULL) is a no-op (covers the early-return guard). * -------------------------------------------------------------------------- */ @@ -1217,6 +1247,7 @@ const test_entry_t pool_entries[] = { { "pool/dispatch_cancelled", test_dispatch_cancelled, NULL, NULL }, { "pool/zero_workers", test_pool_zero_workers, NULL, NULL }, { "pool/total_workers", test_pool_total_workers, NULL, NULL }, + { "pool/init_total", test_pool_init_total, NULL, NULL }, { "pool/free_null", test_pool_free_null, NULL, NULL }, { "pool/init_idempotent", test_pool_init_idempotent, NULL, NULL }, { "pool/destroy_reinit", test_pool_destroy_and_reinit, NULL, NULL }, @@ -1235,4 +1266,3 @@ const test_entry_t pool_entries[] = { { NULL, NULL, NULL, NULL }, }; - diff --git a/test/test_repl.c b/test/test_repl.c index 98980b02..ee5cdeb3 100644 --- a/test/test_repl.c +++ b/test/test_repl.c @@ -2905,9 +2905,52 @@ static test_result_t test_repl_pty_ctrl_c_during_lazy_materialize(void) { PASS(); } +#if defined(__linux__) +/* Run the real launcher in piped REPL mode and count its live task threads. + * /proc/self/task observes the exact process-wide total, so this catches both + * the ordinary off-by-one and the -c 1 collision with the pool's auto mode. */ +static int run_cli_task_count(unsigned cores, long* out_count) { + char command[256]; + snprintf(command, sizeof(command), + "printf '(count (.fs.list \"/proc/self/task\"))\\n'" + " | ./rayforce -c %u -i", cores); + + FILE* pipe = popen(command, "r"); + if (!pipe) return -1; + + char line[128]; + bool have_line = fgets(line, sizeof(line), pipe) != NULL; + int status = pclose(pipe); + if (!have_line || status == -1 || !WIFEXITED(status) || + WEXITSTATUS(status) != 0) + return -2; + + char* end = NULL; + long count = strtol(line, &end, 10); + if (end == line) return -3; + *out_count = count; + return 0; +} + +static test_result_t test_repl_cli_cores_are_total(void) { + long count = 0; + int rc = run_cli_task_count(1, &count); + TEST_ASSERT_FMT(rc == 0, "-c 1 launcher probe failed: %d", rc); + TEST_ASSERT_FMT(count == 1, "-c 1 created %ld total threads", count); + + rc = run_cli_task_count(3, &count); + TEST_ASSERT_FMT(rc == 0, "-c 3 launcher probe failed: %d", rc); + TEST_ASSERT_FMT(count == 3, "-c 3 created %ld total threads", count); + PASS(); +} +#endif + /* ─── Suite definition ───────────────────────────────────────────── */ const test_entry_t repl_entries[] = { +#if defined(__linux__) + { "repl/cli/cores_are_total", test_repl_cli_cores_are_total, NULL, NULL }, +#endif /* file-batch entrypoint — ray_repl_run_file */ { "repl/run_file/happy", test_repl_run_file_happy, repl_setup, repl_teardown }, { "repl/run_file/multi_form", test_repl_run_file_multi_form, repl_setup, repl_teardown }, From 9961388c663097860771181174155b9b8bc9a3c8 Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 16 Jul 2026 12:06:49 +0200 Subject: [PATCH 04/14] fix(parse) Fix nonstring if not defined --- src/lang/parse.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lang/parse.c b/src/lang/parse.c index 701badbe..163bf6e3 100644 --- a/src/lang/parse.c +++ b/src/lang/parse.c @@ -53,7 +53,13 @@ #define PA_MINUS 14 #define PA_SEMI 15 /* ; comment */ -static const char _PA[128] = +#if defined(__has_attribute) && __has_attribute(nonstring) +#define RAY_NONSTRING __attribute__((nonstring)) +#else +#define RAY_NONSTRING +#endif + +static const char _PA[128] RAY_NONSTRING = /* NUL \t \n */ "\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x0c\x00\x00\x0c\x00\x00" /* */ From 49d85247614d815e552a820dfe0be71e91198d48 Mon Sep 17 00:00:00 2001 From: Evgen Date: Sat, 18 Jul 2026 18:14:59 +0200 Subject: [PATCH 05/14] fix(store): surface FlushFileBuffers failure in journal SYNC mode (#335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In RAY_JOURNAL_SYNC mode ray_journal_write_bytes checks fsync's return on POSIX and fails the write with RAY_ERR_IO, but the Windows branch ignored FlushFileBuffers' return. A failed flush there was silently swallowed, so SYNC mode reported success while the data may not have reached disk — dropping the durability guarantee the mode exists to provide. Check FlushFileBuffers (0 = failure) and return RAY_ERR_IO, mirroring the POSIX path. Windows-only branch (not built on the Linux/macOS CI matrix), so it is verified by inspection against the adjacent fsync check; the failure path is not unit-testable, like the existing POSIX one. Co-authored-by: Evgen Byelozorov --- src/store/journal.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/store/journal.c b/src/store/journal.c index f7f82aa2..53ea0ad9 100644 --- a/src/store/journal.c +++ b/src/store/journal.c @@ -173,7 +173,11 @@ ray_err_t ray_journal_write_bytes(const ray_ipc_header_t* hdr, #ifndef RAY_OS_WINDOWS if (fsync(fileno(g_journal.fp)) != 0) return RAY_ERR_IO; #else - FlushFileBuffers((HANDLE)_get_osfhandle(_fileno(g_journal.fp))); + /* FlushFileBuffers returns 0 (FALSE) on failure — surface it as an I/O + * error like the fsync path above, so SYNC mode's durability guarantee + * isn't silently dropped on Windows. */ + if (!FlushFileBuffers((HANDLE)_get_osfhandle(_fileno(g_journal.fp)))) + return RAY_ERR_IO; #endif } return RAY_OK; From cb1c6cd95f39ff802ed24a21608baaed3b17e43a Mon Sep 17 00:00:00 2001 From: Evgen Date: Sat, 18 Jul 2026 23:59:00 +0200 Subject: [PATCH 06/14] fix(hnsw): reject build dims whose vector count overflows size_t (#333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ray_hnsw_build sized the copied vector block as n_nodes * dim * sizeof(float) with no overflow check. Dimensions whose product wraps size_t under-allocate the copy while the memcpy — and every later distance read (vectors + id*dim) — run past the buffer. Guard the product before any allocation, mirroring the per-layer neighbor guard in the loader, and reject overflowing dimensions. This hardens the public C API boundary; the in-tree (hnsw-build ...) path sizes vectors from an in-memory list and cannot reach the overflow, so it is defense-in-depth. Add a regression test driving an overflowing n_nodes/dim pair; with the guard removed it faults under ASan (stack-buffer-overflow at the copy). Co-authored-by: Evgen Byelozorov Co-authored-by: Anton Kundenko --- src/store/hnsw.c | 7 +++++++ test/test_embedding.c | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/store/hnsw.c b/src/store/hnsw.c index 16a5f581..fb53b4b4 100644 --- a/src/store/hnsw.c +++ b/src/store/hnsw.c @@ -464,6 +464,13 @@ ray_hnsw_t* ray_hnsw_build(const float* vectors, int64_t n_nodes, int32_t dim, ray_hnsw_metric_t metric, int32_t M, int32_t ef_construction) { if (!vectors || n_nodes <= 0 || dim <= 0) return NULL; + /* Overflow guard: n_nodes * dim * sizeof(float) sizes the copied vector + * block and also indexes every distance computation (vectors + id*dim). + * If it wraps size_t the copy under-allocates and the memcpy below — and + * later reads during construction — run past the buffer. Reject before + * any allocation. Mirrors the per-layer neighbor guard in the loader. + * n_nodes and dim are > 0 here, so the divide is safe. */ + if ((uint64_t)n_nodes > SIZE_MAX / sizeof(float) / (uint64_t)dim) return NULL; if (M <= 0) M = HNSW_DEFAULT_M; if (ef_construction <= 0) ef_construction = HNSW_DEFAULT_EF_C; if (metric < RAY_HNSW_COSINE || metric > RAY_HNSW_IP) metric = RAY_HNSW_COSINE; diff --git a/test/test_embedding.c b/test/test_embedding.c index ef8aca58..6dea9b10 100644 --- a/test/test_embedding.c +++ b/test/test_embedding.c @@ -969,6 +969,20 @@ static test_result_t test_hnsw_mmap_load(void) { PASS(); } +/* Regression: ray_hnsw_build must reject dimensions whose + * n_nodes * dim * sizeof(float) overflows size_t, before it copies the source + * vectors. Values are chosen so the byte count wraps to 16: without the guard + * the copy under-allocates and memcpy over-reads the 1-element source (ASan + * traps it); with the guard the call returns NULL and the source is never + * touched. (2^62 + 2) * 2 * sizeof(float) == 2^65 + 16 == 16 (mod 2^64). */ +static test_result_t test_hnsw_build_overflow_rejected(void) { + float dummy[1] = { 0.0f }; + ray_hnsw_t* idx = ray_hnsw_build(dummy, ((int64_t)1 << 62) + 2, 2, + RAY_HNSW_L2, 4, 50); + TEST_ASSERT_NULL(idx); + PASS(); +} + /* Trigger the maxheap_sift_down / results-replacement path in hnsw_search_layer. * * The replacement branch (lines 342-344) fires when: @@ -1964,6 +1978,7 @@ const test_entry_t embedding_entries[] = { { "embedding/hnsw_dim_accessor", test_hnsw_dim_accessor, emb_setup, emb_teardown }, { "embedding/hnsw_search_filter_null_accept", test_hnsw_search_filter_null_accept, emb_setup, emb_teardown }, { "embedding/hnsw_mmap_load", test_hnsw_mmap_load, emb_setup, emb_teardown }, + { "embedding/hnsw_build_overflow_rejected", test_hnsw_build_overflow_rejected, emb_setup, emb_teardown }, { "embedding/hnsw_search_sift_down", test_hnsw_search_sift_down, emb_setup, emb_teardown }, /* rerank coverage (S7) */ From 7475949abd1525a8e9eb6f2090807b19d82dfb31 Mon Sep 17 00:00:00 2001 From: Evgen Date: Sun, 19 Jul 2026 00:40:05 +0200 Subject: [PATCH 07/14] fix(store): read full link sidecar to avoid wrong-symbol truncation (#334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit try_load_link_sidecar read the target table's sym name into a fixed 256-byte buffer (fread of 255 bytes). A name longer than 255 bytes was silently truncated, so ray_sym_intern interned a DIFFERENT symbol and the loaded column linked to the wrong table — silent data corruption on a save/load round-trip. The writer already emits the full, untruncated name. Read the whole sidecar into a buffer sized to the file (capped at 1 MiB to bound a corrupt/oversized file), and reject a short read (fread returning fewer bytes than the file size — an I/O error or a race-truncated sidecar) so a partial name can't be interned as a different symbol either. Add a regression test that links through a 300-byte target name and asserts the loaded link_target matches; it fails without the fix. Co-authored-by: Evgen Byelozorov Co-authored-by: Anton Kundenko --- src/store/col.c | 20 +++++++++++++++++--- test/test_link.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/store/col.c b/src/store/col.c index b6af7d00..2d1d04d1 100644 --- a/src/store/col.c +++ b/src/store/col.c @@ -580,14 +580,28 @@ static void try_load_link_sidecar(ray_t* vec, const char* path) { memcpy(link_path + plen, ".link", 6); FILE* lf = fopen(link_path, "rb"); if (!lf) return; - char buf[256]; - size_t n = fread(buf, 1, sizeof(buf) - 1, lf); + /* Read the whole sidecar. The target sym name can be longer than any fixed + * buffer, and a truncated read would intern a DIFFERENT symbol — silently + * linking the column to the wrong table. Size to the file, capped so a + * corrupt/oversized sidecar can't force a huge allocation. */ + if (fseek(lf, 0, SEEK_END) != 0) { fclose(lf); return; } + long fsz = ftell(lf); + if (fsz <= 0 || fsz > (1L << 20)) { fclose(lf); return; } + rewind(lf); + char* buf = (char*)ray_alloc_raw((size_t)fsz); + if (!buf) { fclose(lf); return; } + size_t n = fread(buf, 1, (size_t)fsz, lf); fclose(lf); + /* Reject a short read (I/O error, or the sidecar shrank after the size + * check above): interning a partial name would link the WRONG symbol — + * the very failure this reader guards against. */ + if (n != (size_t)fsz) { ray_free_raw(buf); return; } while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r' || buf[n-1] == ' ' || buf[n-1] == '\t' || buf[n-1] == '\0')) n--; - if (n == 0) return; + if (n == 0) { ray_free_raw(buf); return; } int64_t target_sym = ray_sym_intern(buf, n); + ray_free_raw(buf); if (target_sym < 0) return; vec->link_target = target_sym; vec->attrs |= RAY_ATTR_HAS_LINK; diff --git a/test/test_link.c b/test/test_link.c index de64821f..96552846 100644 --- a/test/test_link.c +++ b/test/test_link.c @@ -350,6 +350,47 @@ static test_result_t test_link_persistence_roundtrip(void) { PASS(); } +/* Regression: a link sidecar whose target sym name is longer than the old + * 256-byte read buffer must round-trip intact. The buggy reader truncated to + * 255 bytes and interned a DIFFERENT symbol, silently linking the column to the + * wrong table. Use a 300-byte name and assert the loaded link_target still + * resolves to the original symbol. */ +static test_result_t test_link_persistence_long_target_name(void) { + char longname[300]; + memset(longname, 'a', sizeof(longname)); /* 300 bytes, > 255 */ + int64_t long_sym = ray_sym_intern(longname, sizeof(longname)); + TEST_ASSERT_TRUE(long_sym >= 0); + + ray_t* target = build_target_table("custs"); + ray_env_set(long_sym, target); + ray_release(target); + + int64_t rids[] = { 0, 1, 2 }; + ray_t* w = make_i64_vec(rids, 3); + TEST_ASSERT_FALSE(RAY_IS_ERR(ray_link_attach(&w, long_sym))); + TEST_ASSERT_EQ_I(w->link_target, long_sym); + + char path[] = "/tmp/link_long_name_test_XXXXXX"; + int fd = mkstemp(path); + TEST_ASSERT_TRUE(fd >= 0); + close(fd); + TEST_ASSERT_EQ_I(ray_col_save(w, path), RAY_OK); + + ray_t* loaded = ray_col_load(path); + TEST_ASSERT_FALSE(RAY_IS_ERR(loaded)); + TEST_ASSERT_TRUE(loaded->attrs & RAY_ATTR_HAS_LINK); + /* Fails if the reader truncated the sidecar to 255 bytes (wrong symbol). */ + TEST_ASSERT_EQ_I(loaded->link_target, long_sym); + + char link_path[512]; + snprintf(link_path, sizeof link_path, "%s.link", path); + unlink(path); + unlink(link_path); + ray_release(loaded); + ray_release(w); + PASS(); +} + /* ─── Sidecar must be picked up by ray_col_mmap too (splay-mmap path) ── */ static test_result_t test_link_mmap_loads_sidecar(void) { @@ -1210,6 +1251,7 @@ const test_entry_t link_entries[] = { { "link/deref_null_propagation", test_link_deref_null_propagation, link_setup, link_teardown }, { "link/deref_oob_yields_null", test_link_deref_oob_yields_null, link_setup, link_teardown }, { "link/persistence_roundtrip", test_link_persistence_roundtrip, link_setup, link_teardown }, + { "link/persistence_long_target_name", test_link_persistence_long_target_name, link_setup, link_teardown }, { "link/coexists_with_index", test_link_coexists_with_index, link_setup, link_teardown }, { "link/mmap_loads_sidecar", test_link_mmap_loads_sidecar, link_setup, link_teardown }, { "link/deref_no_target_leak", test_link_deref_no_target_leak, link_setup, link_teardown }, From ebadd07b0eeb7b627a8b07019fe5702f0dcc3fe6 Mon Sep 17 00:00:00 2001 From: Evgen Belozerov Date: Tue, 21 Jul 2026 17:58:33 +0200 Subject: [PATCH 08/14] fix(hnsw): reject index files whose vector count overflows size_t (#332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hnsw_load_impl read n_nodes and dim straight from the file header and sized the vectors allocation as n_nodes * dim * sizeof(float) with no overflow check. A crafted header could make that product wrap size_t, so ray_sys_alloc under-allocated the buffer while the following fread still read the full (large) element count and wrote past the allocation — a heap-overflow write driven by an untrusted index file. Factor the check into ray_hnsw_vec_size_valid(n_nodes, dim) and reject the header before any allocation, mirroring the per-layer neighbor guard. Add a unit test that drives the helper directly (ordinary dims, non-positive dims, an overflowing pair, and the exact size_t boundary). It is tested at the helper rather than through ray_hnsw_load because an overflow-patched header is refused earlier — the huge node-level read fails first — so a full-load test could not distinguish the guard. Co-authored-by: Evgen Byelozorov --- src/store/hnsw.c | 16 ++++++++++++++++ src/store/hnsw.h | 5 +++++ test/test_embedding.c | 27 +++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/store/hnsw.c b/src/store/hnsw.c index fb53b4b4..339119a1 100644 --- a/src/store/hnsw.c +++ b/src/store/hnsw.c @@ -895,6 +895,18 @@ ray_err_t ray_hnsw_save(const ray_hnsw_t* idx, const char* dir) { return RAY_OK; } +/* Whether the vectors block — n_nodes * dim * sizeof(float) — fits in size_t. + * Split out of hnsw_load_impl so the overflow guard can be unit-tested + * directly. n_nodes and dim come from an untrusted on-disk header; if the + * product wraps size_t the allocation under-sizes the buffer that the + * following fread then overruns (a heap-overflow write from a crafted file), + * so the loader must reject such a header before allocating. Non-positive + * dims are rejected too. Mirrors the per-layer neighbor guard. */ +bool ray_hnsw_vec_size_valid(int64_t n_nodes, int32_t dim) { + if (n_nodes <= 0 || dim <= 0) return false; + return (uint64_t)n_nodes <= SIZE_MAX / sizeof(float) / (uint64_t)dim; +} + static ray_hnsw_t* hnsw_load_impl(const char* dir, bool use_mmap) { if (!dir) return NULL; (void)use_mmap; /* mmap optimization deferred — both paths read into memory */ @@ -915,6 +927,10 @@ static ray_hnsw_t* hnsw_load_impl(const char* dir, bool use_mmap) { hdr.M <= 0 || hdr.M_max0 <= 0 || hdr.entry_point < 0 || hdr.entry_point >= hdr.n_nodes) return NULL; + /* Reject a header whose vectors block would overflow size_t, before any + * allocation (see ray_hnsw_vec_size_valid). */ + if (!ray_hnsw_vec_size_valid(hdr.n_nodes, hdr.dim)) return NULL; + ray_hnsw_t* idx = (ray_hnsw_t*)ray_sys_alloc(sizeof(ray_hnsw_t)); if (!idx) return NULL; memset(idx, 0, sizeof(ray_hnsw_t)); diff --git a/src/store/hnsw.h b/src/store/hnsw.h index 055a3c7e..5e1214c5 100644 --- a/src/store/hnsw.h +++ b/src/store/hnsw.h @@ -130,4 +130,9 @@ ray_err_t ray_hnsw_save(const ray_hnsw_t* idx, const char* dir); ray_hnsw_t* ray_hnsw_load(const char* dir); ray_hnsw_t* ray_hnsw_mmap(const char* dir); +/* True iff n_nodes * dim * sizeof(float) (the vectors block) fits in size_t, + * with n_nodes and dim > 0. The loader uses it to reject a crafted header + * that would overflow the vectors allocation; exposed for direct unit test. */ +bool ray_hnsw_vec_size_valid(int64_t n_nodes, int32_t dim); + #endif /* RAY_HNSW_H */ diff --git a/test/test_embedding.c b/test/test_embedding.c index 6dea9b10..6174116f 100644 --- a/test/test_embedding.c +++ b/test/test_embedding.c @@ -38,6 +38,7 @@ #include "store/hnsw.h" #include #include +#include #include #include @@ -983,6 +984,31 @@ static test_result_t test_hnsw_build_overflow_rejected(void) { PASS(); } +/* Directly exercises the vectors-block overflow guard that hnsw_load_impl + * applies to an untrusted header (factored into ray_hnsw_vec_size_valid). A + * crafted header can make n_nodes * dim * sizeof(float) wrap size_t, under- + * allocating the vectors buffer that the following fread overruns; the guard + * must reject it. Unit-tested at this layer rather than through ray_hnsw_load + * because a header patched to overflow is refused earlier — the huge + * node-level read fails first — so a full-load test cannot distinguish this + * path (with or without the guard the load returns NULL). */ +static test_result_t test_hnsw_vec_size_valid_guard(void) { + /* Ordinary dimensions fit. */ + TEST_ASSERT_TRUE(ray_hnsw_vec_size_valid(100000, 1536)); + /* Non-positive dims are rejected. */ + TEST_ASSERT_FALSE(ray_hnsw_vec_size_valid(0, 4)); + TEST_ASSERT_FALSE(ray_hnsw_vec_size_valid(4, 0)); + TEST_ASSERT_FALSE(ray_hnsw_vec_size_valid(-1, 4)); + /* n_nodes * dim * sizeof(float) overflows size_t → rejected. */ + TEST_ASSERT_FALSE(ray_hnsw_vec_size_valid((int64_t)1 << 40, 1 << 25)); + /* Boundary: the largest element count whose * sizeof(float) still fits, + * then one dim past it. */ + int64_t max_elems = (int64_t)(SIZE_MAX / sizeof(float)); + TEST_ASSERT_TRUE(ray_hnsw_vec_size_valid(max_elems, 1)); + TEST_ASSERT_FALSE(ray_hnsw_vec_size_valid(max_elems, 2)); + PASS(); +} + /* Trigger the maxheap_sift_down / results-replacement path in hnsw_search_layer. * * The replacement branch (lines 342-344) fires when: @@ -1979,6 +2005,7 @@ const test_entry_t embedding_entries[] = { { "embedding/hnsw_search_filter_null_accept", test_hnsw_search_filter_null_accept, emb_setup, emb_teardown }, { "embedding/hnsw_mmap_load", test_hnsw_mmap_load, emb_setup, emb_teardown }, { "embedding/hnsw_build_overflow_rejected", test_hnsw_build_overflow_rejected, emb_setup, emb_teardown }, + { "embedding/hnsw_vec_size_valid_guard", test_hnsw_vec_size_valid_guard, emb_setup, emb_teardown }, { "embedding/hnsw_search_sift_down", test_hnsw_search_sift_down, emb_setup, emb_teardown }, /* rerank coverage (S7) */ From fa39d9d76d36af6ad2e31a90d9c6ee74482f5595 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 22 Jul 2026 11:39:33 +0200 Subject: [PATCH 09/14] fix(docs): remediate F-0001 F-0005 F-0007 Escalate F-0002, F-0003, F-0004, and F-0006 into CF-0001 through CF-0004 after the required corpus census. --- audit/AUDIT.md | 179 ++++++++++++++ audit/LEDGER.md | 13 + audit/XCHECK.md | 228 ++++++++++++++++++ ...1-public-error-cleanup-uses-ray-release.md | 69 ++++++ ...F-0002-ray-eval-str-null-contract-stale.md | 64 +++++ ...CF-0003-null-encoding-comments-conflict.md | 92 +++++++ ...time-api-redeclared-after-public-header.md | 86 +++++++ ...core-lifecycle-example-does-not-compile.md | 103 ++++++++ ...rror-objects-released-with-no-op-helper.md | 83 +++++++ ...l-return-contract-contradicts-invariant.md | 62 +++++ ...ll-sentinel-contract-contradicts-itself.md | 75 ++++++ ...time-atom-documentation-uses-wrong-unit.md | 80 ++++++ ...c-api-test-redeclares-runtime-functions.md | 79 ++++++ ...ch-comment-conflicts-with-api-reference.md | 77 ++++++ audit/passes/P-01-api-doc-consistency.md | 70 ++++++ audit/plans/RP-0001.md | 82 +++++++ audit/templates/class-finding.md | 53 ++++ audit/templates/finding.md | 46 ++++ audit/templates/pass.md | 28 +++ audit/templates/plan.md | 36 +++ docs/docs/c-api/core.md | 11 +- test/test_public_api.c | 20 +- 22 files changed, 1629 insertions(+), 7 deletions(-) create mode 100644 audit/AUDIT.md create mode 100644 audit/LEDGER.md create mode 100644 audit/XCHECK.md create mode 100644 audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md create mode 100644 audit/findings/CF-0002-ray-eval-str-null-contract-stale.md create mode 100644 audit/findings/CF-0003-null-encoding-comments-conflict.md create mode 100644 audit/findings/CF-0004-runtime-api-redeclared-after-public-header.md create mode 100644 audit/findings/F-0001-core-lifecycle-example-does-not-compile.md create mode 100644 audit/findings/F-0002-error-objects-released-with-no-op-helper.md create mode 100644 audit/findings/F-0003-eval-null-return-contract-contradicts-invariant.md create mode 100644 audit/findings/F-0004-null-sentinel-contract-contradicts-itself.md create mode 100644 audit/findings/F-0005-time-atom-documentation-uses-wrong-unit.md create mode 100644 audit/findings/F-0006-public-api-test-redeclares-runtime-functions.md create mode 100644 audit/findings/F-0007-date-epoch-comment-conflicts-with-api-reference.md create mode 100644 audit/passes/P-01-api-doc-consistency.md create mode 100644 audit/plans/RP-0001.md create mode 100644 audit/templates/class-finding.md create mode 100644 audit/templates/finding.md create mode 100644 audit/templates/pass.md create mode 100644 audit/templates/plan.md diff --git a/audit/AUDIT.md b/audit/AUDIT.md new file mode 100644 index 00000000..7b770147 --- /dev/null +++ b/audit/AUDIT.md @@ -0,0 +1,179 @@ +# AUDIT — Rayforce (C engine, Rayfall runtime, tests, and documentation) + +Operator language: English. + +## 0. Scope and batching rules + +The audit covers authored, tracked project material: `include/`, `src/`, `test/`, +`fuzz/`, `examples/`, `bench/`, `docs/`, `website/`, `overrides/`, `scripts/`, +`packaging/`, `.github/`, and the root build/governance files. Each tracked file +is an addressable unit. A heading or enclosing-symbol range is an addressable +sub-unit when a Markdown or C file is too large for one attentive pass. + +Queue globs are resolved with `git ls-files` at the beginning of a pass; the +resolved file list and any symbol/section split must be recorded in the pass +report. Brace sets such as `{arena,cow}` are compact notation: expand every +listed member into its own pathspec. Related implementation/header pairs and +small tests may be batched. +Large units such as `src/ops/query.c`, `src/ops/group.c`, `test/test_exec.c`, and +`test/test_lang.c` must be split by enclosing symbols if the charter cannot be +completed attentively. Any unfinished range becomes a new queued pass under +XCHECK.md §4 rule 4. + +Excluded: `.git/`, `.worktrees/`, `.agents/`, `.codex/`, `audit/` itself, +untracked/generated objects and binaries (`*.o`, `*.d`, `*.a`, `rayforce*`), +coverage/fuzz corpora, release output, caches, and vendored font/image binary +content. Generated/static site output is checked only for contracts that the +repository intentionally maintains (redirects, navigation, and brand shell), +not for minified or third-party asset internals. + +Inventory baseline (2026-07-22): 169 tracked C/header files under `src/`, one +770-line public header, 72 C tests plus four test headers, 432 Rayfall tests, +98 authored files under `docs/docs/`, eight fuzz source/policy files, and ten +GitHub policy/workflow files. Counts are planning estimates, not pass evidence. + +## 1. Norms catalog + +| id | norm source | scope | +|---|---|---| +| N01 | `include/rayforce.h` | Public types, ownership/lifetime, error, vector, table, progress, and API contracts. | +| N02 | `src/**/*.h`, especially `src/ops/ops.h`, `src/core/runtime.h`, `src/mem/*.h`, and `src/store/*.h` | Internal module interfaces and invariants; use only the header relevant to the audited implementation. | +| N03 | `docs/docs/c-api/*.md`, `docs/docs/architecture/*.md`, `docs/docs/language/*.md`, `docs/docs/reference/all-functions.md`, `docs/docs/storage/*.md`, and `docs/docs/guides/{errors,memory}.md` | Published behavior, architecture, language, storage/wire, error/null, and lifecycle contracts. | +| N04 | `README.md` | Product capabilities, supported workflows, project structure, examples, and performance/architecture promises. | +| N05 | `test/*.c`, `test/rfl/**/*.rfl`, and `test/test.h` | Executable behavior and regression contracts. A test is a norm only for behavior it asserts explicitly. | +| N06 | `CONTRIBUTING.md` and `Makefile` | C17, zero-dependency, warnings-as-errors, sanitizer, TSan, fuzz, test-discovery, and build-flavor policy. | +| N07 | `.clang-tidy` and `.tsan-suppressions` | Correctness-only static-analysis policy and the requirement that race suppressions be justified. | +| N08 | `fuzz/README.md`, fuzz drivers, seed policy, and `Makefile` fuzz targets | Untrusted-input surfaces, sandbox restrictions, fuzz coverage, and crash-regression procedure. | +| N09 | `RELEASE.md`, `packaging/*`, `.github/workflows/release.yml`, and `.github/release-notes.sh` | Version, release, artifact, packaging, and supported-platform contracts. | +| N10 | `mkdocs.yml`, `docs/index.md`, and the tracked redirect/static shell files | Documentation navigation, published paths, exclusions, redirects, and site structure. | +| N11 | `.github/workflows/{ci,nightly,version-guard,retarget-prs,pages,rayforce-audit}.yml` | Automated branch, CI, stability, documentation, and audit gates. | +| N12 | ISO C17 and the platform APIs selected by `Makefile`/`src/core/platform.h` | Language-level undefined behavior, integer/pointer rules, atomics, and declared POSIX/Windows portability. Use an exact clause or platform API contract in findings. | +| N13 | Internal consistency | For contradictions not governed elsewhere, quote both conflicting passages as the norm and evidence. | + +## 2. Dimensions + +| key | what it catches | norm source | +|---|---|---| +| memory-lifetime | leaks, double release/free, use-after-free, invalid COW mutation, arena/heap ownership errors, overflow and out-of-bounds access | N01, relevant N02 header, N03 memory docs, N06 sanitizer policy, N12 | +| concurrency-portability | data races, broken atomics, unsafe cross-thread ownership, backend divergence, unsupported platform assumptions | N02, N03 architecture docs, N06, N07, N11, N12 | +| language-semantics | parser/compiler/evaluator/builtin behavior inconsistent with syntax, null/error rules, function reference, or executable tests | N03, N05, N13 | +| query-correctness | optimizer rewrites or execution paths that change results, mishandle nulls/types, or violate DAG/parallel execution contracts | N02, N03 pipeline/DAG docs, N05, N13 | +| storage-integrity | corrupt or incompatible serialization, journal/AOF replay errors, unsafe file handling, symbol-domain loss, persistence/IPC contract drift | N01, N02, N03 storage/IPC docs, N05, N08 | +| api-doc-consistency | declarations, ownership, errors, examples, and registered builtins that disagree across public headers, docs, implementation, or tests | N01, N02, N03, N04, N05, N13 | +| resilience-security | untrusted parser/decoder/input failures, sandbox escapes, restricted-mode gaps, crash/DoS paths, missing fuzz regression coverage | N06, N08, N11, N12 | +| build-release | debug/release/hardened semantic drift, missing source coverage, non-reproducible versioning, broken CI/release/packaging/platform promises | N04, N06, N09, N11, N12 | +| test-adequacy | normative behavior with no meaningful assertion, tests that cannot detect the promised failure, or suites omitted from discovery/gates | N03, N05, N06, N08, N11 | +| docs-structure | broken navigation/redirects, orphaned or duplicated reference material, stale project structure, or internal contradictions | N04, N10, N13 | +| performance-contract | optimizations that violate result semantics or documented resource/complexity claims; benchmark scenarios that do not measure their stated path | N03 architecture docs, N04, `bench/**`, N13 | + +Stylistic preferences are not an audit dimension: `.clang-tidy` explicitly +turns style churn off. A style-only observation has no project norm and is at +most `info` under XCHECK.md §6. + +## 3. Unit map + +| unit family | tracked units / size estimate | audit use | +|---|---:|---| +| Root contracts | `README.md`, `CONTRIBUTING.md`, `RELEASE.md`, `Makefile`, `.clang-tidy`, `.tsan-suppressions`, `mkdocs.yml` (7 files, ~1.1k lines plus 407-line Makefile) | Product, build, analysis, release, and documentation norms. | +| Public API | `include/rayforce.h` (1 file, 770 lines) | Public ABI/API, ownership, errors, and types. | +| Application | `src/app/*` (5 files, ~4.3k lines) | CLI lifecycle, REPL, terminal, formatting. | +| Core/platform | `src/core/*` (34 files, ~7.4k lines) | Runtime, types, pool, progress, IPC/socket, platform backends, diagnostics. | +| I/O | `src/io/*` (2 files, ~3.4k lines) | CSV parsing/writing and parallel I/O. | +| Language | `src/lang/*` (15 files, ~9.5k lines) | Parser, compiler, evaluator/VM, environments, special commands. | +| Memory | `src/mem/*` (8 files, ~3.6k lines) | Arena, buddy heap, system allocation, COW/refcounts. | +| Operations | `src/ops/*` (67 files, ~95.3k lines) | DAGs, optimizer, execution, relational/graph/vector operations and builtins. | +| Storage | `src/store/*` (20 files, ~8.0k lines) | AOF/journal, serde, column/partition stores, CSR, HNSW, metadata. | +| Tables/symbols | `src/table/*` (8 files, ~3.8k lines) | Tables, dictionaries, symbol domains/interning. | +| Values/vectors | `src/vec/*` (10 files, ~2.6k lines) | Atoms, vectors, lists, strings, selections, embedding contract. | +| C test harness | `test/*.c`, `test/*.h` (76 files, ~125k lines) | Executable contracts and adequacy checks; the largest files require symbol splits. | +| Rayfall tests | `test/rfl/**/*.rfl` (432 small files, ~72.9k lines in 37 subject directories) | Language/output regression contracts; batch by subject directory. | +| Fuzzing | `fuzz/*.c`, `fuzz/common.h`, `fuzz/README.md`, committed dictionaries/seeds (43 tracked units; drivers/policy ~425 lines) | Parser, decoder, evaluator, CSV, and journal adversarial coverage. | +| Examples | `examples/*.c`, `examples/rfl/**/*.rfl` (25 files, ~1.3k lines) | User-facing API and language behavior. | +| Benchmarks | `bench/**` (14 tracked files, ~4.6k lines) | Performance claims and benchmark validity. | +| Documentation | `docs/docs/**/*.md`, docs root content/config support, `docs/llms.txt` (109 authored text/code units, ~19.1k lines; binary assets separate) | Published contracts, structure, links, and reference completeness. | +| Site shell | `overrides/**`, authored `website/*.{html,css,js}`, redirect stubs (about 9 principal authored units, ~2.7k lines) | Navigation, redirects, and brand shell consistency. | +| Automation/release | `.github/**` (10 files, ~883 lines), `scripts/**` (5 files, ~444 lines), `packaging/**` (2 files), root release scripts | CI/nightly/release/security gates and packaging. | + +## 4. Pass queue + +All passes stop when their resolved scope is exhausted, 15 findings are filed, +or context is exhausted. In the latter two cases the Auditor records explicit +coverage and queues the unresolved files/symbol ranges as a new pass. + +- [x] P-01 — api-doc-consistency × public lifecycle: compare `include/rayforce.h`, `docs/docs/c-api/core.md`, and `test/test_public_api.c` for declarations, ownership, error, and lifecycle contracts. +- [ ] P-02 — memory-lifetime × arena/COW: audit `src/mem/{arena,cow}.{c,h}` in implementation/header pairs; split into two passes if needed. +- [ ] P-03 — memory-lifetime × heap/system allocator: audit `src/mem/{heap,sys}.{c,h}` against N01/N02/N03/N06/N12. +- [ ] P-04 — memory-lifetime × atoms/vectors: audit `src/vec/{atom,vec}.{c,h}` against public type, allocation, null, and refcount contracts. +- [ ] P-05 — memory-lifetime × list/string/selection: audit `src/vec/{list,str,sel}.{c,h}` plus `src/vec/embedding.h`; split by module. +- [ ] P-06 — memory-lifetime × tables/symbol domains: audit `src/table/{dict,domain,sym,table}.{c,h}`; split by implementation/header pair. +- [ ] P-07 — memory-lifetime × runtime/types: audit `src/core/{runtime,types}.{c,h}` against N01 and error/null/lifetime docs. +- [ ] P-08 — query-correctness × blocks/morsels: audit `src/core/{block,morsel}.{c,h}` against pipeline and execution contracts. +- [ ] P-09 — concurrency-portability × pool/progress/platform: audit `src/core/{pool,progress,platform}.{c,h}`; split by module and record backend assumptions. +- [ ] P-10 — concurrency-portability × event backends: compare `src/core/{poll,epoll,kqueue,iocp}.c` and relevant declarations for semantic parity. +- [ ] P-11 — resilience-security × IPC/socket: audit `src/core/{ipc,sock}.{c,h}` and the applicable IPC tests for framing, authentication/restriction, cleanup, and platform error paths. +- [ ] P-12 — resilience-security × diagnostics/timing: audit `src/core/{crash,qlog,qmeasure,timer}.c` and their headers (`qstats.h`, `profile.h`, etc.) for failure-path safety and hardened-build promises. +- [ ] P-13 — api-doc-consistency × CLI/REPL: audit `src/app/*` against README quick-start/REPL behavior and focused `test/test_{repl,term}.c`; split by module. +- [ ] P-14 — language-semantics × parser/numeric parser: audit `src/lang/{parse,compile}.c` only for parsing/compilation boundaries plus `src/core/numparse.{c,h}`; split large symbol ranges. +- [ ] P-15 — language-semantics × evaluator/VM: audit `src/lang/eval.{c,h}`, `src/lang/cal.h`, and `src/lang/internal.h` against syntax/error/null contracts; split `eval.c` by symbols. +- [ ] P-16 — language-semantics × environment/format/system commands: audit `src/lang/{env,format,nfo,syscmd}.{c,h}` against namespace, rendering, and restricted-mode norms. +- [ ] P-17 — storage-integrity × CSV: audit `src/io/csv.{c,h}`, `test/test_csv.c`, and `docs/docs/storage/index.md` CSV sections; split `csv.c` by symbols. +- [ ] P-18 — storage-integrity × serialization/file metadata: audit `src/store/{serde,fileio,meta}.{c,h}` against wire/storage docs and relevant tests. +- [ ] P-19 — storage-integrity × column/partition/splay: audit `src/store/{col,part,splay}.{c,h}` against storage layout, pruning, and round-trip contracts; split by module. +- [ ] P-20 — storage-integrity × AOF/journal: audit `src/store/{aof,journal}.{c,h}`, focused tests, and documented replay/integrity contracts. +- [ ] P-21 — storage-integrity × CSR/HNSW: audit `src/store/{csr,hnsw}.{c,h}` against graph/vector persistence contracts and focused tests; split by subsystem. +- [ ] P-22 — query-correctness × aggregation engine: audit `src/ops/agg.c`, `agg_engine.{c,h}`, `agg_stream.c`, `agg_acc.h`, and `agg_registry.h`; split by engine phase. +- [ ] P-23 — language-semantics × scalar operations: audit `src/ops/{arith,cmp,temporal,string,strop}.c` with corresponding reference sections and tests; split by operation family. +- [ ] P-24 — resilience-security × builtin/system surface: audit `src/ops/{builtins,system,glob}.c` for registration, error propagation, restricted mode, and fuzz sandbox contracts. +- [ ] P-25 — language-semantics × collections/vectors: audit `src/ops/{collection,fvec}.c` and `fvec.h` against collection/function reference and tests; split large files by symbols. +- [ ] P-26 — query-correctness × datalog/LFTJ: audit `src/ops/{datalog,lftj}.{c,h}` against published APIs and focused tests; split by subsystem. +- [ ] P-27 — storage-integrity × operation journal/dump: audit `src/ops/{journal,dump}.c` and `journal.h` against serialization/replay norms. +- [ ] P-28 — query-correctness × embedding/rerank: audit `src/ops/{embedding,rerank}.c` against vector-search docs and executable tests. +- [ ] P-29 — query-correctness × executor/expression VM: audit `src/ops/exec.{c,h}` and `src/ops/expr.c`; each large file gets its own symbol-range subpass. +- [ ] P-30 — query-correctness × filtering/fused paths: audit `src/ops/{filter,fused_pred,fused_topk}.c` and `fused_{group,pred,topk}.h`; split by path. +- [ ] P-31 — query-correctness × graph/traversal: audit `src/ops/{graph,graph_builtin,traverse}.c` and `graph.h` against graph docs/tests; split by algorithm families. +- [ ] P-32 — query-correctness × grouping/hash/HLL: audit `src/ops/group.c`, `hash.h`, and `hll.{c,h}`; `group.c` must be split by enclosing symbol ranges. +- [ ] P-33 — query-correctness × index/link operations: audit `src/ops/{idxop,linkop}.{c,h}` against index/link docs and tests. +- [ ] P-34 — query-correctness × joins: audit `src/ops/join.c` against join contracts and focused C/Rayfall tests; split by join strategy. +- [ ] P-35 — query-correctness × optimizer/planner: audit `src/ops/{opt,idiom,plan}.{c,h}` against the documented pass order and focused optimizer tests. +- [ ] P-36 — query-correctness × query engine: audit `src/ops/query.c` against query docs/tests; split into explicit select/update/partition/execution symbol ranges. +- [ ] P-37 — query-correctness × pipe/row selection: audit `src/ops/{pipe,rowsel}.{c,h}` against selection/null and pipeline contracts. +- [ ] P-38 — query-correctness × pivot/table operations: audit `src/ops/{pivot,tblop}.c` against table operation docs/tests. +- [ ] P-39 — query-correctness × sort/window: audit `src/ops/{sort,window}.c` against null/order/window contracts; split `sort.c` by symbols. +- [ ] P-40 — api-doc-consistency × DAG registry: compare `src/ops/{ops,internal}.h`, opcode/registration tables, `docs/docs/c-api/dag.md`, and `test/test_agg_registry.c`; split declaration families. +- [ ] P-41 — api-doc-consistency × complete builtin surface: compare the registered runtime surface with `docs/docs/reference/all-functions.md` and `docs/docs/language/functions.md`; record exact missing/extra/signature cases. +- [ ] P-42 — api-doc-consistency × language syntax: compare `docs/docs/language/{syntax,control-flow,math,string,repl}.md` with parser/evaluator behavior and focused Rayfall tests; split by document. +- [ ] P-43 — api-doc-consistency × query/architecture docs: compare `docs/docs/{queries,architecture}/**/*.md` with the applicable operation headers/tests; batch at most three small docs or one large doc. +- [ ] P-44 — api-doc-consistency × storage/IPC docs: compare `docs/docs/storage/*.md` and namespace docs for `.db`, `.csv`, `.ipc`, `.log` with headers/registrations/tests; split by namespace. +- [ ] P-45 — language-semantics × errors/nulls: compare `docs/docs/guides/errors.md`, public error/null declarations, and `test/rfl/{null,expr,regress}/**/*.rfl`; split by error then null behavior. +- [ ] P-46 — test-adequacy × memory/core C tests: audit `test/test_{arena,buddy,cow,heap,heap_parallel,block,morsel,pool,platform,progress,runtime,types}.c` against the corresponding contracts; batch related small tests. +- [ ] P-47 — test-adequacy × language/runtime C tests: audit `test/test_{compile,err,format,lang,repl,term,numparse}.c`; split `test_lang.c` by symbol ranges. +- [ ] P-48 — test-adequacy × execution/query C tests: audit `test/test_{exec,opt,pipe,rowsel,group_extra,group_pushdown,join_buildside,window,sort,idx_route,partition_exec}.c`; split large tests by subsystem. +- [ ] P-49 — test-adequacy × storage/I/O C tests: audit `test/test_{aof,csv,ipc,journal,meta,splay,store,public_api}.c` against persistence and API contracts. +- [ ] P-50 — test-adequacy × values/tables C tests: audit `test/test_{atom,dict,domain,f64_nullmodel,list,sel,str,sym,table,vec}.c` against N01/N02/N03. +- [ ] P-51 — test-adequacy × graph/advanced C tests: audit `test/test_{csr,datalog,embedding,graph,graph_builtin,index,lftj,link,traverse,fused_topk,fvec}.c`; split large algorithm tests. +- [ ] P-52 — test-adequacy × stress/harness: audit `test/{main,stress_eval,stress_store,test_stress_eval,test_stress_matrix,test_stress_random,test_audit}.c` and CI discovery/gating. +- [ ] P-53 — test-adequacy × Rayfall scalar/language: audit `test/rfl/{arith,cmp,lang,type,temporal,strop}/**/*.rfl` for meaningful assertions and reference coverage; split by directory. +- [ ] P-54 — test-adequacy × Rayfall collections/nulls: audit `test/rfl/{collection,dict,hof,null,ops,symbol,table}/**/*.rfl`; split by directory. +- [ ] P-55 — test-adequacy × Rayfall aggregation/query: audit `test/rfl/{agg,group,query,pivot}/**/*.rfl`; split by directory. +- [ ] P-56 — test-adequacy × Rayfall joins/order/windows: audit `test/rfl/{join,linkop,sort,window}/**/*.rfl`; split by directory. +- [ ] P-57 — test-adequacy × Rayfall persistence/system: audit `test/rfl/{io,journal,storage,store,system,mem}/**/*.rfl`; split by directory. +- [ ] P-58 — test-adequacy × Rayfall graph/Datalog/vector: audit `test/rfl/{graph,datalog,embedding}/**/*.rfl`; split by directory. +- [ ] P-59 — test-adequacy × integration/regress/optimizer: audit `test/rfl/{integration,regress,opt,fused,lazy}/**/*.rfl`; split integration by subject and record untested contracts. +- [ ] P-60 — resilience-security × fuzz surfaces: compare all six fuzz drivers, `fuzz/README.md`, seeds/dictionaries, and CI/Makefile discovery; one driver/surface per subpass. +- [ ] P-61 — build-release × build flavors/portability: audit `Makefile`, `CONTRIBUTING.md`, `.clang-tidy`, `.tsan-suppressions`, and `.github/workflows/{ci,nightly}.yml` for source coverage and semantic/gate parity. +- [ ] P-62 — build-release × version/release/packaging: audit `RELEASE.md`, release/version workflows and scripts, `packaging/*`, and install claims in README; split release and packaging paths. +- [ ] P-63 — api-doc-consistency × examples: compile/compare `examples/*.c` and `examples/rfl/**/*.rfl` against the referenced APIs and README/docs; batch at most three examples. +- [ ] P-64 — docs-structure × navigation/redirects: audit `mkdocs.yml`, docs indexes, `docs/llms.txt`, redirect stubs, and link targets for promised reachability and orphan/duplicate content. +- [ ] P-65 — docs-structure × site shells: compare `overrides/**`, authored `website/*.{html,css,js}`, docs root pages, and Pages workflow for route/navigation/brand consistency. +- [ ] P-66 — performance-contract × architecture/benchmarks: compare README and architecture performance/resource claims with `bench/**` scenario setup and the implementation path each benchmark invokes; split by benchmark directory. + +## 5. Limits + +max_findings_per_pass: 15 +remediation_batch_size: 8 +class_threshold: 3 +reopen_limit: 2 + +No graph assistance was present during planning. Cross-unit relationships are +encoded directly in the API/doc, implementation/test, and build/workflow +passes above. diff --git a/audit/LEDGER.md b/audit/LEDGER.md new file mode 100644 index 00000000..130a4e08 --- /dev/null +++ b/audit/LEDGER.md @@ -0,0 +1,13 @@ +| id | title | severity | status | next | updated | +|---|---|---|---|---|---| +| F-0001 | Core lifecycle example does not compile against its documented header | major | fixed | Verifier | 2026-07-22 | +| F-0002 | Documentation and public API tests release errors with a no-op helper | major | superseded-by-class | — | 2026-07-22 | +| F-0003 | ray_eval_str null-return contract contradicts the public value invariant | major | superseded-by-class | — | 2026-07-22 | +| F-0004 | Public null-sentinel contract contradicts its own predicates | major | superseded-by-class | — | 2026-07-22 | +| F-0005 | Time atom documentation uses nanoseconds for a millisecond value | major | fixed | Verifier | 2026-07-22 | +| F-0006 | Public API test redeclares runtime functions instead of testing the header | minor | superseded-by-class | — | 2026-07-22 | +| F-0007 | Public date test labels a different epoch than the API reference | minor | fixed | Verifier | 2026-07-22 | +| CF-0001 | Public error-cleanup examples use ray_release on owned errors | major | reported | human | 2026-07-22 | +| CF-0002 | ray_eval_str comments still claim value-null is bare NULL | major | reported | human | 2026-07-22 | +| CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | reported | human | 2026-07-22 | +| CF-0004 | Runtime API is redeclared after including the public header | minor | reported | human | 2026-07-22 | diff --git a/audit/XCHECK.md b/audit/XCHECK.md new file mode 100644 index 00000000..1121ba90 --- /dev/null +++ b/audit/XCHECK.md @@ -0,0 +1,228 @@ +# XCHECK — Cross-Agent Audit & Remediation Methodology + +version: 0.1.2 + +Self-contained. If you are an agent reading this inside a project's `audit/` +directory, this file plus `AUDIT.md` plus your charter is everything you need. +Do not read the whole project "for context" — the Session Protocol (§4) tells +you exactly what to read. + +Language rule: this methodology and all templates are English. Findings, +plans, and reports are written in the operator's working language — pick one +per audit and stay consistent. Evidence quotes are always verbatim +in the original language of the material. + +## 1. System Overview + +The methodology runs one cycle, repeatedly, over a project of any size: `audit → human triage → remediate → verify`. An Auditor finds defects and files them as evidence-backed findings. A human triages them. A Remediator fixes the accepted ones. A Verifier gives each fix a binding verdict. Findings that survive verification close; findings that don't reopen and go back to remediation. + +The cycle exists because the obvious alternative — one long free-form agent session working through a large project — fails predictably. Long sessions lose the thread: they forget earlier decisions, invent particulars they never actually checked, and hallucinate both findings and fixes. This methodology substitutes three concrete mechanisms for confidence in an agent's memory: narrow chartered sessions that never run long enough to lose the thread, findings anchored to evidence that can be mechanically re-checked, and cross-agent verification where the fixer is never the judge. + +Architecture, in short: a findings ledger plus a finding state machine (§5) is the skeleton — durable, file-based state that outlives any single session. Per-session narrow charters (§4) are the working discipline — what makes each individual session reliable even though it starts from a blank context every time. + +Universality is a property of one boundary, and it is load-bearing: *"XCHECK.md defines mechanics and knows nothing about domains; AUDIT.md instantiates the methodology for one concrete project."* Everything in this file must hold for a project made of code and a project made of text, unchanged. Anything that depends on what kind of material is being audited belongs in AUDIT.md, not here. + +## 2. Artifacts + +A **unit** is one addressable piece of the material — one file, or one section of continuous text. A **dimension** is one audit angle, always backed by a norm (§6). AUDIT.md defines both concretely for a given project; this file only assumes they exist. + +The methodology's state lives entirely in the `audit/` directory of the audited project: + +``` +audit/ + XCHECK.md # copy of this file — the audit is self-contained + AUDIT.md # charter: dimensions, unit map, pass queue, norms, limits + LEDGER.md # index: one row per finding + templates/ # artifact skeletons (finding, class-finding, pass, plan) — copy the matching one when creating a new artifact file + findings/ + F-0001-.md # one finding, one file, full biography inside + CF-0001-.md # one class finding + passes/ + P-01-.md # one pass report: what was covered, what was not + plans/ + RP-0001.md # remediation plan for one batch of findings +``` + +IDs: `F-NNNN`, `CF-NNNN`, `RP-NNNN` are 4 digits. `P-NN` is 2 digits. Slugs: ASCII, +lowercase, hyphenated English gist of the title (titles themselves may be in the operator's working language). + +A finding file's frontmatter is the canonical record: + +```yaml +id: F-0001 # or CF-0001 for a class finding +title: +severity: critical | major | minor | info +dimension: +unit: # a YAML list for a cross-unit finding +status: reported +class: null | CF-NNNN # which class finding, if any, absorbed this one +attempts: 0 # incremented on each reopened cycle +pass: P-01 # which pass discovered it +updated: +``` + +`LEDGER.md` rows follow one fixed format: `| id | title | severity | status | next | updated |`. `next` names whichever role or the human is expected to act on the finding next: a canonical role name, `human`, or `—` for terminal states. The Verifier appends `⚠ needs-human` in the `next` column when `reopen_limit` consecutive reopenings are reached. + +Ownership rules: + +1. A finding file is the single source of truth for that finding. Everything about it — evidence, validation, remediation, verification — lives in that one file. +2. `LEDGER.md` is a derived index, not a second copy. One row per finding, nothing more. +3. The finding file is the source of truth for every status except pending triage decisions. Triage decisions live in LEDGER.md first: when a ledger row shows a triage status that is a legal triage transition from the file's frontmatter status (`reported` → `accepted`/`rejected`/`deferred`; member reversion to `accepted` on a rejected CF), the next agent session that touches that finding applies the decision to the file before any other work. Applying a pending triage decision is executing the human's call, not taking ownership of a triage status (§5). For all other disagreements, fix the ledger to match the file. + +## 3. Roles + +A role is a function, not an agent identity. The default assignment is Codex for Planner, Auditor, and Verifier, and Claude for Remediator — but any role can be played by any capable agent, and a third agent can join without changing this file. One rule is hard: **the Verifier is never the agent or session that produced the fix it verifies.** Prefer a different agent; the minimum acceptable substitute is a different, fresh session of the same agent. + +The only mandatory human step in the cycle is Triage. Everything else runs agent-to-agent through files. + +### Planner +- **Mission:** inventory the project and its norms; produce dimensions, a unit map, and a pass queue. +- **Default agent:** Codex, one session. +- **Reads:** XCHECK.md; the project's normative documents; a structural listing of the project. Not the full content of every unit. +- **Writes:** AUDIT.md. +- **Stop conditions:** AUDIT.md exists, with at least one dimension backed by a norm and a non-empty pass queue. +- **Forbidden:** filing findings; reading unit content end to end "to get a feel for it" — inventory only. + +### Auditor +- **Mission:** run one pass — one dimension against a batch of units — and record findings; also give disputed findings one written round of objection. +- **Default agent:** Codex, one session per pass (or a split of one, §4 rule 4). +- **Reads:** XCHECK.md; AUDIT.md; the pass charter; the units in the charter's scope; the norm the charter cites; templates/pass.md and templates/finding.md, copied when starting a pass or filing a finding. +- **Writes:** `findings/`, `passes/P-NN-.md`, new rows in LEDGER.md; the AUDIT.md pass queue — tick the completed pass's checkbox; append split remainders as new queued passes; one round of written objection in a finding file when the Remediator disputes it (§5, §9 rule 3). +- **Dedup rule:** before filing a finding, check it against ledger titles only — not finding bodies, not memory of earlier passes. A matching title gets new evidence appended to the existing finding, not a duplicate. +- **Stop conditions:** charter's unit range exhausted, or `max_findings_per_pass` (§10) reached, or context budget exhausted. +- **Forbidden:** fixing anything; exceeding the charter's unit range instead of splitting it; filing a finding that does not meet the Evidence Standard (§6). + +### Triage +- **Mission:** decide which findings get worked. +- **Default agent:** human. The one mandatory gate in the cycle. +- **Reads:** LEDGER.md; finding files for any finding whose title alone doesn't settle the call. +- **Writes:** the status column in LEDGER.md only — `reported` to `accepted`, `rejected`, or `deferred`. Batch decisions are allowed, e.g. all `critical` and `major` findings to `accepted` in one action. Rejecting a CF also reverts every finding in its `members:` list to `accepted` (write the ledger rows; agents apply to files per §2). +- **Stop conditions:** every `reported` row under review has a decision. +- **Forbidden:** editing finding file body sections — frontmatter sync for triage decisions is carried out by agent sessions per §2; setting any status other than `accepted`, `rejected`, or `deferred`. +- **Agent-as-pen:** the human may run triage through an interactive agent session. The agent presents findings and records the human's stated decisions in LEDGER.md verbatim; the decisions remain the human's. An agent must never accept, reject, or defer a finding on its own judgment, and must not batch-infer decisions the human did not state ("everything else rejected" counts only if the human said it). + +### Remediator +- **Mission:** fix a batch of accepted findings. +- **Default agent:** Claude, one or more sessions. +- **Reads:** XCHECK.md; AUDIT.md; the charter (a list of finding IDs); the finding files in scope; templates/plan.md and templates/class-finding.md, copied when opening a remediation plan or a class finding. +- **Writes:** `plans/RP-NNNN.md`; edits to the project material; the Validation and Remediation sections of each finding file; LEDGER.md. +- **Mandatory in-session sequence:** **validate → census → plan → fix → self-check.** No step is skipped or reordered (§7, §8 rule 1). Self-check means re-running the finding's own "how to verify" procedure before marking it fixed — catching what the Verifier would catch anyway, before it costs a reopen cycle. +- **Stop conditions:** every finding in the charter reaches `fixed`, `disputed`, `obsolete`, or `superseded-by-class`; or `remediation_batch_size` (§10) is exhausted; or context budget exhausted. +- **Forbidden:** touching a finding outside the charter; marking a finding `fixed` without a Remediation section naming what changed and where; verifying its own fix. + +### Verifier +- **Mission:** give a binding verdict on a batch of fixed findings. +- **Default agent:** Codex, one or more sessions. +- **Reads:** XCHECK.md; AUDIT.md; the charter; the finding files in scope; the diffs or changes the Remediation section points to. +- **Writes:** the Verification section of each finding file; LEDGER.md (`closed` or `reopened`). +- **Hard rule:** never the agent or session that produced the fix. **Adversarial stance:** *"Your job is to prove the fix wrong, not to confirm it."* +- **Stop conditions:** every finding in the charter has a verdict. +- **Forbidden:** closing a finding without running its "how to verify" procedure; skipping the adversarial check of the surrounding change; verifying its own fix. + +## 4. Session Protocol + +Eight rules. They apply to every session, in every role. + +1. **Charter required.** No session starts without an exact scope and stop conditions — e.g. "dimension X, units 4–6, stop after 15 findings" for an Auditor, or "findings F-0012..F-0019" for a Remediator. +2. **Fresh eyes.** State comes only from files. A session does not inherit conclusions from earlier sessions by memory. Anything a session acts on is re-verified against its source at the moment it is touched, regardless of what any prior session recorded about it. +3. **Explicit reading list.** Read XCHECK.md, AUDIT.md, your charter, the files the ledger points you to, and the artifact templates you instantiate. Nothing else. Reading the whole project "for context" is forbidden. +4. **Stop conditions are sacred.** Scope overflow: split the charter and write the remainder into the pass queue in AUDIT.md; skimming through to the end anyway is forbidden. Context running out: record what was covered and what was not, then exit cleanly. +5. **Coverage report is mandatory.** Every pass states explicitly what it did not cover. A silent gap is a protocol violation, not an acceptable shortcut. +6. **No finding quota.** Caps (§10) are upper bounds, not targets, and there is no minimum. A clean pass — zero findings — is a valid result and is recorded as one. +7. **Role launch is a one-liner.** A human starts a session with a single line: `Read audit/XCHECK.md. Role: . Charter: .` +8. **One active writing session per project at a time.** Coordination in v1 is manual: one human relays the baton between sessions. Concurrent writers are out of scope. Launcher tooling and scripted chains must serialize the same way: never start a writing session while another is active. + +## 5. Finding Lifecycle + +``` +reported → (Triage) → accepted | rejected | deferred +accepted → (Remediator) → validated | disputed | obsolete +validated → planned → fixed +fixed → (Verifier) → closed | reopened +reopened → back to the Remediator (planned → fixed again), attempts += 1 +disputed → (Auditor/human) → accepted | withdrawn +any status → (Remediator) → superseded-by-class (absorbed by a CF finding, §8) +superseded-by-class → (Triage) → accepted (CF rejected at triage, §8 rule 4) +``` + +| From | Event / actor | To | +|---|---|---| +| `reported` | Triage decides | `accepted`, `rejected`, or `deferred` | +| `accepted` | Remediator confirms the evidence | `validated` | +| `accepted` | Remediator cannot confirm the evidence | `disputed` | +| `accepted` | Remediator finds the defect already gone | `obsolete` | +| `validated` | Remediator writes a plan | `planned` | +| `planned` | Remediator applies the fix | `fixed` | +| `fixed` | Verifier confirms | `closed` | +| `fixed` | Verifier rejects | `reopened` | +| `reopened` | returns to the Remediator | `planned`, then `fixed` again; the Remediator increments `attempts` | +| `disputed` | Auditor or human sides with the finding | `accepted` | +| `disputed` | Auditor or human sides against the finding | `withdrawn` | +| any | Remediator opens a class finding for its pattern | `superseded-by-class` | +| `superseded-by-class` | Triage rejects the CF | `accepted` | + +Status ownership is the rule that keeps five roles from stepping on each other: *"One status, one owner: triage statuses are changed only by the human; validated/planned/fixed only by the Remediator; closed/reopened only by the Verifier; disputed resolution by the Auditor or the human. Never touch a status you do not own."* + +Definitions: + +- **`obsolete`** — validation shows the defect is already gone: an unrelated edit removed it before remediation touched it. +- **`disputed`** — the quote is not found in the material, or the problem is not substantively confirmed. +- **`reopen_limit` breach** — `reopen_limit` (default 2, §10) consecutive `reopened` verdicts on the same finding set the ledger flag `⚠ needs-human`; the automatic cycle stops and a human decides what happens next. +- **`withdrawn`** — a `disputed` finding resolved against the Auditor; it closes without a fix. +- **`superseded-by-class`** — set immediately on every member finding when a class finding is opened for its pattern (§8 rule 4); reverts to `accepted` if the human rejects the CF at triage. + +## 6. Evidence Standard + +Evidence is the anti-hallucination mechanism at the center of this methodology. Six rules govern every finding: + +1. **The quote is the primary anchor.** Character-exact, findable by a literal search in the material. A line number is a hint, not an anchor — text moves as the material is edited. +2. **A norm reference is mandatory.** A finding is a discrepancy between the material and a norm: a style contract, a spec, an ADR, a sourced fact, or internal consistency — in which case the contradicting passage is itself quoted as the norm. No norm, no finding: it is an opinion, and its severity is capped at `info`. +3. **"How to verify the fix" is written by the Auditor at creation time**, while the defect is in front of them. It is never written by the Verifier after the fact. +4. **A finding without evidence meeting this standard is invalid by construction.** It must not reach Triage. +5. **Locators follow the material kind.** Code: `file:line` plus the enclosing symbol name. Text: file, section, and the quote itself. +6. **Auditor-authored sections of a finding are append-only for other roles.** Evidence, Why this is a defect, and How to verify the fix are written once, by the Auditor; other roles may add to them but not rewrite them. The only in-place edit allowed is a locator update (§9 rule 1). + +## 7. Validation & Verification + +Two checks apply the Evidence Standard at two different points in the cycle. + +**Validation** is the mandatory first step of remediation (§3). Before planning a fix, the Remediator confirms the finding against the source: the quote exists, and the problem is real. This is close to free — the source has to be read to plan the fix anyway. Failure routes the finding to `disputed` (problem not confirmed) or `obsolete` (defect already gone). + +**Verification** is the Verifier's binding verdict on a `fixed` finding, and it has two parts. First, run the procedure the Auditor wrote in "How to verify the fix" against the current material. Second, adversarially inspect the surroundings of the change for collateral damage that procedure alone would not catch. The verdict is `closed` or `reopened`. Evidence for a `reopened` verdict is held to the same Evidence Standard (§6) as an original finding — an exact quote and a norm, not an impression. When a fix installed an automated guard, verify the guard adversarially: plant the defect it claims to catch in a temporary copy and expect the guard to fail it — a guard that passes its own selftest but not a live mutation is a hole, not a defense. + +## 8. Class Escalation + +A finding may be one instance of a systematic defect rather than a one-off. Escalation exists to catch that and fix it globally instead of one instance at a time. + +1. **Census is the mandatory second step of remediation**, after validation and before planning. The Remediator formulates the finding's pattern and searches for it across the whole project — not only in the finding's own unit. The search procedure fits the material (a literal-text or pattern search across files; a targeted read across sections where the pattern could recur) and must be recorded precisely enough for someone else to rerun it. +2. **Escalation threshold: `class_threshold` instances (default 3, §10).** Below threshold, fix the instance found and note its siblings in the finding file. At or above threshold, open a class finding. +3. **A class finding (`CF-NNNN`) contains:** an exact definition of the pattern and its search procedure; a full census of instances with locators, including any the original pass missed; the root cause — most often a norm that is missing, ambiguous, or inconsistently enforced; and a global strategy on an increasing scale: + - (a) fix every instance; + - (b) (a), plus fix the norm so the class cannot recur; + - (c) (b), plus an automated guard where one is possible — a search check, a lint rule, a style script — so the class becomes structurally hard to reintroduce. +4. **A CF finding passes through the same Triage gate** as any other finding — status `reported`, the human decides. The single human gate does not multiply, but a global change never bypasses it. On creation, every member finding moves to `superseded-by-class` immediately; if the human rejects the CF at triage, members revert to `accepted` for point fixes instead. The Remediator continues the current session with the rest of the batch — the CF waits for the next triage round. +5. **Class verification is a re-census**, performed by the Verifier: rerun the recorded search procedure and expect zero instances, or explicitly documented exceptions. This is what catches "fixed 12 of 15." +6. **Norm write-back is part of the fix, not a side effect.** Strategies (b) and (c) edit the project's normative documents in the same remediation, not as optional follow-up work. If the project maintains a persistent agent-memory or lessons store, record the norm change there in the same remediation. +7. **The five-step Remediator sequence maps onto an accepted CF as follows:** validate = re-run the recorded census procedure; census = already done (the CF's census IS the scope); plan = the Global strategy section; fix as usual; self-check = re-run the census procedure expecting zero instances or documented exceptions. +8. **Norm ratification gate.** If a class fix under strategy (b) or (c) changes a norm, relies on a norm that another normative source contradicts (including machine registries and configuration files), or must pick a side in any conflict between norms — the Remediator stops after writing the plan and routes the conflict to the norm owner through triage before executing. Documenting a norm conflict and proceeding anyway is a protocol violation for class fixes: a class fix in the wrong direction multiplies one error across the whole corpus. + +## 9. Failure Modes + +1. **Stale finding.** The material moved since the finding was filed, and the quote is no longer found by a literal search. Re-locate by meaning. If the defect is gone, mark `obsolete`. If it moved, update the locator and proceed. Fixing "from memory of where it used to be" is forbidden. +2. **Non-converging cycle.** `reopen_limit` (§10) consecutive `reopened` verdicts on one finding set `⚠ needs-human` in the ledger (§5). The automatic cycle does not keep spinning on its own. +3. **Agent dispute.** The Auditor insists on a finding the Remediator marked `disputed`. One written round of objection in the finding file, then the human decides. Ping-pong between agents is forbidden. +4. **Ledger drift.** The finding file is truth; LEDGER.md is a derived index (§2). Whoever notices a mismatch fixes the ledger silently — except a pending triage decision, which flows the other way: apply it to the file per §2 rule 3. +5. **Charter overflow.** The remainder of an overflowing charter becomes a new queued pass in AUDIT.md (§4 rule 4). A session interrupted mid-charter must still leave a coverage report (§4 rule 5) — otherwise the pass counts as not done at all. +6. **Concurrent human edits to the material.** Not forbidden. The Evidence Standard (§6) self-protects: a quote either still matches, or the finding falls into the stale-finding path above. +7. **Git.** If the project is under git: remediation commits reference finding IDs, and verification reads diffs. If not: the finding's Remediation section lists the files or sections changed. Git is an amplifier, not a requirement. + +## 10. Configuration Defaults + +| Key | Default | Meaning | +|---|---|---| +| `max_findings_per_pass` | 15 | Upper bound on findings recorded by one Auditor pass (§4 rule 6). | +| `remediation_batch_size` | 8 | Upper bound on findings worked by one Remediator session (§3). | +| `class_threshold` | 3 | Minimum instance count that escalates a finding to a class finding (§8 rule 2). | +| `reopen_limit` | 2 | Consecutive `reopened` verdicts on one finding before it is flagged `⚠ needs-human` (§5, §9 rule 2). | + +AUDIT.md may override any of these defaults per project. diff --git a/audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md b/audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md new file mode 100644 index 00000000..665c0089 --- /dev/null +++ b/audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md @@ -0,0 +1,69 @@ +--- +id: CF-0001 +title: Public error-cleanup examples use ray_release on owned errors +severity: major +dimension: api-doc-consistency +unit: + - docs/docs/c-api/core.md + - docs/docs/c-api/dag.md + - test/test_public_api.c +status: reported +class: null +members: + - F-0002 +attempts: 0 +pass: P-01 +created-by: RP-0001 +updated: 2026-07-22 +--- + +## Pattern + +An owned `RAY_ERROR` in a public C API reference example or the +public-header conformance test is cleaned up with `ray_release`, even though +the public ownership contract says that helper is a no-op for errors and that +the owner must call `ray_error_free`. + +Reproduce the census with: + +```sh +rg -n -C 12 'ray_release\((result|err)\)' docs/docs/c-api test/test_public_api.c +``` + +For every match, retain it only when the same example/test has already proved +the released value is an error with `RAY_IS_ERR`. The search covers the whole +public C API reference tree and its public-header conformance test. + +## Census + +- `docs/docs/c-api/core.md:490-492`, the `RAY_IS_ERR(p)` example releases the + owned `result` error. +- `docs/docs/c-api/dag.md:373-375`, the `ray_execute` example releases the + owned `result` error. +- `test/test_public_api.c:580-592`, + `test_public_get_error_trace_populated` releases the owned `err` error. +- `test/test_public_api.c:601-604`, + `test_public_get_error_trace_cleared_on_eval` releases the owned `err` + error. + +Four instances meet the project threshold of three. + +## Validation + + +## Root cause + +The public error-ownership norm in `include/rayforce.h` was added without a +complete update of the public examples and conformance tests that demonstrate +error cleanup. + +## Global strategy + +Rung (a): replace all four cleanup calls with `ray_error_free`, then run the +public API test and rebuild the documentation. Re-run the recorded census and +require zero public-contract instances. + +## Remediation + + +## Verification diff --git a/audit/findings/CF-0002-ray-eval-str-null-contract-stale.md b/audit/findings/CF-0002-ray-eval-str-null-contract-stale.md new file mode 100644 index 00000000..02a75b35 --- /dev/null +++ b/audit/findings/CF-0002-ray-eval-str-null-contract-stale.md @@ -0,0 +1,64 @@ +--- +id: CF-0002 +title: ray_eval_str comments still claim value-null is bare NULL +severity: major +dimension: api-doc-consistency +unit: + - include/rayforce.h + - test/main.c + - test/stress_eval.c +status: reported +class: null +members: + - F-0003 +attempts: 0 +pass: P-01 +created-by: RP-0001 +updated: 2026-07-22 +--- + +## Pattern + +A comment describing `ray_eval_str` says a void/null value is represented by +bare C `NULL`, contradicting the public value invariant and current evaluator, +which use `RAY_NULL_OBJ`. + +Reproduce the census with: + +```sh +rg -n -i 'ray_eval_str.*(returns? NULL|NULL = void/null|void results)' \ + include src docs test README.md examples fuzz bench +``` + +## Census + +- `include/rayforce.h:667`, the public declaration says “Returns NULL for + void / null results”. +- `test/main.c:247-248`, the Rayfall harness says `ray_eval_str` returns NULL + for void results and describes bare-NULL comparison semantics. +- `test/stress_eval.c:69`, the stress harness annotates the result with + “NULL = void/null result, fine”. + +Three instances meet the project threshold of three. + +## Validation + + +## Root cause + +The evaluator's null-result contract moved to the `RAY_NULL_OBJ` value +invariant, but comments in the public header and two harnesses were not migrated +with it. + +## Global strategy + +Rung (b): update all three comments to the `RAY_NULL_OBJ` contract and make +the public invariant the named source of truth. Retain defensive bare-NULL +checks where they protect against allocation failure, but do not describe bare +NULL as a successful evaluation result. Re-run the focused null-evaluation +test and the recorded census. + +## Remediation + + +## Verification diff --git a/audit/findings/CF-0003-null-encoding-comments-conflict.md b/audit/findings/CF-0003-null-encoding-comments-conflict.md new file mode 100644 index 00000000..8dfaafb6 --- /dev/null +++ b/audit/findings/CF-0003-null-encoding-comments-conflict.md @@ -0,0 +1,92 @@ +--- +id: CF-0003 +title: Null-encoding comments conflict across scalar and vector surfaces +severity: major +dimension: api-doc-consistency +unit: + - include/rayforce.h + - src/vec/atom.c + - src/vec/vec.c + - src/table/domain.h + - src/ops/pivot.c + - docs/docs/namespaces/csv.md + - test/stress_store.c + - test/test_dict.c + - test/test_format.c + - test/test_str.c + - test/rfl/integration/null.rfl + - test/rfl/strop/split.rfl +status: reported +class: null +members: + - F-0004 +attempts: 0 +pass: P-01 +created-by: RP-0001 +updated: 2026-07-22 +--- + +## Pattern + +A comment or API-facing statement assigns null status to empty SYM/STR values, +or says F32 has no sentinel, while the public atom/vector predicates and +current focused tests say SYM/STR empty values are ordinary values and F32 +uses NaN. + +Reproduce the candidate census with: + +```sh +rg -n -i '(SYM null\s*=|STR null\s*=|empty string IS.*null|empty symbol.*null|BOOL/U8/F32|types without a sentinel|global id 0.*null|id 0 is the SYM null)' \ + include src docs test README.md examples fuzz bench +``` + +Classify each candidate against `ray_atom_is_null_fn` in +`include/rayforce.h`, `ray_vec_is_null` in `src/vec/vec.c`, and the explicit +`nil?` expectations in `test/rfl/null/sym_no_null.rfl`. + +## Census + +- `include/rayforce.h:441-442` assigns SYM id 0 and empty STR as null. +- `include/rayforce.h:451-452` incorrectly groups F32 with types lacking a + sentinel. +- `src/vec/atom.c:185-186` repeats the F32 no-sentinel claim. +- `src/table/domain.h:50-51` calls global SYM id 0 null. +- `src/ops/pivot.c:1268-1269` calls SYM id 0 the SYM null. +- `src/vec/vec.c:1156-1157` calls an empty STR descriptor a STR null. +- `src/vec/vec.c:1200` repeats “STR null = empty string”. +- `docs/docs/namespaces/csv.md:25` says empty strings are read as null and + marked `RAY_ATTR_HAS_NULLS`. +- `test/stress_store.c:118` labels the empty symbol as a SYM null. +- `test/test_dict.c:1177-1179` says empty STR is a null atom. +- `test/test_dict.c:1214` repeats the STR-null/empty-string conflation. +- `test/test_format.c:751-753` calls the empty string and “null string” the + same null-like value, despite the predicate returning false. +- `test/test_str.c:1830` says a STR null is stored as an empty string. +- `test/rfl/integration/null.rfl:8` says “STR null = empty string” immediately + before an expectation that `(nil? "")` is false. +- `test/rfl/strop/split.rfl:4-5` says empty string is the STR null immediately + before an expectation that the resulting empty value is not null. + +Fifteen instances meet the project threshold of three. + +## Validation + + +## Root cause + +The project lacks one ratified nullability matrix that distinguishes typed-null +construction, payload sentinels, empty-value collapse, and the public null +predicates. Comments from incompatible historical models coexist. + +## Global strategy + +Proposed rung (b): first obtain norm-owner ratification for a single per-type +matrix, because the current normative and executable surfaces conflict. Then +fix all census instances and write the ratified matrix into the public API +contract. The norm ratification gate applies: no global edit should execute +until triage records which semantics own SYM/STR empty values and F32. + +## Remediation + + +## Verification diff --git a/audit/findings/CF-0004-runtime-api-redeclared-after-public-header.md b/audit/findings/CF-0004-runtime-api-redeclared-after-public-header.md new file mode 100644 index 00000000..d268a41e --- /dev/null +++ b/audit/findings/CF-0004-runtime-api-redeclared-after-public-header.md @@ -0,0 +1,86 @@ +--- +id: CF-0004 +title: Runtime API is redeclared after including the public header +severity: minor +dimension: api-doc-consistency +unit: + - fuzz/common.h + - test/main.c + - test/test_compile.c + - test/test_datalog.c + - test/test_embedding.c + - test/test_format.c + - test/test_fused_topk.c + - test/test_ipc.c + - test/test_journal.c + - test/test_lang.c + - test/test_link.c + - test/test_public_api.c + - test/test_repl.c + - test/test_store.c + - test/test_term.c + - test/test_types.c +status: reported +class: null +members: + - F-0006 +attempts: 0 +pass: P-01 +created-by: RP-0001 +updated: 2026-07-22 +--- + +## Pattern + +A C translation unit directly includes `rayforce.h` and then locally repeats +the public `ray_runtime_t`, `ray_runtime_create`, or `ray_runtime_destroy` +declarations. Those repetitions mask removal or drift in the public header. + +Reproduce the census with: + +```sh +rg -l -0 '#include[[:space:]]*[<"]rayforce\.h[>"]' --glob '*.[ch]' . \ + | xargs -0 rg -n '(struct ray_runtime_s;|typedef struct ray_runtime_s ray_runtime_t;|extern[[:space:]]+ray_runtime_t\*[[:space:]]*ray_runtime_create|extern[[:space:]]+void[[:space:]]+ray_runtime_destroy)' +``` + +## Census + +- `fuzz/common.h:38-40` +- `test/main.c:54-57` +- `test/test_compile.c:40-43` +- `test/test_datalog.c:43` +- `test/test_embedding.c:45-48` +- `test/test_format.c:36-39` +- `test/test_fused_topk.c:51-54` +- `test/test_ipc.c:86-88` +- `test/test_journal.c:46-49` +- `test/test_lang.c:54-57` +- `test/test_link.c:50-53` +- `test/test_public_api.c:37-40` +- `test/test_repl.c:77-80` +- `test/test_store.c:60-62` +- `test/test_term.c:76-79` +- `test/test_types.c:32-35` + +Sixteen translation units meet the project threshold of three. + +## Validation + + +## Root cause + +The runtime declarations were historically internal and were copied into test +and fuzz harnesses. When they became public, the local compatibility blocks +were not removed and no guard prohibited redeclaring public API symbols. + +## Global strategy + +Rung (c): remove every redundant public runtime declaration while retaining +declarations of genuinely internal globals such as `__RUNTIME`; compile all +affected test/fuzz targets; and add a lightweight source check that rejects +future runtime API redeclarations in files that include `rayforce.h`. + +## Remediation + + +## Verification diff --git a/audit/findings/F-0001-core-lifecycle-example-does-not-compile.md b/audit/findings/F-0001-core-lifecycle-example-does-not-compile.md new file mode 100644 index 00000000..34777be5 --- /dev/null +++ b/audit/findings/F-0001-core-lifecycle-example-does-not-compile.md @@ -0,0 +1,103 @@ +--- +id: F-0001 +title: Core lifecycle example does not compile against its documented header +severity: major +dimension: api-doc-consistency +unit: + - docs/docs/c-api/core.md + - include/rayforce.h + - test/test_public_api.c +status: fixed +class: null +attempts: 0 +pass: P-01 +updated: 2026-07-22 +--- + +## Evidence + +In `docs/docs/c-api/core.md`, section “Headers” (line 4), the page says: + +> Include `` for the bulk of the public API. A few internal helpers documented here live in their own headers — in particular `ray_cow` and the `ray_heap_init` / `ray_heap_destroy` lifecycle calls are declared in `"mem/heap.h"`, not in ``. + +But its “Typical lifecycle pattern” (lines 42–54) contains only: + +```c +#include + +int main(void) { + ray_heap_init(); + ray_sym_init(); + /* ... use Rayforce API ... */ + ray_sym_destroy(); + ray_heap_destroy(); +``` + +Compiling that pattern as C17 with `cc -std=c17 -Werror -fsyntax-only +-Iinclude` fails with implicit declarations for both heap calls. + +The public lifecycle norm in `include/rayforce.h`, section “Runtime + Rayfall +Eval API” (lines 623–664), instead says: + +> Embedders build a runtime, optionally restoring a persisted symbol table, then evaluate Rayfall source strings against the global env. + +and declares: + +```c +ray_runtime_t* ray_runtime_create(int argc, char** argv); +void ray_runtime_destroy(ray_runtime_t* rt); +``` + +`test/test_public_api.c`, `test_public_runtime_create_with_sym_eval` (lines +406–426), also exercises the public runtime create/destroy lifecycle rather +than the undocumented heap declarations. + +## Why this is a defect + +The page's canonical lifecycle example fails under the project's C17/Werror +contract and steers embedders away from the lifecycle actually exported by the +public header and exercised by the public API test. + +## How to verify the fix + +Extract the revised “Typical lifecycle pattern” into a temporary `.c` file and +run `cc -std=c17 -Werror -fsyntax-only -Iinclude `. It must compile using +exactly the headers shown in the example, and its create/destroy pairing must +match a lifecycle declared in `include/rayforce.h` and exercised by +`test/test_public_api.c`. + +## Validation + +Validated. The quoted documentation and public declarations are still present. +Recompiling the documented pattern with +`cc -std=c17 -Werror -fsyntax-only -Iinclude` fails on implicit declarations +of `ray_heap_init` and `ray_heap_destroy`, while the public header and public API +tests expose the runtime create/destroy lifecycle. + +## Census + +Pattern: a standalone public C example calls `ray_heap_init` or +`ray_heap_destroy` without including `mem/heap.h`, or presents those internal +calls as the public embedder lifecycle. Searched with +`rg -n 'ray_heap_init|ray_heap_destroy|ray_runtime_create|ray_runtime_destroy' +README.md docs examples` and inspected every containing C example's include +set. The only defective standalone example is the “Typical lifecycle pattern” +in `docs/docs/c-api/core.md`; README and complete DAG examples include +`mem/heap.h`. One instance is below the class threshold. + +## Objection + + +## Remediation + +Replaced the “Typical lifecycle pattern” in `docs/docs/c-api/core.md` with the +public `ray_runtime_create`/`ray_runtime_destroy` lifecycle declared by +`rayforce.h`. The example checks runtime creation before use and no longer +calls undeclared internal heap functions. + +Self-check: extracted the revised snippet to a temporary C file and ran +`cc -std=c17 -Werror -fsyntax-only -Iinclude`; it exited 0. The full sanitized +suite also passed, 3638/3638. + + +## Verification diff --git a/audit/findings/F-0002-error-objects-released-with-no-op-helper.md b/audit/findings/F-0002-error-objects-released-with-no-op-helper.md new file mode 100644 index 00000000..0341a04f --- /dev/null +++ b/audit/findings/F-0002-error-objects-released-with-no-op-helper.md @@ -0,0 +1,83 @@ +--- +id: F-0002 +title: Documentation and public API tests release errors with a no-op helper +severity: major +dimension: api-doc-consistency +unit: + - docs/docs/c-api/core.md + - test/test_public_api.c + - include/rayforce.h +status: superseded-by-class +class: CF-0001 +attempts: 0 +pass: P-01 +updated: 2026-07-22 +--- + +## Evidence + +`docs/docs/c-api/core.md`, section “RAY_IS_ERR(p)” (lines 488–493), teaches: + +```c +if (RAY_IS_ERR(result)) { + printf("Error: %s\n", ray_err_code(result)); + ray_release(result); + return 1; +} +``` + +`test/test_public_api.c` repeats the same cleanup in +`test_public_get_error_trace_populated` (line 592) and +`test_public_get_error_trace_cleared_on_eval` (line 604): + +```c +ray_release(err); +``` + +The ownership norm in `include/rayforce.h`, immediately above +`ray_error_free` (lines 237–241), says the opposite: + +> Free a RAY_ERROR object. ray_release() is a deliberate no-op for error ray_t* (see src/mem/cow.c), so callers that hold the sole reference and want the block reclaimed must use this helper instead — otherwise the error leaks until heap teardown. + +and declares: + +```c +void ray_error_free(ray_t* err); +``` + +## Why this is a defect + +The primary error-handling example and its public API tests normalize a cleanup +operation the public header explicitly says leaks. A long-lived embedder that +copies the example retains every error object until heap teardown. + +## How to verify the fix + +Search these units for cleanup of values already proven by `RAY_IS_ERR`: +`rg -n 'ray_release\((result|err)\)' docs/docs/c-api/core.md +test/test_public_api.c`. Every such owned error must be reclaimed according to +the `ray_error_free` contract, and the focused public API tests must still pass. + +## Validation + +Validated. All three quoted cleanup calls remain. `src/mem/cow.c`, +`ray_release`, still returns immediately for `RAY_IS_ERR(v)`, confirming that +the documented/test cleanup does not reclaim error objects and that +`ray_error_free` is required for owned errors. + +## Census + +The public-contract cleanup pattern recurs four times across the C API +reference and public-header conformance test. The full reproducible census is +recorded in CF-0001. Because four meets the configured class threshold of +three, this finding is superseded by CF-0001 pending human triage. + +## Objection + + +## Remediation + +Superseded by CF-0001 during RP-0001; no point fix was applied. + + +## Verification diff --git a/audit/findings/F-0003-eval-null-return-contract-contradicts-invariant.md b/audit/findings/F-0003-eval-null-return-contract-contradicts-invariant.md new file mode 100644 index 00000000..070ccd10 --- /dev/null +++ b/audit/findings/F-0003-eval-null-return-contract-contradicts-invariant.md @@ -0,0 +1,62 @@ +--- +id: F-0003 +title: ray_eval_str null-return contract contradicts the public value invariant +severity: major +dimension: api-doc-consistency +unit: include/rayforce.h +status: superseded-by-class +class: CF-0002 +attempts: 0 +pass: P-01 +updated: 2026-07-22 +--- + +## Evidence + +`include/rayforce.h`, immediately above `RAY_ASSERT_VALUE` (lines 208–210), +states the public value norm: + +> A value position (an eval'd / builtin-returned ray_t*) is never a bare C NULL — value-null is always RAY_NULL_OBJ. + +The contract immediately above `ray_eval_str` in the same header (lines +666–670) contradicts it: + +> Parse and evaluate a Rayfall source string against the global env. Returns NULL for void / null results, an error ray_t* on failure (test with RAY_IS_ERR and inspect with ray_err_code), or the result value otherwise. + +## Why this is a defect + +Embedders cannot determine from the public header whether an evaluated null is +tested with `p == NULL` or `RAY_IS_NULL(p)`. Those alternatives imply different +safe control flow because the surrounding API explicitly distinguishes bare C +NULL from `RAY_NULL_OBJ`. + +## How to verify the fix + +Re-read both quoted contracts and compile/run a focused public API test that +calls `ray_eval_str` on a void/null expression. The observed return and both +header comments must agree on exactly one representation; the test must assert +that representation explicitly. + +## Validation + +Validated. Both contradictory header comments remain. The current evaluator +returns `RAY_NULL_OBJ` for no-value paths, and the existing +`test_eval_println` assertion explicitly confirms `RAY_IS_NULL(result)`, so the +`ray_eval_str` comment is the stale side of the contract. + +## Census + +Exact searches for comments coupling `ray_eval_str` with bare NULL null/void +results found three instances: `include/rayforce.h:667`, `test/main.c:247-248`, +and `test/stress_eval.c:69`. The complete procedure is in CF-0002. Three meets +the class threshold, so this finding is superseded pending triage of CF-0002. + +## Objection + + +## Remediation + +Superseded by CF-0002 during RP-0001; no point fix was applied. + + +## Verification diff --git a/audit/findings/F-0004-null-sentinel-contract-contradicts-itself.md b/audit/findings/F-0004-null-sentinel-contract-contradicts-itself.md new file mode 100644 index 00000000..58202e96 --- /dev/null +++ b/audit/findings/F-0004-null-sentinel-contract-contradicts-itself.md @@ -0,0 +1,75 @@ +--- +id: F-0004 +title: Public null-sentinel contract contradicts its own predicates +severity: major +dimension: api-doc-consistency +unit: include/rayforce.h +status: superseded-by-class +class: CF-0003 +attempts: 0 +pass: P-01 +updated: 2026-07-22 +--- + +## Evidence + +`include/rayforce.h`, section “Null Sentinel Values” (lines 438–447), declares: + +> SYM null = sym ID 0; STR null = empty string (length 0); BOOL and U8 are non-nullable. + +and defines `NULL_F32`: + +```c +#define NULL_F32 ((float)__builtin_nanf("")) +``` + +The public `ray_atom_is_null_fn` contract and implementation directly below it +(lines 449–488) instead say: + +> types without a sentinel (BOOL/U8/F32) fall back to the aux[0]&1 bit written by ray_typed_null. + +and: + +> SYM has no null — sym 0 is the empty symbol ' (a value) ... A SYM atom is never null. + +> STR has no null distinct from "" ... A STR atom is never null — the empty string is a value. + +The function also has a dedicated `RAY_F32` NaN branch before the fallback. + +## Why this is a defect + +The same public section assigns incompatible null encodings to SYM, STR, BOOL, +U8, and F32. Callers using the declared sentinels can therefore disagree with +the header's own public predicate about whether a value is null. + +## How to verify the fix + +For every scalar type named in the conflicting comments, construct its ordinary +zero/empty value and `ray_typed_null(type)`, then assert `RAY_ATOM_IS_NULL` on +both. The test results, sentinel definitions, and all comments in the section +must describe the same matrix with no exception left implicit. + +## Validation + +Validated. The conflicting comments and `NULL_F32` definition remain. +`ray_typed_null` and `ray_atom_is_null_fn` confirm that F32 uses NaN, BOOL/U8 +use the aux bit, and SYM/STR ordinary empty values are not null. Existing tests +cover numeric sentinels but not the full public matrix named by the finding. + +## Census + +A project-wide search for statements assigning null status to empty SYM/STR +values or denying F32's sentinel found fifteen conflicting statements across +the public header, implementation comments, documentation, and tests. The full +census and ratification requirement are recorded in CF-0003. This finding is +superseded pending triage of that class finding. + +## Objection + + +## Remediation + +Superseded by CF-0003 during RP-0001; no point fix was applied. + + +## Verification diff --git a/audit/findings/F-0005-time-atom-documentation-uses-wrong-unit.md b/audit/findings/F-0005-time-atom-documentation-uses-wrong-unit.md new file mode 100644 index 00000000..8ce7b4f7 --- /dev/null +++ b/audit/findings/F-0005-time-atom-documentation-uses-wrong-unit.md @@ -0,0 +1,80 @@ +--- +id: F-0005 +title: Time atom documentation uses nanoseconds for a millisecond value +severity: major +dimension: api-doc-consistency +unit: + - docs/docs/c-api/core.md + - include/rayforce.h + - test/test_public_api.c +status: fixed +class: null +attempts: 0 +pass: P-01 +updated: 2026-07-22 +--- + +## Evidence + +`docs/docs/c-api/core.md`, section “ray_time” (lines 197–203), specifies: + +> Creates a time atom. The value is nanoseconds since midnight. + +`test/test_public_api.c`, `test_public_vec_get_i64_time` (lines 273–280), uses +values explicitly labelled as wall-clock times: + +```c +int32_t xs[] = { 0, 43200000, 86399000 }; /* 00:00:00.000, 12:00:00.000, 23:59:59.000 */ +``` + +Those values are milliseconds since midnight, not nanoseconds. The same test's +section comment (lines 256–258) and the public null predicate in +`include/rayforce.h` (lines 465–467) also establish that `RAY_TIME` uses a +32-bit `i32` representation; a full day in nanoseconds cannot fit in that +representation. + +## Why this is a defect + +The public constructor documentation scales every time value by the wrong +factor of one million. An embedder following it cannot represent ordinary +times in the declared 32-bit storage and will create incorrect values. + +## How to verify the fix + +Re-read the `ray_time` section and run a public API test constructing noon and +23:59:59.000 with the documented unit. Expected values must match the existing +32-bit executable contract (`43200000` and `86399000` for milliseconds), and +the documentation must name that unit exactly. + +## Validation + +Validated. The API page still says nanoseconds. `src/lang/format.c` converts the +stored `int32_t` value by dividing by 1000, `src/ops/temporal.c` names the unit +as milliseconds, and the public API test uses `43200000` for noon. The runtime +contract is milliseconds. + +## Census + +Pattern: a contract for `RAY_TIME` or `ray_time` names a unit other than +milliseconds since midnight. Searched with +`rg -n -i '(RAY_TIME|time atom|time-of-day).*\b(nanosecond|microsecond|millisecond)|\b(nanosecond|microsecond|millisecond)s? since midnight' +include src docs test README.md examples fuzz bench`. Only +`docs/docs/c-api/core.md:199` says nanoseconds; the type guide, data-type guide, +syntax reference, implementation comments, and tests consistently say +milliseconds. One instance is below threshold. + +## Objection + + +## Remediation + +Changed `docs/docs/c-api/core.md`, section `ray_time`, to specify milliseconds +since midnight. Strengthened `test_public_vec_get_i64_time` in +`test/test_public_api.c` by constructing `ray_time(43200000)` and asserting its +public formatted value is `12:00:00.000`. + +Self-check: the stale “nanoseconds since midnight” text is absent, the focused +public TIME test passed, and `make test` passed 3638/3638 under ASan/UBSan. + + +## Verification diff --git a/audit/findings/F-0006-public-api-test-redeclares-runtime-functions.md b/audit/findings/F-0006-public-api-test-redeclares-runtime-functions.md new file mode 100644 index 00000000..0ec30df4 --- /dev/null +++ b/audit/findings/F-0006-public-api-test-redeclares-runtime-functions.md @@ -0,0 +1,79 @@ +--- +id: F-0006 +title: Public API test redeclares runtime functions instead of testing the header +severity: minor +dimension: api-doc-consistency +unit: + - test/test_public_api.c + - include/rayforce.h +status: superseded-by-class +class: CF-0004 +attempts: 0 +pass: P-01 +updated: 2026-07-22 +--- + +## Evidence + +`test/test_public_api.c` includes the public header at line 27: + +```c +#include +``` + +but then manually supplies the public runtime declarations in the file-scope +setup block (lines 34–40): + +```c +struct ray_runtime_s; +typedef struct ray_runtime_s ray_runtime_t; +extern ray_runtime_t* ray_runtime_create(int argc, char** argv); +extern void ray_runtime_destroy(ray_runtime_t* rt); +``` + +The declarations the test is meant to guard already exist in +`include/rayforce.h` (lines 644–664): + +```c +typedef struct ray_runtime_s ray_runtime_t; +ray_runtime_t* ray_runtime_create(int argc, char** argv); +void ray_runtime_destroy(ray_runtime_t* rt); +``` + +## Why this is a defect + +The “public API” test would continue compiling if those runtime declarations +were accidentally removed from the public header, because the test silently +recreates them. It therefore does not guard the public declaration contract it +appears to exercise. + +## How to verify the fix + +Remove the local runtime type/function redeclarations and compile the public API +test using only `` for those symbols. It must pass. As an adversarial +guard check in a temporary copy, hide `ray_runtime_create` from the public +header and confirm compilation then fails. + +## Validation + +Validated. `test/test_public_api.c` still includes `` and then +redeclares `ray_runtime_t`, `ray_runtime_create`, and `ray_runtime_destroy`. +The same declarations remain in the public header, so the local declarations +still mask a header-removal regression. + +## Census + +The two-stage project search recorded in CF-0004 found sixteen files that +directly include `rayforce.h` and then repeat at least one public runtime type +or function declaration. Sixteen exceeds the threshold, so this finding is +superseded by CF-0004 pending human triage. + +## Objection + + +## Remediation + +Superseded by CF-0004 during RP-0001; no point fix was applied. + + +## Verification diff --git a/audit/findings/F-0007-date-epoch-comment-conflicts-with-api-reference.md b/audit/findings/F-0007-date-epoch-comment-conflicts-with-api-reference.md new file mode 100644 index 00000000..f5db554a --- /dev/null +++ b/audit/findings/F-0007-date-epoch-comment-conflicts-with-api-reference.md @@ -0,0 +1,77 @@ +--- +id: F-0007 +title: Public date test labels a different epoch than the API reference +severity: minor +dimension: api-doc-consistency +unit: + - docs/docs/c-api/core.md + - test/test_public_api.c +status: fixed +class: null +attempts: 0 +pass: P-01 +updated: 2026-07-22 +--- + +## Evidence + +`docs/docs/c-api/core.md`, section “ray_date” (lines 189–195), defines the norm: + +> Creates a date atom. The value is an integer representing days since epoch (2000-01-01). + +`test/test_public_api.c`, `test_public_vec_get_i64_date` (lines 260–268), labels +the raw values using a Unix epoch instead: + +```c +int32_t xs[] = { 0, 8766, 19724 }; /* 1970.01.01, 1994.01.01, 2024.01.01 */ +``` + +The two passages are an N13 internal-consistency contradiction: in the API +reference, day zero is 2000-01-01; in the public API test comment, day zero is +1970-01-01. + +## Why this is a defect + +The accessor test's misleading semantic labels can cause future tests and API +examples to encode dates against the wrong epoch, even though the current test +only asserts raw integer round-trips. + +## How to verify the fix + +Search both units for the quoted epoch/date labels and confirm they name one +epoch consistently. Add or run a focused semantic test in which day zero is +formatted or otherwise converted to the documented calendar date, rather than +only round-tripped as an integer. + +## Validation + +Validated. The contradictory 1970 and 2000 labels remain. The calendar helpers +and temporal implementation use 2000-01-01 as the Rayforce epoch, so the API +reference is correct and the public test's inline calendar labels are stale. + +## Census + +Pattern: a `RAY_DATE` raw-value comment or contract labels day zero as 1970 +rather than 2000-01-01. Searched date contracts and semantic tests with +`rg -n -i '(RAY_DATE|date atom|date value|date).*\b(1970|2000-01-01|days since|epoch)|\b1970\.01\.01' +include src docs test README.md examples fuzz bench`. Only +`test/test_public_api.c:264` uses the 1970 label; implementation, API reference, +and semantic tests consistently use 2000-01-01. One instance is below +threshold. + +## Objection + + +## Remediation + +Corrected the calendar labels beside the raw values in +`test_public_vec_get_i64_date` to the 2000-01-01 epoch: day 0 is 2000.01.01, +day 8766 is 2024.01.01, and day 19724 is 2054.01.01. Added a public semantic +check that formats `ray_date(0)` and requires `2000.01.01`, while retaining all +raw accessor assertions. + +Self-check: the stale 1970/1994 labels are absent, the focused public DATE test +passed, and `make test` passed 3638/3638 under ASan/UBSan. + + +## Verification diff --git a/audit/passes/P-01-api-doc-consistency.md b/audit/passes/P-01-api-doc-consistency.md new file mode 100644 index 00000000..6a8f7446 --- /dev/null +++ b/audit/passes/P-01-api-doc-consistency.md @@ -0,0 +1,70 @@ +--- +id: P-01 +dimension: api-doc-consistency +units: + - include/rayforce.h + - docs/docs/c-api/core.md + - test/test_public_api.c +status: done +findings: + - F-0001 + - F-0002 + - F-0003 + - F-0004 + - F-0005 + - F-0006 + - F-0007 +updated: 2026-07-22 +--- + +## Charter + +Compare the public header, published Core C API reference, and public API test +for declaration, ownership, error, and lifecycle consistency. Stop when all +three units are exhausted, 15 findings are filed, or context is exhausted. + +## Coverage + +COVERED: + +- `include/rayforce.h`, lines 1–770: all public declarations and comments were + read attentively, with focused comparison of lifecycle, ownership, errors, + null representation, temporal units, and declarations exercised by the + public API test. +- `docs/docs/c-api/core.md`, lines 1–517: every section, declaration, example, + ownership statement, lifecycle instruction, temporal description, and error + table was compared against the public header and applicable test evidence. +- `test/test_public_api.c`, lines 1–662: all setup/teardown paths, public symbol + guards, accessor cases, runtime lifecycle tests, interrupt/restricted-mode + tests, and error cleanup paths were examined. +- A temporary extraction of the documented lifecycle pattern was compiled with + `cc -std=c17 -Werror -fsyntax-only -Iinclude`; it failed on the two undeclared + heap lifecycle calls, corroborating F-0001. +- A mechanical `ray_*` symbol comparison and literal-anchor search was run + across all three units to check omissions and re-find every filed quote. + +NOT COVERED: + +- Nothing inside the three-unit charter was skipped. +- Implementations outside the charter (including `src/core/runtime.c`, + `src/mem/cow.c`, and `src/vec/atom.c`) were not opened. Runtime behavior was + not inferred from those files; findings rely on contradictions among the + scoped public contracts and executable-test material. +- The full test suite was not run because this pass audits API consistency, not + implementation correctness; each finding supplies its own focused procedure + for remediation self-check and independent verification. + +## Findings + +- F-0001 — Core lifecycle example does not compile against its documented header +- F-0002 — Documentation and public API tests release errors with a no-op helper +- F-0003 — ray_eval_str null-return contract contradicts the public value invariant +- F-0004 — Public null-sentinel contract contradicts its own predicates +- F-0005 — Time atom documentation uses nanoseconds for a millisecond value +- F-0006 — Public API test redeclares runtime functions instead of testing the header +- F-0007 — Public date test labels a different epoch than the API reference + +## Handoff + +No leftover P-01 scope and no split pass. All seven findings are `reported` and +await human triage. P-02 remains the next queued audit pass. diff --git a/audit/plans/RP-0001.md b/audit/plans/RP-0001.md new file mode 100644 index 00000000..57907c6c --- /dev/null +++ b/audit/plans/RP-0001.md @@ -0,0 +1,82 @@ +--- +id: RP-0001 +findings: + - F-0001 + - F-0002 + - F-0003 + - F-0004 + - F-0005 + - F-0006 + - F-0007 +status: done +updated: 2026-07-22 +--- + +## Batch validation results + +- F-0001: validated. The documented lifecycle snippet still fails C17/Werror + syntax checking because it calls internal heap functions without their + header; the public runtime lifecycle is declared and tested. +- F-0002: validated. All three cited calls remain and `ray_release` still + returns immediately for an error object. +- F-0003: validated. The public comment still says bare NULL while the + evaluator and focused test use `RAY_NULL_OBJ`. +- F-0004: validated. The null-contract comments still contradict the public + predicate and typed-null implementation. +- F-0005: validated. The C API page says nanoseconds while the runtime, + formatter, and tests use milliseconds. +- F-0006: validated. The public API test still repeats declarations already + supplied by `rayforce.h`. +- F-0007: validated. The public test still labels the raw date values with a + 1970 epoch while the implementation and reference use 2000-01-01. + +## Census results + +- F-0001: one defective standalone lifecycle example. Project-wide review of + examples using `ray_heap_init`/`ray_heap_destroy` found the README and DAG + examples include `mem/heap.h`; the core “Typical lifecycle pattern” does + not. Below threshold; proceed with the point fix. +- F-0002: four public-contract instances. Escalated to CF-0001; F-0002 is + superseded-by-class and the CF returns to human triage. +- F-0003: three comments claim successful null evaluation returns bare NULL. + Escalated to CF-0002; F-0003 is superseded-by-class. +- F-0004: fifteen conflicting null-encoding statements. Escalated to CF-0003; + F-0004 is superseded-by-class and its norm conflict requires ratification. +- F-0005: one incorrect TIME-unit statement; all searched sibling contracts + say milliseconds. Below threshold; proceed with the point fix. +- F-0006: sixteen files include the public header and then redeclare its + runtime API. Escalated to CF-0004; F-0006 is superseded-by-class. +- F-0007: one 1970-epoch label; searched date contracts and semantic tests use + the 2000-01-01 epoch. Below threshold; proceed with the point fix. + +## Fix plan + +- F-0001: replace the internal heap/symbol lifecycle in the canonical snippet + with `ray_runtime_create`/`ray_runtime_destroy`, check creation failure, and + syntax-check the extracted snippet against only `rayforce.h`. Risk: the page + still documents internal lifecycle functions elsewhere; keep that material + explicitly identified as internal rather than silently changing its scope. +- F-0005: change the `ray_time` reference unit from nanoseconds to + milliseconds and strengthen the public accessor test with a constructed noon + atom so the documented unit is executable. Risk: avoid changing TIMESTAMP's + separate nanosecond contract. +- F-0007: correct the test values/calendar labels to the 2000 epoch and add a + semantic day-zero assertion through public formatting. Risk: keep the raw + accessor round-trip coverage intact. + +F-0002, F-0003, F-0004, and F-0006 receive no project edits in this session; +their newly reported CFs must pass triage first. + +## Execution log + +- F-0001: revised `docs/docs/c-api/core.md` to use the public runtime + lifecycle. Extracted snippet passed C17/Werror syntax checking. +- F-0005: corrected the TIME unit in `docs/docs/c-api/core.md` and added a + noon-format assertion in `test/test_public_api.c`. +- F-0007: corrected the date epoch labels and added a day-zero format assertion + in `test/test_public_api.c`. +- F-0002, F-0003, F-0004, F-0006: opened CF-0001 through CF-0004 respectively + and made no project edits for those classes pending triage. +- Batch self-check: `make test` completed with 3638/3638 passing and no + failures; the lifecycle compile probe exited 0; stale TIME/DATE strings were + absent on re-search. diff --git a/audit/templates/class-finding.md b/audit/templates/class-finding.md new file mode 100644 index 00000000..7d698a65 --- /dev/null +++ b/audit/templates/class-finding.md @@ -0,0 +1,53 @@ +--- +id: CF-XXXX +title: # short title of the systemic defect (operator's working language) +severity: # highest severity among members +dimension: # dimension key from AUDIT.md +unit: [] # YAML list of every unit the class spans (from the census); canonical field per §2 +status: reported +class: null # a CF is itself the class — it is not absorbed by another class +members: [] # F-XXXX ids absorbed by this class (they get status superseded-by-class) +attempts: 0 +pass: P-XX # the pass whose finding triggered the census escalation (— if surfaced purely in remediation) +created-by: RP-XXXX # the remediation session whose census escalated +updated: YYYY-MM-DD +--- + +## Pattern + + +## Census + + +## Validation + + +## Root cause + + +## Global strategy + + +## Remediation + + +## Verification + diff --git a/audit/templates/finding.md b/audit/templates/finding.md new file mode 100644 index 00000000..3e72dcca --- /dev/null +++ b/audit/templates/finding.md @@ -0,0 +1,46 @@ +--- +id: F-XXXX +title: # short title of the defect (operator's working language) +severity: # critical | major | minor | info +dimension: # one dimension key from AUDIT.md +unit: # material unit; YAML list if the finding spans units +status: reported +class: null # CF-XXXX once absorbed by a class finding +attempts: 0 # the Remediator increments this when re-taking a reopened finding +pass: P-XX # the audit pass that produced this finding +updated: YYYY-MM-DD +--- + +## Evidence + + +## Why this is a defect + + +## How to verify the fix + + +## Validation + + +## Objection + + +## Remediation + + +## Verification + diff --git a/audit/templates/pass.md b/audit/templates/pass.md new file mode 100644 index 00000000..20ed97e0 --- /dev/null +++ b/audit/templates/pass.md @@ -0,0 +1,28 @@ +--- +id: P-XX +dimension: # one dimension key from AUDIT.md +units: [] # material units this pass covers +status: queued # queued | done | split +findings: [] # F-XXXX ids produced +updated: YYYY-MM-DD +--- + +## Charter + + +## Coverage + + +## Findings + + +## Handoff + diff --git a/audit/templates/plan.md b/audit/templates/plan.md new file mode 100644 index 00000000..24118c66 --- /dev/null +++ b/audit/templates/plan.md @@ -0,0 +1,36 @@ +--- +id: RP-XXXX +findings: [] # accepted F-XXXX and/or CF-XXXX ids in this batch (≤ remediation_batch_size); + # an accepted CF is a batch of its own (§8) +status: open # open | done +updated: YYYY-MM-DD +--- + +## Batch validation results + + +## Census results + + +## Fix plan + + +## Execution log + diff --git a/docs/docs/c-api/core.md b/docs/docs/c-api/core.md index e1511b96..43eec277 100644 --- a/docs/docs/c-api/core.md +++ b/docs/docs/c-api/core.md @@ -44,14 +44,13 @@ void ray_sym_destroy(void); ```c #include -int main(void) { - ray_heap_init(); - ray_sym_init(); +int main(int argc, char** argv) { + ray_runtime_t* runtime = ray_runtime_create(argc, argv); + if (!runtime) return 1; /* ... use Rayforce API ... */ - ray_sym_destroy(); - ray_heap_destroy(); + ray_runtime_destroy(runtime); return 0; } ``` @@ -196,7 +195,7 @@ ray_t* ray_date(int64_t val); ### ray_time -Creates a time atom. The value is nanoseconds since midnight. +Creates a time atom. The value is milliseconds since midnight. ```c ray_t* ray_time(int64_t val); diff --git a/test/test_public_api.c b/test/test_public_api.c index f41d434f..09430868 100644 --- a/test/test_public_api.c +++ b/test/test_public_api.c @@ -261,11 +261,20 @@ static test_result_t test_public_vec_get_i64_date(void) { ray_t* v = ray_vec_new(RAY_DATE, 3); /* Pick three distinct int32 day values that differ in both halves so * a wrong-width read would catch obviously-wrong adjacent bytes. */ - int32_t xs[] = { 0, 8766, 19724 }; /* 1970.01.01, 1994.01.01, 2024.01.01 */ + int32_t xs[] = { 0, 8766, 19724 }; /* 2000.01.01, 2024.01.01, 2054.01.01 */ for (int i = 0; i < 3; i++) v = ray_vec_append(v, &xs[i]); TEST_ASSERT_EQ_I(ray_vec_get_i64(v, 0), xs[0]); TEST_ASSERT_EQ_I(ray_vec_get_i64(v, 1), xs[1]); TEST_ASSERT_EQ_I(ray_vec_get_i64(v, 2), xs[2]); + + ray_t* epoch = ray_date(xs[0]); + ray_t* formatted = ray_fmt(epoch, 0); + TEST_ASSERT_NOT_NULL(formatted); + TEST_ASSERT_FALSE(RAY_IS_ERR(formatted)); + TEST_ASSERT_EQ_I(ray_str_len(formatted), 10); + TEST_ASSERT_MEM_EQ(10, ray_str_ptr(formatted), "2000.01.01"); + ray_release(formatted); + ray_release(epoch); ray_release(v); PASS(); } @@ -277,6 +286,15 @@ static test_result_t test_public_vec_get_i64_time(void) { TEST_ASSERT_EQ_I(ray_vec_get_i64(v, 0), xs[0]); TEST_ASSERT_EQ_I(ray_vec_get_i64(v, 1), xs[1]); TEST_ASSERT_EQ_I(ray_vec_get_i64(v, 2), xs[2]); + + ray_t* noon = ray_time(xs[1]); + ray_t* formatted = ray_fmt(noon, 0); + TEST_ASSERT_NOT_NULL(formatted); + TEST_ASSERT_FALSE(RAY_IS_ERR(formatted)); + TEST_ASSERT_EQ_I(ray_str_len(formatted), 12); + TEST_ASSERT_MEM_EQ(12, ray_str_ptr(formatted), "12:00:00.000"); + ray_release(formatted); + ray_release(noon); ray_release(v); PASS(); } From ffd784f4831ac14073b150b82731930a04a40c62 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 22 Jul 2026 11:54:10 +0200 Subject: [PATCH 10/14] fix(docs): remediate CF-0001 --- audit/LEDGER.md | 8 ++-- ...1-public-error-cleanup-uses-ray-release.md | 32 +++++++++++++- audit/plans/RP-0002.md | 42 +++++++++++++++++++ docs/docs/c-api/core.md | 2 +- docs/docs/c-api/dag.md | 2 +- test/test_public_api.c | 4 +- 6 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 audit/plans/RP-0002.md diff --git a/audit/LEDGER.md b/audit/LEDGER.md index 130a4e08..e2b8fd50 100644 --- a/audit/LEDGER.md +++ b/audit/LEDGER.md @@ -7,7 +7,7 @@ | F-0005 | Time atom documentation uses nanoseconds for a millisecond value | major | fixed | Verifier | 2026-07-22 | | F-0006 | Public API test redeclares runtime functions instead of testing the header | minor | superseded-by-class | — | 2026-07-22 | | F-0007 | Public date test labels a different epoch than the API reference | minor | fixed | Verifier | 2026-07-22 | -| CF-0001 | Public error-cleanup examples use ray_release on owned errors | major | reported | human | 2026-07-22 | -| CF-0002 | ray_eval_str comments still claim value-null is bare NULL | major | reported | human | 2026-07-22 | -| CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | reported | human | 2026-07-22 | -| CF-0004 | Runtime API is redeclared after including the public header | minor | reported | human | 2026-07-22 | +| CF-0001 | Public error-cleanup examples use ray_release on owned errors | major | fixed | Verifier | 2026-07-22 | +| CF-0002 | ray_eval_str comments still claim value-null is bare NULL | major | accepted | human | 2026-07-22 | +| CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | accepted | human | 2026-07-22 | +| CF-0004 | Runtime API is redeclared after including the public header | minor | accepted | human | 2026-07-22 | diff --git a/audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md b/audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md index 665c0089..fef684dc 100644 --- a/audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md +++ b/audit/findings/CF-0001-public-error-cleanup-uses-ray-release.md @@ -7,7 +7,7 @@ unit: - docs/docs/c-api/core.md - docs/docs/c-api/dag.md - test/test_public_api.c -status: reported +status: fixed class: null members: - F-0002 @@ -50,6 +50,19 @@ Four instances meet the project threshold of three. ## Validation +Re-ran the recorded census on 2026-07-22: + +```sh +rg -n -C 12 'ray_release\((result|err)\)' docs/docs/c-api test/test_public_api.c +``` + +All four recorded instances remain eligible: each value is proved to be a +`RAY_ERROR` before cleanup. `include/rayforce.h` documents +`ray_error_free` as the owned-error cleanup and `src/mem/cow.c` confirms that +`ray_release` deliberately returns without freeing errors. The trace returned +by `ray_get_error_trace` is separately owned by the VM, so freeing the error +result does not invalidate either public trace test. + ## Root cause @@ -65,5 +78,22 @@ require zero public-contract instances. ## Remediation +Planned in `audit/plans/RP-0002.md` using the accepted rung-(a) global +strategy. No ownership norm changes or conflicts require ratification. + +Replaced all four eligible cleanup calls with `ray_error_free`: the +`RAY_IS_ERR` examples in `docs/docs/c-api/core.md` and +`docs/docs/c-api/dag.md`, plus both error-trace cases in +`test/test_public_api.c`. + +Self-checks completed on 2026-07-22: + +- The recorded census returned only three success-path `ray_release(result)` + calls guarded by `!RAY_IS_ERR`; zero eligible class instances remain. +- `make test` passed 3638 of 3638 tests with 0 skipped and 0 failed. +- `mkdocs build --strict` completed successfully. + ## Verification + +Pending independent Verifier review. diff --git a/audit/plans/RP-0002.md b/audit/plans/RP-0002.md new file mode 100644 index 00000000..25798699 --- /dev/null +++ b/audit/plans/RP-0002.md @@ -0,0 +1,42 @@ +--- +id: RP-0002 +findings: + - CF-0001 +status: done +updated: 2026-07-22 +--- + +## Batch validation results + +- CF-0001: validated. Re-running the recorded census reproduced all four + eligible public-contract instances. The header and implementation confirm + that `ray_release` is a no-op for errors and `ray_error_free` is the owned + error cleanup. + +## Census results + +- CF-0001: the accepted class census is the batch scope. Its recorded search + found four eligible instances across the public C API reference and + public-header conformance test; re-running it reproduced the same four. + +## Fix plan + +- CF-0001: apply the accepted rung-(a) strategy by replacing each eligible + `ray_release(result)` or `ray_release(err)` call with `ray_error_free` in + `docs/docs/c-api/core.md`, `docs/docs/c-api/dag.md`, and + `test/test_public_api.c`. Then re-run the census, execute the public API test + through the project test suite, and rebuild the documentation strictly. + Risk: an error might be referenced by error-trace state, but inspection + confirms the trace is a separate VM-owned list and the next evaluation + clears it independently. + +## Execution log + +- CF-0001: replaced four erroneous `ray_release` cleanup calls with + `ray_error_free` across the two public API reference examples and two + public-header conformance tests. +- Census self-check: the recorded search has zero eligible error-cleanup + instances; its remaining three matches are success paths explicitly guarded + by `!RAY_IS_ERR`. +- Test self-check: `make test` passed 3638/3638 with no skips or failures. +- Documentation self-check: `mkdocs build --strict` completed successfully. diff --git a/docs/docs/c-api/core.md b/docs/docs/c-api/core.md index 43eec277..846b1faf 100644 --- a/docs/docs/c-api/core.md +++ b/docs/docs/c-api/core.md @@ -488,7 +488,7 @@ Macro. Tests whether a `ray_t*` is an error object. Safe to call on `NULL`. Retu ray_t* result = ray_execute(g, root); if (RAY_IS_ERR(result)) { printf("Error: %s\n", ray_err_code(result)); - ray_release(result); + ray_error_free(result); return 1; } ``` diff --git a/docs/docs/c-api/dag.md b/docs/docs/c-api/dag.md index 47334004..5d6d6180 100644 --- a/docs/docs/c-api/dag.md +++ b/docs/docs/c-api/dag.md @@ -372,7 +372,7 @@ ray_t* ray_execute(ray_graph_t* g, ray_op_t* root); ray_t* result = ray_execute(g, ray_optimize(g, root)); if (RAY_IS_ERR(result)) { /* handle error */ - ray_release(result); + ray_error_free(result); } ``` diff --git a/test/test_public_api.c b/test/test_public_api.c index 09430868..7270887f 100644 --- a/test/test_public_api.c +++ b/test/test_public_api.c @@ -607,7 +607,7 @@ static test_result_t test_public_get_error_trace_populated(void) { TEST_ASSERT_EQ_I(frame0->type, RAY_LIST); TEST_ASSERT_EQ_I(ray_len(frame0), 4); - ray_release(err); + ray_error_free(err); PASS(); } @@ -619,7 +619,7 @@ static test_result_t test_public_get_error_trace_cleared_on_eval(void) { ray_t* err = ray_eval_str("(boom2 \"x\")"); TEST_ASSERT_TRUE(RAY_IS_ERR(err)); TEST_ASSERT_NOT_NULL(ray_get_error_trace()); - ray_release(err); + ray_error_free(err); ray_t* ok = ray_eval_str("(+ 10 20)"); TEST_ASSERT_NOT_NULL(ok); From 1f27e61652c71e492b6f8a302367d9a7c1226ab0 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 22 Jul 2026 12:00:57 +0200 Subject: [PATCH 11/14] fix(docs): remediate CF-0002 --- audit/LEDGER.md | 2 +- ...F-0002-ray-eval-str-null-contract-stale.md | 37 ++++++++++++++- audit/plans/RP-0003.md | 47 +++++++++++++++++++ include/rayforce.h | 8 ++-- test/main.c | 5 +- test/stress_eval.c | 4 +- 6 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 audit/plans/RP-0003.md diff --git a/audit/LEDGER.md b/audit/LEDGER.md index e2b8fd50..6caee386 100644 --- a/audit/LEDGER.md +++ b/audit/LEDGER.md @@ -8,6 +8,6 @@ | F-0006 | Public API test redeclares runtime functions instead of testing the header | minor | superseded-by-class | — | 2026-07-22 | | F-0007 | Public date test labels a different epoch than the API reference | minor | fixed | Verifier | 2026-07-22 | | CF-0001 | Public error-cleanup examples use ray_release on owned errors | major | fixed | Verifier | 2026-07-22 | -| CF-0002 | ray_eval_str comments still claim value-null is bare NULL | major | accepted | human | 2026-07-22 | +| CF-0002 | ray_eval_str comments still claim value-null is bare NULL | major | fixed | Verifier | 2026-07-22 | | CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | accepted | human | 2026-07-22 | | CF-0004 | Runtime API is redeclared after including the public header | minor | accepted | human | 2026-07-22 | diff --git a/audit/findings/CF-0002-ray-eval-str-null-contract-stale.md b/audit/findings/CF-0002-ray-eval-str-null-contract-stale.md index 02a75b35..7a5a28ce 100644 --- a/audit/findings/CF-0002-ray-eval-str-null-contract-stale.md +++ b/audit/findings/CF-0002-ray-eval-str-null-contract-stale.md @@ -7,7 +7,7 @@ unit: - include/rayforce.h - test/main.c - test/stress_eval.c -status: reported +status: fixed class: null members: - F-0003 @@ -43,6 +43,21 @@ Three instances meet the project threshold of three. ## Validation +Re-ran the recorded census on 2026-07-22. It reproduced the two single-line +harness comments but missed the public-header instance because `ray_eval_str` +and “Returns NULL” are on separate lines. A supplementary exact-literal +census reproducibly finds all three recorded instances: + +```sh +rg -n -i 'Returns NULL for void / null results|ray_eval_str returns NULL for void results|NULL = void/null result' \ + include src docs test README.md examples fuzz bench +``` + +The public invariant in `include/rayforce.h` states that value-null is always +`RAY_NULL_OBJ`; `test_eval_println` and `test_eval_null_keyword` assert that +behavior directly. All three stale comments therefore remain substantively +confirmed. + ## Root cause @@ -60,5 +75,25 @@ test and the recorded census. ## Remediation +Planned in `audit/plans/RP-0003.md` using the human-accepted rung-(b) +strategy. The direction is the existing public `RAY_NULL_OBJ` invariant; no +new or different norm choice is introduced. + +Updated the public `ray_eval_str` contract in `include/rayforce.h` and the +harness comments in `test/main.c` and `test/stress_eval.c` to identify +`RAY_NULL_OBJ` as the successful void/null result. Defensive bare-NULL paths +remain unchanged and are described only as no-result fallbacks. + +Self-checks completed on 2026-07-22: + +- The supplementary exact-literal census returned zero stale instances. +- The original regex returned one documented non-defect exception: the + corrected `test/main.c` comment, because its broad `void results` alternative + also matches `RAY_NULL_OBJ` wording. +- Focused tests `lang/eval/println` and `lang/eval/null_keyword` each passed + 1/1 with no skips or failures. + ## Verification + +Pending independent Verifier review. diff --git a/audit/plans/RP-0003.md b/audit/plans/RP-0003.md new file mode 100644 index 00000000..65d53339 --- /dev/null +++ b/audit/plans/RP-0003.md @@ -0,0 +1,47 @@ +--- +id: RP-0003 +findings: + - CF-0002 +status: done +updated: 2026-07-22 +--- + +## Batch validation results + +- CF-0002: validated. The two harness comments are found by the recorded + regex; the multiline public-header comment requires the supplementary + exact-literal census recorded in the finding. Runtime-focused tests assert + that successful null evaluation returns `RAY_NULL_OBJ`. + +## Census results + +- CF-0002: the accepted class census is the batch scope. The corrected + supplementary search reproduces all three recorded instances across + `include/rayforce.h`, `test/main.c`, and `test/stress_eval.c`. + +## Fix plan + +- CF-0002: apply the accepted rung-(b) strategy. Name `RAY_NULL_OBJ` as the + successful void/null result in the public `ray_eval_str` contract and both + harness comments. Preserve the harnesses' defensive bare-NULL branches, but + describe bare NULL only as a defensive allocation-failure/no-result case, + never as successful value-null. Then run the focused null-evaluation tests + and require both census procedures to return zero stale instances. +- Norm ratification: the human accepted CF-0002 after its Global strategy + explicitly selected the existing `RAY_NULL_OBJ` public invariant as the + source of truth. That triage decision ratifies this rung-(b) direction; the + plan introduces no different norm choice. +- Risk: comments must not overstate the defensive bare-NULL paths as ordinary + successful evaluation results or change executable harness behavior. + +## Execution log + +- CF-0002: aligned the public header and both harness comments with the + `RAY_NULL_OBJ` successful-null contract while preserving defensive bare-NULL + branches unchanged. +- Census self-check: the supplementary stale-literal search returned zero. + The original overbroad regex has one documented corrected-comment exception + in `test/main.c` because it matches any `ray_eval_str` comment containing + “void results.” +- Focused self-check: `lang/eval/println` and `lang/eval/null_keyword` each + passed 1/1 with no skips or failures. diff --git a/include/rayforce.h b/include/rayforce.h index 6a722d54..b67ffd6b 100644 --- a/include/rayforce.h +++ b/include/rayforce.h @@ -664,10 +664,10 @@ ray_runtime_t* ray_runtime_create_with_sym_err(const char* sym_path, void ray_runtime_destroy(ray_runtime_t* rt); /* Parse and evaluate a Rayfall source string against the global env. - * Returns NULL for void / null results, an error ray_t* on failure - * (test with RAY_IS_ERR and inspect with ray_err_code), or the result - * value otherwise. Caller owns the returned reference; release with - * ray_release. */ + * Returns RAY_NULL_OBJ for successful void / null results, an error ray_t* + * on failure (test with RAY_IS_ERR and inspect with ray_err_code), or the + * result value otherwise. Caller owns the returned reference; release + * with ray_release. */ ray_t* ray_eval_str(const char* source); ray_t* ray_select(ray_t** args, int64_t n); diff --git a/test/main.c b/test/main.c index c29406b2..095d7ec6 100644 --- a/test/main.c +++ b/test/main.c @@ -244,8 +244,9 @@ static int g_rfl_count = 0; */ static int fmt_eq(ray_t* a, ray_t* b) { - /* ray_eval_str returns NULL for void results (like evaluating `null`). - * Two NULLs format-equal; one NULL differs from anything else. */ + /* ray_eval_str returns RAY_NULL_OBJ for successful void results (like + * evaluating `null`). Bare C NULL is reserved for defensive no-result + * handling: two NULLs compare equal; one differs from anything else. */ if (a == NULL && b == NULL) return 1; if (a == NULL || b == NULL) return 0; ray_t* sa = ray_fmt(a, 0); diff --git a/test/stress_eval.c b/test/stress_eval.c index 19cfde24..4739d7f6 100644 --- a/test/stress_eval.c +++ b/test/stress_eval.c @@ -66,7 +66,9 @@ bool stress_eval_exec(stress_ctx_t* c, const char* src) { "not called?)"); return false; } - ray_t* r = ray_eval_str(src); /* NULL = void/null result, fine */ + /* Successful void/null evaluation returns RAY_NULL_OBJ; bare NULL remains + * a defensive no-result fallback in this stress harness. */ + ray_t* r = ray_eval_str(src); if (r && RAY_IS_ERR(r)) { stress_dump_failure(c, "eval error '%s' for: %.300s", ray_err_code(r), src); From 465e58e058af593b3d87129e47821b4a913075d7 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 22 Jul 2026 12:05:50 +0200 Subject: [PATCH 12/14] chore(audit): plan CF-0003 ratification --- audit/LEDGER.md | 2 +- ...CF-0003-null-encoding-comments-conflict.md | 30 ++++++++++- audit/plans/RP-0004.md | 50 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 audit/plans/RP-0004.md diff --git a/audit/LEDGER.md b/audit/LEDGER.md index 6caee386..c4b8a3b7 100644 --- a/audit/LEDGER.md +++ b/audit/LEDGER.md @@ -9,5 +9,5 @@ | F-0007 | Public date test labels a different epoch than the API reference | minor | fixed | Verifier | 2026-07-22 | | CF-0001 | Public error-cleanup examples use ray_release on owned errors | major | fixed | Verifier | 2026-07-22 | | CF-0002 | ray_eval_str comments still claim value-null is bare NULL | major | fixed | Verifier | 2026-07-22 | -| CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | accepted | human | 2026-07-22 | +| CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | planned | human | 2026-07-22 | | CF-0004 | Runtime API is redeclared after including the public header | minor | accepted | human | 2026-07-22 | diff --git a/audit/findings/CF-0003-null-encoding-comments-conflict.md b/audit/findings/CF-0003-null-encoding-comments-conflict.md index 8dfaafb6..51877737 100644 --- a/audit/findings/CF-0003-null-encoding-comments-conflict.md +++ b/audit/findings/CF-0003-null-encoding-comments-conflict.md @@ -16,7 +16,7 @@ unit: - test/test_str.c - test/rfl/integration/null.rfl - test/rfl/strop/split.rfl -status: reported +status: planned class: null members: - F-0004 @@ -71,6 +71,30 @@ Fifteen instances meet the project threshold of three. ## Validation +Re-ran the recorded candidate census on 2026-07-22 and manually classified +each result against `ray_atom_is_null_fn`, `ray_vec_is_null`, +`ray_vec_set_null_checked`, and the focused `nil?` expectations. All fifteen +recorded conflicting statements remain in their cited locations. The search +also returns correct counterexamples that explicitly say empty SYM/STR values +are not null; those are classification references, not defects. + +The current executable behavior is internally consistent enough to expose the +norm choice but not to authorize it: + +- F32 atoms and vectors use NaN as a null sentinel. +- SYM and STR atoms have no null distinct from their empty values; + `ray_typed_null` collapses to the ordinary empty value and + `RAY_ATOM_IS_NULL` returns false. +- SYM and STR vectors are likewise treated as non-nullable by + `ray_vec_is_null`; missing inputs collapse to empty payloads without + `RAY_ATTR_HAS_NULLS`. +- BOOL and U8 typed-null atoms use the auxiliary null bit, while BOOL and U8 + vectors reject null insertion. + +The conflict is therefore substantive and the finding is validated. Because +the Global strategy is rung (b) and explicitly requires the norm owner to +choose the cross-surface matrix, validation does not ratify either direction. + ## Root cause @@ -88,5 +112,9 @@ until triage records which semantics own SYM/STR empty values and F32. ## Remediation +Planned in `audit/plans/RP-0004.md`. Execution is paused at the mandatory +norm-ratification gate pending the human norm owner's explicit selection of +the SYM/STR and F32 cross-surface semantics. + ## Verification diff --git a/audit/plans/RP-0004.md b/audit/plans/RP-0004.md new file mode 100644 index 00000000..ab71d7b6 --- /dev/null +++ b/audit/plans/RP-0004.md @@ -0,0 +1,50 @@ +--- +id: RP-0004 +findings: + - CF-0003 +status: open +updated: 2026-07-22 +--- + +## Batch validation results + +- CF-0003: validated. The fifteen cited conflicting statements remain. + Implementation and focused tests expose a consistent current behavior, but + public and internal normative comments contradict it. + +## Census results + +- CF-0003: the accepted class census is the batch scope. Re-running the + recorded candidate search and manually classifying results reproduced all + fifteen cited conflicts plus correct counterexamples used as classification + references. + +## Fix plan + +- Mandatory norm-ratification gate: stop before project edits and ask the + human norm owner to select one cross-surface nullability matrix. +- Recommended matrix (preserve executable behavior): + - F32/F64 use NaN null sentinels for atoms and vectors. + - Integer and temporal types use their width-correct `NULL_*` sentinels for + atoms and vectors; GUID uses its all-zero sentinel. + - BOOL/U8 typed-null atoms use the auxiliary bit, while BOOL/U8 vectors are + non-nullable. + - SYM/STR atoms and vectors have no null distinct from their ordinary empty + values; missing SYM/STR inputs collapse to empty without + `RAY_ATTR_HAS_NULLS`. +- Alternative matrix: make empty SYM/STR genuinely null. This requires a + distinct representation and behavioral redesign so ordinary empty values + remain expressible; it cannot be implemented as comment cleanup alone. +- If the recommended matrix is ratified, write it into the public API contract, + correct all fifteen census comments/docs/tests without changing executable + behavior, add a focused public matrix test covering ordinary zero/empty and + `ray_typed_null` for every named scalar type, then run the focused null, + vector, CSV, dictionary, formatting, string, pivot, and stress tests and + reclassify the census to zero conflicts. +- If the alternative is selected, replace this plan with an implementation + plan for the new representation before touching the corpus. +- Risk: changing SYM/STR representation affects ingestion, storage, grouping, + dictionary lookup, formatting, and persistence. Comment-only normalization + is safe only after the preserve-behavior matrix is explicitly ratified. + +## Execution log From 8f8033c03f66aba89189ef5156235de943096e22 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 22 Jul 2026 12:18:15 +0200 Subject: [PATCH 13/14] fix(docs): remediate CF-0003 --- audit/LEDGER.md | 2 +- ...CF-0003-null-encoding-comments-conflict.md | 26 +++++++++++++--- audit/plans/RP-0004.md | 17 +++++++++- docs/docs/namespaces/csv.md | 2 +- include/rayforce.h | 30 ++++++++++++------ src/ops/pivot.c | 9 +++--- src/table/domain.h | 4 +-- src/vec/atom.c | 6 ++-- src/vec/vec.c | 9 +++--- test/rfl/integration/null.rfl | 2 +- test/rfl/strop/split.rfl | 5 ++- test/stress_store.c | 2 +- test/test_dict.c | 10 +++--- test/test_format.c | 11 +++---- test/test_public_api.c | 31 +++++++++++++++++++ test/test_str.c | 3 +- 16 files changed, 121 insertions(+), 48 deletions(-) diff --git a/audit/LEDGER.md b/audit/LEDGER.md index c4b8a3b7..46f43785 100644 --- a/audit/LEDGER.md +++ b/audit/LEDGER.md @@ -9,5 +9,5 @@ | F-0007 | Public date test labels a different epoch than the API reference | minor | fixed | Verifier | 2026-07-22 | | CF-0001 | Public error-cleanup examples use ray_release on owned errors | major | fixed | Verifier | 2026-07-22 | | CF-0002 | ray_eval_str comments still claim value-null is bare NULL | major | fixed | Verifier | 2026-07-22 | -| CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | planned | human | 2026-07-22 | +| CF-0003 | Null-encoding comments conflict across scalar and vector surfaces | major | fixed | Verifier | 2026-07-22 | | CF-0004 | Runtime API is redeclared after including the public header | minor | accepted | human | 2026-07-22 | diff --git a/audit/findings/CF-0003-null-encoding-comments-conflict.md b/audit/findings/CF-0003-null-encoding-comments-conflict.md index 51877737..8ecf42d7 100644 --- a/audit/findings/CF-0003-null-encoding-comments-conflict.md +++ b/audit/findings/CF-0003-null-encoding-comments-conflict.md @@ -16,7 +16,7 @@ unit: - test/test_str.c - test/rfl/integration/null.rfl - test/rfl/strop/split.rfl -status: planned +status: fixed class: null members: - F-0004 @@ -112,9 +112,27 @@ until triage records which semantics own SYM/STR empty values and F32. ## Remediation -Planned in `audit/plans/RP-0004.md`. Execution is paused at the mandatory -norm-ratification gate pending the human norm owner's explicit selection of -the SYM/STR and F32 cross-surface semantics. +The human norm owner ratified the recommended preserve-behavior matrix on +2026-07-22. Under `audit/plans/RP-0004.md`, the public contract in +`include/rayforce.h` now publishes the cross-surface matrix; all fifteen +censused statements in the header, implementation comments, documentation, +and tests now describe empty SYM/STR values and F32 consistently. The new +`public/nullability_matrix` test in `test/test_public_api.c` pins ordinary and +typed-null atom behavior for SYM, STR, BOOL, U8, and F32. Executable null +representation was not changed. + +Self-checks completed on 2026-07-22: + +- The recorded class census returned seven matches, all manually classified + as correct counterexamples that explicitly say empty SYM values are not + null; it returned zero conflicting statements. +- The ASan/UBSan debug build completed cleanly with warnings as errors. +- The full test suite passed 3,639/3,639 with zero skips and zero failures, + including `public/nullability_matrix` and every touched focused area. +- `mkdocs build --strict` completed successfully. +- `git diff --check` reported no whitespace errors. ## Verification + +Pending independent Verifier review. diff --git a/audit/plans/RP-0004.md b/audit/plans/RP-0004.md index ab71d7b6..fef405b4 100644 --- a/audit/plans/RP-0004.md +++ b/audit/plans/RP-0004.md @@ -2,7 +2,7 @@ id: RP-0004 findings: - CF-0003 -status: open +status: done updated: 2026-07-22 --- @@ -48,3 +48,18 @@ updated: 2026-07-22 is safe only after the preserve-behavior matrix is explicitly ratified. ## Execution log + +- 2026-07-22: the human norm owner explicitly ratified the recommended + preserve-behavior matrix. The mandatory gate is satisfied; execution began. +- CF-0003: published the matrix in `include/rayforce.h`, normalized all + fifteen censused statements, and added `public/nullability_matrix` without + changing executable representation. +- Census self-check: zero conflicting statements; the seven remaining regex + matches are correct counterexamples saying empty SYM is not null. +- Build self-check: ASan/UBSan debug build succeeded under `-Werror`. +- Test self-check: 3,639/3,639 passed with zero skips and zero failures. +- Documentation self-check: `mkdocs build --strict` succeeded. +- Supplemental review found a separate behavior-level candidate around + approximate distinct handling of empty SYM/STR values. It is outside the + accepted fifteen-statement comment census and was not modified here; a + future audit pass can evaluate it against the newly ratified contract. diff --git a/docs/docs/namespaces/csv.md b/docs/docs/namespaces/csv.md index 9aaf5d90..29b00307 100644 --- a/docs/docs/namespaces/csv.md +++ b/docs/docs/namespaces/csv.md @@ -22,7 +22,7 @@ Signatures: - `(.csv.read [types] "path")` — types is a SYM vector naming `B8`, `I64`, `F64`, `STR`, `SYMBOL`, `F32`, `DATE`, `TIME`, `TIMESTAMP`, `GUID` (case-insensitive). One entry per column. - `(.csv.read [names] [types] "path")` — also override the column names. With explicit names the input is assumed to have **no header row**. -Returns: a `table`. Empty fields and the empty string are read as null with `RAY_ATTR_HAS_NULLS` set on the column. +Returns: a `table`. Empty fields use the selected type's missing-input behavior. Sentinel-nullable columns record nulls with `RAY_ATTR_HAS_NULLS`; `SYM`/`STR` fields collapse to ordinary empty values and those columns do not acquire `RAY_ATTR_HAS_NULLS` from them. Errors: `type` (bad arg), `domain` (path is null / too long, or zero columns), `io` (open/read failure), `limit` (>256 columns). diff --git a/include/rayforce.h b/include/rayforce.h index b67ffd6b..b7d79c03 100644 --- a/include/rayforce.h +++ b/include/rayforce.h @@ -433,13 +433,25 @@ int64_t ray_timestamp_now_ns(void); ray_t* ray_guid(const uint8_t* bytes); ray_t* ray_typed_null(int8_t type); -/* ===== Null Sentinel Values ===== +/* ===== Nullability and Null Sentinel Values ===== * - * Per-type null encoding for nullable scalar types. Callers compare values - * directly (e.g. `x == NULL_I64`, `x != x` for NaN); there are no predicate - * macros or aliases. Temporal types (DATE/TIME/TIMESTAMP) reuse NULL_I32 or - * NULL_I64 based on their storage width. SYM null = sym ID 0; STR null = - * empty string (length 0); BOOL and U8 are non-nullable. */ + * Public nullability matrix: + * + * type family atom encoding vector encoding + * ---------------- ------------------------ ------------------------ + * I16/I32/I64 width-correct NULL_* width-correct NULL_* + * DATE/TIME/TS width-correct NULL_* width-correct NULL_* + * F32/F64 NaN NaN + * GUID 16 all-zero bytes 16 all-zero bytes + * BOOL/U8 ray_typed_null aux bit non-nullable + * SYM/STR no distinct null non-nullable + * + * Empty SYM/STR values are ordinary values. A requested typed or input null + * for either type collapses to the ordinary empty value; its public null + * predicate remains false and a vector does not acquire HAS_NULLS. Callers + * compare sentinel-backed payloads directly (for example `x == NULL_I64` or + * `x != x` for NaN). Temporal types reuse NULL_I32 or NULL_I64 according to + * storage width. */ #define NULL_I16 ((int16_t)INT16_MIN) #define NULL_I32 ((int32_t)INT32_MIN) #define NULL_I64 ((int64_t)INT64_MIN) @@ -447,9 +459,9 @@ ray_t* ray_typed_null(int8_t type); #define NULL_F64 (__builtin_nan("")) /* Atom null check. RAY_NULL_OBJ is the untyped null singleton. - * Typed atoms with a defined NULL_* sentinel use payload-compare; - * types without a sentinel (BOOL/U8/F32) fall back to the - * aux[0]&1 bit written by ray_typed_null. */ + * Sentinel-backed atoms use payload comparison. BOOL/U8 typed-null atoms + * use the aux[0]&1 bit written by ray_typed_null. SYM/STR always return + * false because their empty payloads are ordinary values. */ static inline bool ray_atom_is_null_fn(const union ray_t* x) { if (RAY_IS_NULL(x)) return true; if (x->type >= 0) return false; diff --git a/src/ops/pivot.c b/src/ops/pivot.c index 4b8f1445..659b66a5 100644 --- a/src/ops/pivot.c +++ b/src/ops/pivot.c @@ -1264,9 +1264,10 @@ ray_t* exec_pivot(ray_graph_t* g, ray_op_t* op, ray_t* tbl) { * distinguishable from a real 0 (review 2.10). Present cells get * overwritten in the scatter loop below; whatever remains is null. * par_finalize_nulls (after scatter) flips HAS_NULLS if any - * sentinel survives. Non-sentinel types (SYM/STR/BOOL/U8/GUID) - * fall back to zero-fill: SYM id 0 is the SYM null already; the - * others carry no null sentinel. */ + * sentinel survives. Types without a dedicated initialization arm + * (SYM/STR/BOOL/U8/GUID) fall back to zero-fill. For SYM/STR/BOOL/U8 + * that is an ordinary zero/empty value and cannot represent "no + * data"; GUID is also zero-filled on this path. */ switch (new_col->type) { case RAY_F64: { double* d = (double*)ray_data(new_col); @@ -1425,7 +1426,7 @@ ray_t* exec_pivot(ray_graph_t* g, ray_op_t* op, ray_t* tbl) { * sentinel — either a missing cell (no source row) left as null by * the init above, or (F64) a value canonicalized to NULL_F64 by * ray_f64_fin (avg division, sum overflow). par_finalize_nulls is - * a no-op for non-sentinel types (SYM/STR/BOOL/U8/GUID). */ + * a no-op for types it does not handle (SYM/STR/BOOL/U8/GUID). */ par_finalize_nulls(new_col); result = ray_table_add_col(result, col_sym, new_col); ray_release(new_col); diff --git a/src/table/domain.h b/src/table/domain.h index e7dabd84..6d62bf4d 100644 --- a/src/table/domain.h +++ b/src/table/domain.h @@ -47,8 +47,8 @@ * ray_sym_domain_str a lock-free array read. * * Position-0 reservation: position 0 of every non-empty FILE domain is - * the empty string "" (mirrors global id 0 — the SYM null; group kernels - * and null conventions treat id 0 as null). ray_sym_domain_open + * the ordinary empty symbol "" (mirrors global id 0). SYM has no distinct + * null; group kernels treat id 0 as an ordinary value. ray_sym_domain_open * VALIDATES this and refuses (NULL) files that violate it; intern on an * empty domain seeds "" at position 0 before the first real symbol, so * newly created symfiles always carry it. Empty vocabularies are fine. diff --git a/src/vec/atom.c b/src/vec/atom.c index 1ca2c9aa..7ae4ce7f 100644 --- a/src/vec/atom.c +++ b/src/vec/atom.c @@ -181,9 +181,9 @@ ray_t* ray_typed_null(int8_t type) { /* GUID null is the canonical all-zero 16-byte value: allocate the * U8 payload buffer up front (same shape as ray_guid) so consumers * can deref obj without a NULL check. Other types use the payload - * union — the sentinel write below is the source of truth; the - * aux[0] bit is retained for atom types without a sentinel - * (BOOL/U8/F32). */ + * union — the sentinel write below is the source of truth. The + * aux[0] bit represents typed null only for BOOL/U8; F32 uses NaN. + * SYM/STR typed-null requests collapse to ordinary empty values. */ if (type == -RAY_GUID) { static const uint8_t NULL_GUID_BYTES[16] = {0}; ray_t* v = ray_guid(NULL_GUID_BYTES); diff --git a/src/vec/vec.c b/src/vec/vec.c index 158fae6e..68679928 100644 --- a/src/vec/vec.c +++ b/src/vec/vec.c @@ -1152,9 +1152,10 @@ ray_t* ray_str_vec_append(ray_t* vec, const char* s, size_t len) { * * Allocates the string pool once (not per-element like ray_str_vec_append). * Pass 1 sums pooled bytes; pass 2 fills descriptors and pool in one sweep. - * ptrs[i] may be NULL when lens[i]==0. nulls may be NULL (no nulls); when - * non-NULL, nulls[i]!=0 marks element i null (STR null = empty string — - * len==0, no RAY_ATTR_HAS_NULLS set). + * ptrs[i] may be NULL when lens[i]==0. nulls may be NULL (no missing + * inputs); when non-NULL, nulls[i]!=0 requests a missing input. STR cannot + * represent a distinct null, so that request collapses to an ordinary empty + * string (len==0) without RAY_ATTR_HAS_NULLS. * -------------------------------------------------------------------------- */ ray_t* ray_str_vec_from_parts(const char* const* ptrs, const uint32_t* lens, @@ -1197,7 +1198,7 @@ ray_t* ray_str_vec_from_parts(const char* const* ptrs, const uint32_t* lens, ray_str_t* d = &elems[i]; memset(d, 0, sizeof(ray_str_t)); if (nulls && nulls[i]) { - /* STR null = empty string: d is already zeroed (len=0) */ + /* Missing STR input collapses to ordinary empty: already len=0. */ } else if (lens[i] <= RAY_STR_INLINE_MAX) { d->len = lens[i]; if (lens[i] > 0) memcpy(d->data, ptrs[i], lens[i]); diff --git a/test/rfl/integration/null.rfl b/test/rfl/integration/null.rfl index 8d935199..f134ec71 100644 --- a/test/rfl/integration/null.rfl +++ b/test/rfl/integration/null.rfl @@ -5,7 +5,7 @@ (nil? 0Nl) -- true (nil? 0) -- false (nil? 1) -- false -;; STR null = empty string (len 0). +;; STR has no distinct null; "" is the ordinary empty value. (nil? "") -- false ;; nil? distinguishes typed nulls from zero-valued atoms across types (nil? 0Ni) -- true diff --git a/test/rfl/strop/split.rfl b/test/rfl/strop/split.rfl index 84f82db5..29797c68 100644 --- a/test/rfl/strop/split.rfl +++ b/test/rfl/strop/split.rfl @@ -1,9 +1,8 @@ ;; Invariants for `split`. (split "a,b,c" ",") -- ["a" "b" "c"] -;; Empty string IS the STR null, so split-of-"" yields a one-element -;; vector whose only element is null. Assert via nil? rather than a -;; [0Nc] literal (parser doesn't accept that form). +;; Empty string is an ordinary STR value, so split-of-"" yields a +;; one-element vector whose only element is not null. (count (split "" ",")) -- 1 (nil? (at (split "" ",") 0)) -- false (split "abc" ",") -- ["abc"] diff --git a/test/stress_store.c b/test/stress_store.c index 8757f03e..71ac326d 100644 --- a/test/stress_store.c +++ b/test/stress_store.c @@ -115,7 +115,7 @@ void stress_gen_row(stress_ctx_t* c, stress_sym_pattern_t pat, stress_row_t* out) { memset(out, 0, sizeof(*out)); if (pat == STRESS_SYMS_NULLS && (stress_rand(c) & 1)) { - out->ticker[0] = '\0'; /* SYM null = sym 0, the empty string */ + out->ticker[0] = '\0'; /* ordinary empty SYM (sym 0) */ out->price = NULL_F64; out->qty = NULL_I64; return; diff --git a/test/test_dict.c b/test/test_dict.c index 569b17f4..a868b6d8 100644 --- a/test/test_dict.c +++ b/test/test_dict.c @@ -1174,9 +1174,8 @@ static test_result_t test_dict_find_idx_str_with_nulls(void) { TEST_ASSERT_EQ_I(ray_dict_find_idx(d, ka), 2); ray_release(ka); - /* Empty string IS a null STR atom. An empty-string lookup is - * therefore a null lookup and resolves to the first null slot - * (index 1) — STR null = empty string is a deliberate conflation. */ + /* STR has no distinct null. ray_vec_set_null above collapses slot 1 to + * the ordinary empty string, so lookup resolves by ordinary equality. */ ka = ray_str("", 0); TEST_ASSERT_EQ_I(ray_dict_find_idx(d, ka), 1); ray_release(ka); @@ -1209,9 +1208,8 @@ static test_result_t test_dict_find_idx_guid_with_nulls(void) { TEST_ASSERT_EQ_I(ray_dict_find_idx(d, ka), 2); ray_release(ka); - /* NULL_GUID = 16 all-zero bytes. An all-zero GUID lookup IS a null - * lookup and resolves to the first null slot (index 1) — same - * conflation as STR null = empty string. */ + /* NULL_GUID = 16 all-zero bytes. Unlike STR, GUID has a sentinel-backed + * null, so an all-zero lookup resolves to the first null slot (index 1). */ ka = ray_guid(g1); TEST_ASSERT_EQ_I(ray_dict_find_idx(d, ka), 1); ray_release(ka); diff --git a/test/test_format.c b/test/test_format.c index 716e03b0..e18d46d5 100644 --- a/test/test_format.c +++ b/test/test_format.c @@ -748,9 +748,9 @@ static test_result_t test_fmt_null_date(void) { } static test_result_t test_fmt_null_str(void) { - /* A STR atom has no distinct null: the empty string and the null - * string are the same value, and both render as the empty-string - * literal "" — never 0Nc (which the parser cannot even read back). */ + /* A STR atom has no distinct null: ray_typed_null collapses to the + * ordinary empty string and renders as "" — never 0Nc (which the parser + * cannot even read back). */ ray_t* result = ray_fmt(ray_typed_null(-RAY_STR), 1); TEST_ASSERT_NOT_NULL(result); TEST_ASSERT_FALSE(RAY_IS_ERR(result)); @@ -946,8 +946,8 @@ static test_result_t test_fmt_vec_str(void) { } static test_result_t test_fmt_vec_str_null(void) { - /* A null element of a STR vector renders as "" — same rule as the - * STR atom: empty and null are indistinguishable for strings. */ + /* A requested null in a STR vector collapses to the ordinary empty + * string, so it renders as "" and its null predicate remains false. */ ray_t* vec = ray_vec_new(RAY_STR, 2); TEST_ASSERT_NOT_NULL(vec); TEST_ASSERT_FALSE(RAY_IS_ERR(vec)); @@ -1769,4 +1769,3 @@ const test_entry_t format_entries[] = { { NULL, NULL, NULL, NULL }, }; - diff --git a/test/test_public_api.c b/test/test_public_api.c index 7270887f..9e7b6a54 100644 --- a/test/test_public_api.c +++ b/test/test_public_api.c @@ -629,6 +629,36 @@ static test_result_t test_public_get_error_trace_cleared_on_eval(void) { PASS(); } +static test_result_t test_public_nullability_matrix(void) { + ray_t* ordinary[] = { + ray_sym(0), + ray_str("", 0), + ray_bool(false), + ray_u8(0), + ray_f32(0.0f), + }; + ray_t* requested_null[] = { + ray_typed_null(-RAY_SYM), + ray_typed_null(-RAY_STR), + ray_typed_null(-RAY_BOOL), + ray_typed_null(-RAY_U8), + ray_typed_null(-RAY_F32), + }; + const bool requested_is_null[] = { false, false, true, true, true }; + + for (size_t i = 0; i < sizeof(ordinary) / sizeof(ordinary[0]); i++) { + TEST_ASSERT_NOT_NULL(ordinary[i]); + TEST_ASSERT_FALSE(RAY_IS_ERR(ordinary[i])); + TEST_ASSERT_FALSE(RAY_ATOM_IS_NULL(ordinary[i])); + TEST_ASSERT_NOT_NULL(requested_null[i]); + TEST_ASSERT_FALSE(RAY_IS_ERR(requested_null[i])); + TEST_ASSERT_EQ_I(RAY_ATOM_IS_NULL(requested_null[i]), requested_is_null[i]); + ray_release(ordinary[i]); + ray_release(requested_null[i]); + } + PASS(); +} + const test_entry_t public_api_entries[] = { { "public/ipc_client_symbols", test_public_ipc_client_symbols, NULL, NULL }, { "public/query_and_format_symbols", test_public_query_and_format_symbols, NULL, NULL }, @@ -675,6 +705,7 @@ const test_entry_t public_api_entries[] = { { "public/eval_restricted_allows_arith", test_public_eval_restricted_allows_arith, public_api_setup, public_api_teardown }, { "public/get_error_trace_populated", test_public_get_error_trace_populated, public_api_setup, public_api_teardown }, { "public/get_error_trace_cleared_on_eval",test_public_get_error_trace_cleared_on_eval,public_api_setup, public_api_teardown }, + { "public/nullability_matrix", test_public_nullability_matrix, public_api_setup, public_api_teardown }, { NULL, NULL, NULL, NULL }, }; diff --git a/test/test_str.c b/test/test_str.c index 9ed23f24..77d6b4dd 100644 --- a/test/test_str.c +++ b/test/test_str.c @@ -1827,7 +1827,7 @@ static test_result_t test_str_vec_from_parts(void) { p = ray_str_vec_get(v, 1, &l); TEST_ASSERT_EQ_U(l, 3); TEST_ASSERT_EQ_I(memcmp(p,"abc",3), 0); p = ray_str_vec_get(v, 2, &l); TEST_ASSERT_EQ_U(l, 27); TEST_ASSERT_EQ_I(memcmp(p,"this_is_long_enough_to_pool",27), 0); p = ray_str_vec_get(v, 3, &l); TEST_ASSERT_EQ_U(l, 1); TEST_ASSERT_EQ_I(p[0],'x'); - /* STR has no null distinct from "" — null stored as empty string */ + /* Missing STR input collapses to the ordinary empty string. */ p = ray_str_vec_get(v, 4, &l); TEST_ASSERT_EQ_U(l, 0); (void)p; ray_release(v); ray_heap_destroy(); @@ -1962,4 +1962,3 @@ const test_entry_t str_entries[] = { { "str/ne_nonempty", test_str_ne_nonempty, NULL, NULL }, { NULL, NULL, NULL, NULL }, }; - From 3ae79f56b144ac23878bfb0352665d0971d20b90 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 22 Jul 2026 14:42:39 +0200 Subject: [PATCH 14/14] feat(docs): redesign website and documentation Rebuild the MkDocs and marketing surfaces around the Rayforce brand, add the live market demo and cloud preview, unify responsive navigation, and eliminate reload layout shifts. --- docs/assets/ai-assist-widget.css | 185 ++++ docs/assets/extra.css | 866 +++++++++++++++++- docs/assets/landing.css | 1157 ++++--------------------- docs/assets/rayforce-engine-hero.webp | Bin 0 -> 235016 bytes docs/javascripts/ai-assist-widget.js | 35 +- docs/javascripts/landing.js | 451 +++------- docs/javascripts/main.js | 50 +- examples/rfl/market_demo.rfl | 21 + mkdocs.yml | 3 +- overrides/about.html | 21 +- overrides/home.html | 561 ++++++------ overrides/main.html | 92 +- overrides/partials/header.html | 125 ++- 13 files changed, 1777 insertions(+), 1790 deletions(-) create mode 100644 docs/assets/rayforce-engine-hero.webp create mode 100644 examples/rfl/market_demo.rfl diff --git a/docs/assets/ai-assist-widget.css b/docs/assets/ai-assist-widget.css index b985ed16..957dc522 100644 --- a/docs/assets/ai-assist-widget.css +++ b/docs/assets/ai-assist-widget.css @@ -325,3 +325,188 @@ .rfai-messages { padding: 12px; } .rfai-msg { max-width: 100%; } } + +/* 2026 editorial shell: keep assistance present without obscuring the page. */ +.rfai-launcher { + right: max(22px, env(safe-area-inset-right)); + bottom: max(22px, env(safe-area-inset-bottom)); + min-width: 112px; + min-height: 48px; + gap: 8px; + padding: 0 15px 0 10px; + border-radius: 0; + background: rgba(8, 11, 16, .9); + border-color: rgba(233, 160, 51, .55); + color: var(--rfai-primary-lighter); + box-shadow: 0 16px 54px rgba(0, 0, 0, .42); + backdrop-filter: blur(14px); + font-family: var(--rfai-mono); + font-size: .72rem; + letter-spacing: .02em; +} +.rfai-launcher:hover { + border-color: var(--rfai-primary); + background: var(--rfai-primary); + color: #171109; + box-shadow: 0 18px 60px rgba(0, 0, 0, .48); +} +.rfai-launcher-icon { width: 28px; height: 28px; border-radius: 0; background: rgba(233,160,51,.12); } +.rfai-launcher-icon svg { width: 18px; height: 18px; filter: none; } +.rfai-dialog { border-radius: 0; border-color: rgba(233,160,51,.28); } +.rfai-icon-button,.rfai-msg,.rfai-question,.rfai-send,.rfai-suggestions button,.rfai-table-wrap,.rfai-msg .rfai-content pre { border-radius: 0; } + +@media (max-width: 768px) { + .rfai-launcher { + right: max(12px, env(safe-area-inset-right)); + bottom: max(12px, env(safe-area-inset-bottom)); + min-width: 46px; + width: 46px; + min-height: 46px; + padding: 0; + } + .rfai-launcher > span:last-child { display: none; } + .rfai-launcher-icon { background: transparent; } + .rfai-dialog { width: calc(100vw - 16px); height: calc(100vh - 22px); border-radius: 0; } +} + +/* Unified 2026 brand surface: the AI panel mirrors the landing workbench. */ +.rfai-launcher { + min-width: 124px; + min-height: 50px; + padding: 0 16px 0 10px; + border-color: rgba(233,160,51,.5); + background: rgba(8,11,16,.94); + color: #f3c46b; + font-family: var(--rfai-mono); + font-size: 11px; + font-weight: 600; + letter-spacing: .08em; + text-transform: uppercase; +} +.rfai-launcher:hover { + border-color: #e9a033; + background: #e9a033; + color: #171109; +} +.rfai-launcher-icon { border: 1px solid rgba(233,160,51,.28); box-shadow: none; } + +.rfai-dialog { + width: min(860px, calc(100vw - 48px)); + height: min(650px, calc(100vh - 72px)); + background: #080b10; + border-color: rgba(255,255,255,.18); + box-shadow: 0 32px 120px rgba(0,0,0,.72); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} +.rfai-header { + min-height: 76px; + padding: 0 20px; + border-bottom-color: rgba(255,255,255,.11); + background: #080b10; +} +.rfai-title-row { gap: 18px; } +.rfai-logo { + width: 122px; + padding-right: 18px; + border-right: 1px solid rgba(255,255,255,.12); +} +.rfai-header h2 { + font-size: 24px; + letter-spacing: -.02em; + text-transform: uppercase; +} +.rfai-header p { + margin-top: 4px; + color: #667583; + font-family: var(--rfai-mono); + font-size: 9px; + letter-spacing: .08em; + text-transform: uppercase; +} +.rfai-icon-button { + width: 40px; + height: 40px; + border-color: rgba(255,255,255,.14); + background: transparent; +} +.rfai-icon-button:hover { border-color: #e9a033; background: rgba(233,160,51,.08); } + +.rfai-messages { + gap: 14px; + padding: 22px; + background-color: #0b1017; + background-image: + linear-gradient(rgba(255,255,255,.022) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,.022) 1px, transparent 1px); + background-size: 56px 56px; +} +.rfai-msg { + padding: 13px 15px; + border-color: rgba(255,255,255,.11); + background: #101720; + box-shadow: none; +} +.rfai-msg .rfai-content p, +.rfai-msg .rfai-content ul { color: #9aa7b3; font-family: var(--rfai-font); font-size: 14px; } +.rfai-msg-assistant.rfai-welcome { + border-left: 3px solid #e9a033; + background: rgba(17,25,35,.92); +} +.rfai-msg-user { + border-color: rgba(233,160,51,.38); + background: rgba(233,160,51,.1); +} +.rfai-msg .rfai-content h3, +.rfai-msg .rfai-content h4, +.rfai-msg .rfai-content h5 { + letter-spacing: -.01em; + text-transform: uppercase; +} +.rfai-msg .rfai-content code { border-radius: 0; } +.rfai-suggestions { gap: 8px; margin-top: 0; } +.rfai-suggestions button { + min-height: 34px; + padding: 7px 11px; + border-color: rgba(233,160,51,.22); + background: rgba(8,11,16,.8); + color: #9aa7b3; + font-family: var(--rfai-mono); + font-size: 10px; + font-weight: 600; + letter-spacing: .06em; + text-transform: uppercase; +} +.rfai-suggestions button:hover { color: #171109; border-color: #e9a033; background: #e9a033; } +.rfai-form { + gap: 10px; + padding: 16px; + border-top-color: rgba(255,255,255,.11); + background: #080b10; +} +.rfai-question { + min-height: 54px; + border-color: rgba(255,255,255,.14); + background: #0b1017; + color: #f8fafc; + font-family: var(--rfai-font); + font-size: 14px; +} +.rfai-question:focus { border-color: #e9a033; box-shadow: none; } +.rfai-send { + width: 56px; + min-height: 54px; + border-color: #e9a033; + background: #e9a033; + color: #171109; + box-shadow: none; +} +.rfai-send:hover { background: #f3c46b; } + +@media (max-width: 768px) { + .rfai-dialog { width: calc(100vw - 12px); height: calc(100svh - 12px); } + .rfai-header { min-height: 68px; padding: 0 14px; } + .rfai-logo { width: 102px; padding-right: 12px; } + .rfai-header h2 { font-size: 20px; } + .rfai-messages { padding: 14px; } +} diff --git a/docs/assets/extra.css b/docs/assets/extra.css index 6d73c055..73c59ada 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -10,14 +10,14 @@ font-family: 'Inter'; font-style: normal; font-weight: 400; - font-display: swap; + font-display: block; src: url('/assets/fonts/inter-400.woff2') format('woff2'); } @font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; - font-display: swap; + font-display: block; src: url('/assets/fonts/inter-600.woff2') format('woff2'); } @font-face { @@ -38,21 +38,21 @@ font-family: 'Oswald'; font-style: normal; font-weight: 700; - font-display: swap; + font-display: block; src: url('/assets/fonts/oswald-700.woff2') format('woff2'); } @font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; - font-display: swap; + font-display: block; src: url('/assets/fonts/jbmono-400.woff2') format('woff2'); } @font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 600; - font-display: swap; + font-display: block; src: url('/assets/fonts/jbmono-600.woff2') format('woff2'); } @@ -94,6 +94,7 @@ share the same tokens as the marketing site without hand-editing colour values across the file. */ :root { + --rf-banner-h: 34px; --bg: #0a0e13; --bg-section: #0d1219; --bg-card: #111820; @@ -172,20 +173,6 @@ --md-typeset-a-color: #c4821a; } -/* ── Heading permalinks: hide the ¶ but keep them clickable on hover. - `permalink: true` in mkdocs.yml emits a - beside every heading — useful for "copy link to section" but visually - noisy. Hide by default, fade in only when the heading is hovered. */ -.md-typeset .headerlink { - opacity: 0; - margin-left: 0.4em; - color: var(--accent); - transition: opacity .15s; -} -.md-typeset :is(h1, h2, h3, h4, h5, h6):hover .headerlink, -.md-typeset .headerlink:focus { opacity: 0.7; } -.md-typeset .headerlink:hover { opacity: 1; } - /* ── Headings: Oswald, marketing-style ────────────────────────── */ .md-typeset h1, @@ -509,36 +496,845 @@ html.landing-page body .footer.landing-footer { display: block; } .md-banner__inner wrappers so .rf-banner is the only styled box. In normal flow at the very top of every page: on docs it scrolls away above the sticky header (Material-native), on the landing/about pages main.js offsets the - floating pill nav below it via --rf-banner-h. */ + fixed site nav below it via --rf-banner-h. */ .md-banner, .md-banner__inner { display: contents; } .rf-banner { - display: flex; align-items: center; justify-content: center; gap: 10px; - position: relative; padding: 8px 46px; min-height: 38px; - background: linear-gradient(90deg, var(--navy, #0e1b24), #122430); - color: var(--text, #f1f5f9); - border-bottom: 1px solid var(--border, rgba(233, 160, 51, 0.18)); - font-size: .82rem; text-align: center; + display: block; position: relative; min-height: 34px; padding: 0; + background: #0d292b; color: #bad0d0; + border-bottom: 1px solid rgba(94, 234, 212, .22); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: .63rem; letter-spacing: .035em; } .rf-banner[hidden] { display: none; } +.rf-banner-inner { + display: flex; align-items: center; gap: 14px; + width: min(calc(100% - 80px), 1380px); min-height: 34px; margin: 0 auto; +} +.rf-banner-label { + align-self: stretch; display: inline-flex; align-items: center; padding: 0 12px; + background: #5eead4; color: #071516; font-weight: 700; + letter-spacing: .1em; text-transform: uppercase; +} .rf-banner-msg { - display: inline-flex; align-items: center; gap: 8px; - flex-wrap: wrap; justify-content: center; + overflow: hidden; color: #bad0d0; text-overflow: ellipsis; white-space: nowrap; } -.rf-banner-spark { color: var(--primary, #e9a033); } .rf-banner-link { - display: inline-flex; align-items: center; gap: 5px; - color: var(--primary-light, #f0b954); font-weight: 700; - text-decoration: none; border-bottom: 1px solid transparent; + display: inline-flex; align-items: center; gap: 7px; margin-left: auto; + width: 140px; justify-content: flex-end; + color: #f3c46b; font-weight: 700; text-transform: uppercase; + text-decoration: none; } .rf-banner-link:hover { - color: var(--primary-lighter, #f5d080); border-bottom-color: currentColor; + color: #f8fafc; } .rf-banner-close { - position: absolute; right: 12px; top: 50%; transform: translateY(-50%); display: inline-flex; padding: 4px; border: 0; background: none; - color: var(--text-muted, #7b8fa0); cursor: pointer; border-radius: 6px; + color: #526170; cursor: pointer; border-radius: 0; line-height: 0; transition: color .2s, background .2s; } .rf-banner-close:hover { color: var(--text, #f1f5f9); background: rgba(255, 255, 255, 0.08); } +@media screen and (max-width: 600px) { + .rf-banner-inner { width: calc(100% - 24px); gap: 9px; } + .rf-banner-label { padding-inline: 8px; } + .rf-banner-msg { flex: 1; } + .rf-banner-link { width: auto; margin-left: 0; } + .rf-banner-link [data-download-label] { display: none; } +} + +/* 2026 documentation shell — the same sharp, technical visual language as home. */ +[data-md-color-scheme="slate"] { + --md-default-bg-color: #080b10; + --md-default-bg-color--light: #0c1118; + --md-default-bg-color--lighter: #111923; + --md-default-bg-color--lightest: #17212c; + --md-code-bg-color: #0b1017; +} + +[data-md-color-scheme="slate"] body { + background-color: #080b10; + background-image: + linear-gradient(rgba(255,255,255,.014) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,.014) 1px, transparent 1px); + background-size: 72px 72px; +} + +.md-header { + height: 70px; + background: rgba(8, 11, 16, .94); + border-bottom: 1px solid rgba(255,255,255,.09); + box-shadow: none; + backdrop-filter: blur(16px); +} +.md-header__inner { height: 70px; max-width: 1240px; } +.md-header .md-logo img, +.md-header .md-logo svg { height: 40px; max-height: 40px; } +.md-search__form { border: 1px solid rgba(255,255,255,.08); border-radius: 0; background: #0b1017; } +.md-search__input { border-radius: 0; } + +.md-main__inner { max-width: 1240px; padding-top: 2.6rem; } +.md-content__inner { max-width: 780px; } +.md-sidebar { color: #9aa7b3; } +.md-sidebar__scrollwrap { scrollbar-width: thin; } +.md-nav__title { color: #7f8f9d; letter-spacing: .1em; } +.md-nav__link { line-height: 1.35; } +.md-nav__link:hover { color: #f3c46b; } +.md-nav__link--active { + color: #f3c46b; + padding-left: .65rem; + border-left: 2px solid #e9a033; +} + +.md-typeset h1 { + margin-bottom: 2.2rem; + color: #f8fafc; + font-size: clamp(2.8rem, 5vw, 4.6rem); + line-height: .96; + letter-spacing: -.025em; +} +.md-typeset h2 { + margin-top: 3.4rem; + padding-bottom: .55rem; + border-bottom-color: rgba(233,160,51,.22); + font-size: 2rem; +} +.md-typeset h3 { color: #f3c46b; } +.md-typeset p, +.md-typeset li { line-height: 1.7; } + +.md-typeset .md-button { + min-height: 48px; + padding: 12px 22px; + border-radius: 0; + font-family: 'JetBrains Mono', monospace; + font-size: .72rem; +} +.md-typeset .md-button--primary { + color: #171109; + border: 1px solid #e9a033; + background: #e9a033; + box-shadow: 0 12px 36px rgba(233,160,51,.16); +} +.md-typeset .md-button--primary:hover { + color: #171109; + background: #f3c46b; + box-shadow: 0 16px 44px rgba(233,160,51,.22); +} +.md-typeset code { border-radius: 0; } +.md-typeset pre > code, +.md-typeset .highlight, +.md-typeset table:not([class]), +.md-typeset .admonition, +.md-typeset details { border-radius: 0; } +.md-typeset pre > code { border-color: rgba(255,255,255,.1); } +.md-typeset table:not([class]) { border-color: rgba(255,255,255,.1); } +.md-footer { border-top-color: rgba(255,255,255,.1); } + +[data-md-color-scheme="slate"] .md-search__output { border-radius: 0; } +.rf-banner { background: #0d292b; border-bottom-color: rgba(94,234,212,.22); } + +@media screen and (max-width: 76.234375em) { + .md-main__inner { padding-inline: 1rem; } +} + +/* Unified site navigation for documentation pages. */ +.md-header { + height: 76px; + background: rgba(8,11,16,.96); + border-bottom: 1px solid rgba(255,255,255,.11); +} +.md-header__inner { + width: min(calc(100% - 80px), 1380px); + max-width: 1380px; + height: 76px; + margin-inline: auto; + padding: 0; +} +.md-header .md-logo img, +.md-header .md-logo svg { + width: 128px; + height: auto; + max-height: 44px; +} +.docs-nav-links { + display: flex; + align-items: center; + gap: 3px; +} +.docs-nav-links a { + padding: 10px 15px; + color: #b5c0ca; + font-family: 'Inter', 'Inter Fallback', sans-serif; + font-size: 14px; + font-weight: 600; + line-height: 1; + text-decoration: none; + transition: color .2s, background .2s; +} +.docs-nav-links a:hover, +.docs-nav-links a.active { + color: #f8fafc; + background: rgba(255,255,255,.05); +} +.docs-nav-cloud { display: inline-flex; align-items: center; gap: 7px; } +.docs-nav-cloud i { + width: 6px; + height: 6px; + border-radius: 50%; + background: #e9a033; + box-shadow: 0 0 12px #e9a033; +} +.md-header__spacer { flex: 1 1 26px; } +.md-search { margin-left: 10px; } +.md-search__form { width: 184px; } +.docs-nav-github { + min-width: 64px; + min-height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-left: 12px; + padding: 8px 12px; + border: 1px solid rgba(255,255,255,.2); + color: #f8fafc; + font: 600 12px 'JetBrains Mono', monospace; + text-decoration: none; +} +.docs-nav-github:hover { border-color: #e9a033; background: rgba(233,160,51,.07); } +.docs-nav-github svg { width: 17px; height: 17px; fill: currentColor; } + +/* Docs typography follows the same editorial hierarchy as the home sections. */ +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4 { + font-family: 'Oswald', 'Oswald Fallback', sans-serif; + text-transform: uppercase; +} +.md-typeset h1 { letter-spacing: -.035em; } +.md-typeset h2 { letter-spacing: -.02em; } +.md-nav__title { + font-family: 'JetBrains Mono', monospace; + font-size: .62rem; + letter-spacing: .11em; +} +.md-nav__link { font-family: 'Inter', 'Inter Fallback', sans-serif; } + +@media screen and (max-width: 76.234375em) { + .md-header, + .md-header__inner { height: 70px; } + .md-header__inner { width: calc(100% - 32px); } + .docs-nav-links, + .docs-nav-github { display: none; } + .md-header__spacer { flex: 1; } + .md-search { margin-left: 0; } +} + +/* Editorial docs shell — the documentation is a technical section of the + marketing site, not a separate Material-themed product. */ +html:not(.landing-page) .md-main { + position: relative; + isolation: isolate; + background: + radial-gradient(circle at 76% 8%, rgba(233,160,51,.07), transparent 28%), + linear-gradient(rgba(255,255,255,.018) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,.018) 1px, transparent 1px), + #080b10; + background-size: auto, 72px 72px, 72px 72px, auto; +} +html:not(.landing-page) .md-main::before { + content: ''; + position: absolute; + z-index: -1; + inset: 0 0 auto 38%; + height: 760px; + background: url('/assets/rayforce-engine-hero.webp') top right / cover no-repeat; + opacity: .075; + filter: saturate(.8); + mask-image: linear-gradient(to bottom, #000, transparent 82%); + pointer-events: none; +} +html:not(.landing-page) .md-main__inner { + width: min(calc(100% - 80px), 1380px); + max-width: 1380px; + margin-inline: auto; + padding-top: 64px; + padding-bottom: 100px; +} +html:not(.landing-page) .md-content { + position: relative; + min-width: 0; + padding-inline: clamp(38px, 4vw, 68px); + border-inline: 1px solid rgba(255,255,255,.08); + background: rgba(8,11,16,.6); + backdrop-filter: blur(2px); +} +html:not(.landing-page) .md-content__inner { + max-width: 860px; + margin-inline: auto; + padding-top: 0; + padding-bottom: 5rem; +} +html:not(.landing-page) .md-content__inner::before { display: none; } +html:not(.landing-page) .md-typeset h1 { + margin: 0 0 36px; + color: #f8fafc; + font-size: clamp(3.6rem, 5vw, 5.8rem); + line-height: .9; + letter-spacing: -.045em; +} +html:not(.landing-page) .md-typeset h1::before { + content: 'RAYFORCE / DOCUMENTATION'; + display: block; + margin-bottom: 26px; + padding-left: 46px; + background: linear-gradient(#e9a033,#e9a033) left center / 32px 1px no-repeat; + color: #f3c46b; + font: 600 10px/1.2 'JetBrains Mono', monospace; + letter-spacing: .13em; +} +html:not(.landing-page) .md-typeset h1 + p { + color: #c1cad3; + font-size: 1.02rem; + line-height: 1.75; +} +html:not(.landing-page) .md-typeset h2 { + margin-top: 4.2rem; + padding-bottom: .7rem; + border-bottom: 1px solid rgba(233,160,51,.24); + font-size: clamp(2.25rem, 3.2vw, 3.5rem); + line-height: 1; +} +html:not(.landing-page) .md-typeset h3 { + margin-top: 2.5rem; + font-size: 1.6rem; + letter-spacing: -.01em; +} +html:not(.landing-page) .md-typeset p, +html:not(.landing-page) .md-typeset li { color: #aab5bf; } +html:not(.landing-page) .md-typeset strong { color: #edf2f6; } +html:not(.landing-page) .md-sidebar { + color: #8f9daa; +} +html:not(.landing-page) .md-sidebar__inner { + padding: 18px 18px 24px; + border-top: 2px solid rgba(233,160,51,.72); + border-right: 1px solid rgba(255,255,255,.08); + border-bottom: 1px solid rgba(255,255,255,.08); + border-left: 1px solid rgba(255,255,255,.08); + background: rgba(11,16,23,.78); +} +html:not(.landing-page) .md-sidebar--secondary .md-sidebar__inner { + border-top-color: rgba(94,234,212,.55); +} +html:not(.landing-page) .md-nav__title { + margin-bottom: 12px; + color: #e9a033; +} +html:not(.landing-page) .md-sidebar--secondary .md-nav__title { color: #5eead4; } +html:not(.landing-page) .md-nav__link { + padding-block: 3px; + font-size: .76rem; +} +html:not(.landing-page) .md-nav__link--active { + padding-left: 10px; + border-left: 2px solid #e9a033; + color: #f3c46b; +} +html:not(.landing-page) .md-typeset pre > code, +html:not(.landing-page) .md-typeset table:not([class]), +html:not(.landing-page) .md-typeset .admonition, +html:not(.landing-page) .md-typeset details { + border-color: rgba(255,255,255,.12); + background-color: rgba(11,16,23,.88); +} +html:not(.landing-page) .md-footer { + background: #06090d; + border-top: 1px solid rgba(255,255,255,.1); +} + +@media screen and (max-width: 76.234375em) { + html:not(.landing-page) .md-main__inner { + width: calc(100% - 32px); + padding-top: 42px; + } + html:not(.landing-page) .md-content { + padding-inline: clamp(18px, 5vw, 48px); + border-inline: 0; + } + html:not(.landing-page) .md-typeset h1 { font-size: clamp(3rem, 12vw, 4.6rem); } +} + +@media screen and (max-width: 30em) { + html:not(.landing-page) .md-typeset .md-button { + display: table; + width: fit-content; + max-width: 100%; + margin: 0 0 10px; + } +} + +/* Documentation surface v2 — use the marketing site's actual composition, + not Material's scaled three-panel application shell. */ +html:not(.landing-page) { font-size: 16px; } +html:not(.landing-page) body { + font-size: 1rem; + line-height: 1.5; + background: #080b10; +} + +/* Exact marketing navigation, with docs-only search kept as an icon overlay. */ +html:not(.landing-page) .docs-site-nav { + position: fixed; + z-index: 1000; + top: var(--rf-banner-h, 0px); + left: 0; + width: 100%; + height: 76px; + color: #f8fafc; + background: rgba(8,11,16,.82); + border-bottom: 1px solid rgba(255,255,255,.11); + box-shadow: none; + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + transition: background .25s, border-color .25s; +} +html:not(.landing-page) .docs-site-nav.nav-scrolled { + background: rgba(8,11,16,.97); + border-color: rgba(233,160,51,.24); +} +html:not(.landing-page) .docs-site-nav .nav-inner { + width: min(calc(100% - 80px), 1380px); + max-width: 1380px; + height: 100%; + display: flex; + align-items: center; + margin-inline: auto; + padding: 0; +} +html:not(.landing-page) .docs-site-nav .nav-brand { + display: flex; + align-items: center; + flex: 0 0 auto; + margin: 0 auto 0 0; + padding: 0; +} +html:not(.landing-page) .docs-site-nav .nav-brand img, +html:not(.landing-page) .docs-site-nav .nav-brand svg { + display: block; + width: 128px; + height: auto; + max-height: 44px; +} +html:not(.landing-page) .docs-site-nav .nav-links { + display: flex; + align-items: center; + gap: 3px; +} +html:not(.landing-page) .docs-site-nav .nav-links > a { + padding: 10px 15px; + color: #b5c0ca; + font: 600 .88rem/1 Inter, 'Inter Fallback', sans-serif; + text-decoration: none; + transition: color .2s, background .2s; +} +html:not(.landing-page) .docs-site-nav .nav-links > a:hover, +html:not(.landing-page) .docs-site-nav .nav-links > a.active { + color: #f8fafc; + background: rgba(255,255,255,.05); +} +html:not(.landing-page) .docs-site-nav .nav-cloud { + display: inline-flex; + align-items: center; + gap: 7px; +} +html:not(.landing-page) .docs-site-nav .nav-cloud i { + width: 6px; + height: 6px; + border-radius: 50%; + background: #e9a033; + box-shadow: 0 0 12px #e9a033; +} +html:not(.landing-page) .docs-site-nav .nav-playground { + display: inline-flex; + align-items: center; + gap: 6px; + color: #f3c46b; +} +html:not(.landing-page) .docs-site-nav .nav-playground svg { + width: 12px; + height: 12px; + fill: currentColor; +} +html:not(.landing-page) .docs-site-nav .nav-github { + width: 90px; + min-width: 64px; + flex: 0 0 90px; + min-height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-left: 10px; + padding: 9px 12px; + border: 1px solid rgba(255,255,255,.2); + color: #f8fafc; + font: 600 .76rem/1 'JetBrains Mono', monospace; +} +html:not(.landing-page) .docs-site-nav .nav-github:hover { + border-color: #e9a033; + background: rgba(233,160,51,.07); +} +html:not(.landing-page) .docs-site-nav .nav-github svg { + width: 16px; + height: 16px; + fill: currentColor; +} +html:not(.landing-page) .docs-site-nav .nav-toggle { display: none; } +html:not(.landing-page) .docs-search-trigger { + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 10px; + border: 1px solid rgba(255,255,255,.2); + color: #b5c0ca; + cursor: pointer; + transition: color .2s, border-color .2s, background .2s; +} +html:not(.landing-page) .docs-search-trigger:hover { + color: #f8fafc; + border-color: #e9a033; + background: rgba(233,160,51,.07); +} +html:not(.landing-page) .docs-search-trigger > span, +html:not(.landing-page) .docs-browse-trigger { display: none; } +html:not(.landing-page) .docs-site-nav .md-search { + flex: 0 0 0; + width: 0; + margin: 0; +} +html:not(.landing-page) .docs-site-nav .md-search__inner { + width: 0; + overflow: hidden; +} +html:not(.landing-page) .docs-site-nav .md-search__form { width: 100%; } + +/* Full-bleed technical canvas: navigation + article, with the third Material + column removed. Empty space is intentional and carries the engine artwork. */ +html:not(.landing-page) .md-container { padding-top: 76px; } +html:not(.landing-page) .md-main { + position: relative; + isolation: isolate; + min-height: calc(100svh - 76px); + overflow: clip; + background: + radial-gradient(circle at 78% 10%, rgba(233,160,51,.07), transparent 29%), + linear-gradient(rgba(255,255,255,.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,.02) 1px, transparent 1px), + #080b10; + background-size: auto, 76px 76px, 76px 76px, auto; +} +html:not(.landing-page) .md-main::before { + content: ''; + position: absolute; + z-index: -1; + top: 0; + right: 0; + width: 58%; + height: 720px; + background: url('/assets/rayforce-engine-hero.webp') top right / cover no-repeat; + opacity: .11; + filter: saturate(.8); + -webkit-mask-image: linear-gradient(90deg, transparent, #000 25%, #000 70%, transparent); + mask-image: linear-gradient(90deg, transparent, #000 25%, #000 70%, transparent); + pointer-events: none; +} +html:not(.landing-page) .md-main__inner { + width: min(calc(100% - 80px), 1380px); + max-width: 1380px; + min-height: calc(100svh - 110px); + display: grid; + grid-template-columns: 240px minmax(0, 850px) minmax(80px, 1fr); + column-gap: clamp(42px, 5vw, 76px); + align-items: start; + margin: 0 auto; + padding: 72px 0 120px; +} +html:not(.landing-page) .md-sidebar--primary { + grid-column: 1; + width: auto; +} +html:not(.landing-page) .md-sidebar--secondary { display: none; } +html:not(.landing-page) .md-content { + grid-column: 2; + min-width: 0; + padding: 0; + border: 0; + background: transparent; + backdrop-filter: none; +} +html:not(.landing-page) .md-content__inner { + max-width: none; + margin: 0; + padding: 0 0 5rem; +} +html:not(.landing-page) .md-content__inner::before { display: none; } + +/* Navigation is a quiet index rail, not a boxed dashboard panel. */ +html:not(.landing-page) .md-sidebar--primary .md-sidebar__inner { + padding: 24px 30px 30px 0; + border: 0; + border-top: 1px solid rgba(233,160,51,.55); + border-right: 1px solid rgba(255,255,255,.11); + background: transparent; +} +html:not(.landing-page) .md-nav__title { + margin: 0 0 18px; + padding: 0; + color: #e9a033; + font: 600 .64rem/1.3 'JetBrains Mono', monospace; + letter-spacing: .13em; +} +html:not(.landing-page) .md-nav__link { + padding-block: 4px; + color: #8997a4; + font: 500 .8rem/1.4 Inter, 'Inter Fallback', sans-serif; +} +html:not(.landing-page) .md-nav__link:hover { color: #f8fafc; } +html:not(.landing-page) .md-nav__link--active { + padding-left: 11px; + border-left: 2px solid #e9a033; + color: #f3c46b; + font-weight: 600; +} + +/* Same editorial scale as the main site's copy—not Material's wide-screen + root scaling and not a second marketing hero inside the docs. */ +html:not(.landing-page) .md-typeset { + color: #a9b4bf; + font-size: 1rem; + line-height: 1.72; +} +html:not(.landing-page) .md-typeset h1, +html:not(.landing-page) .md-typeset h2, +html:not(.landing-page) .md-typeset h3, +html:not(.landing-page) .md-typeset h4 { + color: #f8fafc; + font-family: Oswald, 'Oswald Fallback', sans-serif; + font-weight: 700; + text-transform: uppercase; +} +html:not(.landing-page) .md-typeset h1 { + max-width: 800px; + margin: 0 0 30px; + font-size: clamp(3.25rem, 4.2vw, 4.5rem); + line-height: .94; + letter-spacing: -.04em; +} +html:not(.landing-page) .md-typeset h1::before { + content: 'RAYFORCE / DOCUMENTATION'; + display: block; + margin-bottom: 24px; + padding-left: 46px; + background: linear-gradient(#e9a033,#e9a033) left center / 32px 1px no-repeat; + color: #f3c46b; + font: 600 .62rem/1.2 'JetBrains Mono', monospace; + letter-spacing: .13em; +} +html:not(.landing-page) .md-typeset h1 + p { + max-width: 760px; + color: #c1cad3; + font-size: 1.12rem; + line-height: 1.7; +} +html:not(.landing-page) .md-typeset h2 { + margin-top: 4rem; + padding-bottom: .65rem; + border-bottom: 1px solid rgba(233,160,51,.24); + font-size: clamp(2.15rem, 3vw, 3rem); + line-height: 1; + letter-spacing: -.025em; +} +html:not(.landing-page) .md-typeset h3 { + margin-top: 2.6rem; + color: #f3c46b; + font-size: 1.55rem; + line-height: 1.2; +} +html:not(.landing-page) .md-typeset p, +html:not(.landing-page) .md-typeset li { color: #a9b4bf; } +html:not(.landing-page) .md-typeset strong { color: #edf2f6; } +html:not(.landing-page) .md-typeset hr { + margin: 30px 0; + border-color: rgba(255,255,255,.12); +} +html:not(.landing-page) .md-typeset .md-button { + min-height: 50px; + margin-right: 8px; + padding: 13px 22px; + border-radius: 0; + font: 600 .76rem/1 'JetBrains Mono', monospace; + letter-spacing: .03em; +} +html:not(.landing-page) .md-typeset .md-button--primary { + color: #15100a; + border: 1px solid #e9a033; + background: #e9a033; + box-shadow: 0 10px 40px rgba(233,160,51,.16); +} +html:not(.landing-page) .md-typeset pre > code, +html:not(.landing-page) .md-typeset table:not([class]), +html:not(.landing-page) .md-typeset .admonition, +html:not(.landing-page) .md-typeset details { + border-radius: 0; + border-color: rgba(255,255,255,.12); + background-color: rgba(11,16,23,.88); +} +html:not(.landing-page) .md-footer { + background: #06090d; + border-top: 1px solid rgba(255,255,255,.1); +} + +@media screen and (max-width: 76.234375em) { + html:not(.landing-page) .md-main__inner { + width: min(calc(100% - 48px), 920px); + display: block; + padding: 58px 0 100px; + } + html:not(.landing-page) .md-content { width: 100%; } + html:not(.landing-page) .md-sidebar--primary { + left: 0; + width: 100vw; + max-width: 100vw; + transform: translateX(-100%); + } + html:not(.landing-page) #__drawer:checked ~ .md-container .md-sidebar--primary { + transform: translateX(0); + } + html:not(.landing-page) .md-sidebar--primary .md-sidebar__scrollwrap { + background: #080b10; + } + html:not(.landing-page) .md-sidebar--primary .md-nav--primary { + width: min(calc(100% - 32px), 520px); + margin-inline: auto; + } + html:not(.landing-page) .md-sidebar--primary .md-sidebar__inner { + padding: 1.2rem; + border: 0; + background: #0b1017; + } +} + +@media screen and (max-width: 76.234375em) { + html:not(.landing-page) .docs-site-nav { height: 70px; } + html:not(.landing-page) .docs-site-nav .nav-inner { width: calc(100% - 32px); } + html:not(.landing-page) .docs-site-nav .nav-brand img, + html:not(.landing-page) .docs-site-nav .nav-brand svg { width: 116px; } + html:not(.landing-page) .docs-site-nav .nav-toggle { + width: 42px; + height: 42px; + display: block; + padding: 10px; + color: #f8fafc; + cursor: pointer; + } + html:not(.landing-page) .docs-site-nav .nav-toggle span { + display: block; + width: 21px; + height: 1px; + margin: 5px auto; + background: currentColor; + transition: .2s; + } + html:not(.landing-page) .docs-site-nav .nav-toggle.open span:first-child { transform: translateY(6px) rotate(45deg); } + html:not(.landing-page) .docs-site-nav .nav-toggle.open span:nth-child(2) { opacity: 0; } + html:not(.landing-page) .docs-site-nav .nav-toggle.open span:last-child { transform: translateY(-6px) rotate(-45deg); } + html:not(.landing-page) .docs-site-nav .nav-links { + display: none; + position: fixed; + z-index: 1; + top: calc(var(--rf-banner-h, 0px) + 70px); + right: 0; + left: 0; + height: calc(100svh - var(--rf-banner-h, 0px) - 70px); + flex-direction: column; + align-items: stretch; + justify-content: center; + padding: clamp(28px, 8vw, 72px); + overflow: auto; + background: rgba(8,11,16,.99); + border-top: 1px solid rgba(255,255,255,.11); + } + html:not(.landing-page) .docs-site-nav .nav-links.open { display: flex; } + html:not(.landing-page) .docs-site-nav .nav-links > a, + html:not(.landing-page) .docs-search-trigger, + html:not(.landing-page) .docs-browse-trigger { + width: 100%; + height: auto; + min-height: 0; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 14px; + margin: 0; + padding: 12px 0; + border: 0; + border-bottom: 1px solid rgba(255,255,255,.11); + background: none; + color: #a8b3bd; + font: 600 clamp(1.8rem, 7vw, 3.3rem)/1 Oswald, 'Oswald Fallback', sans-serif; + letter-spacing: -.02em; + text-transform: uppercase; + cursor: pointer; + } + html:not(.landing-page) .docs-site-nav .nav-links > a:hover, + html:not(.landing-page) .docs-site-nav .nav-links > a.active, + html:not(.landing-page) .docs-search-trigger:hover, + html:not(.landing-page) .docs-browse-trigger:hover { + color: #f3c46b; + background: none; + } + html:not(.landing-page) .docs-search-trigger > span { display: inline; } + html:not(.landing-page) .docs-search-trigger svg { width: 24px; height: 24px; } + html:not(.landing-page) .docs-site-nav .nav-github { + width: 100%; + flex: 0 0 auto; + margin-top: 12px; + border-bottom: 0; + } + html:not(.landing-page) .docs-site-nav .nav-github svg { width: 24px; height: 24px; } + html:not(.landing-page) .md-container { padding-top: 70px; } + html:not(.landing-page) .md-main { min-height: calc(100svh - 70px); } + html:not(.landing-page) .md-main::before { width: 100%; opacity: .07; } + html:not(.landing-page) .md-sidebar--primary { + top: calc(var(--rf-banner-h, 0px) + 70px); + height: calc(100svh - var(--rf-banner-h, 0px) - 70px); + } +} + +@media screen and (max-width: 600px) { + html:not(.landing-page) .md-main__inner { + width: calc(100% - 32px); + padding: 44px 0 80px; + } + html:not(.landing-page) .md-typeset h1 { + font-size: clamp(2.8rem, 14vw, 4rem); + } + html:not(.landing-page) .md-typeset h1::before { + margin-bottom: 20px; + padding-left: 34px; + background-size: 22px 1px; + font-size: .56rem; + } + html:not(.landing-page) .md-typeset h1 + p { font-size: 1rem; } + html:not(.landing-page) .md-typeset .md-button { + display: table; + width: fit-content; + max-width: 100%; + margin: 0 0 10px; + } +} diff --git a/docs/assets/landing.css b/docs/assets/landing.css index f292e75d..de2109a3 100644 --- a/docs/assets/landing.css +++ b/docs/assets/landing.css @@ -1,971 +1,186 @@ -/* ── Self-hosted fonts (latin subset) ────────── */ -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('/assets/fonts/inter-400.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('/assets/fonts/inter-600.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url('/assets/fonts/inter-700.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -@font-face { - font-family: 'Oswald'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('/assets/fonts/oswald-600.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -@font-face { - font-family: 'Oswald'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url('/assets/fonts/oswald-700.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('/assets/fonts/jbmono-400.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('/assets/fonts/jbmono-600.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -/* ── Fallback font-face overrides ───────────────────────────── - Make the system fallback render at the same visual metrics as - the web font, so swapping from fallback -> web font produces - no layout shift. Used as the second name in the font stack. - font-weight: 100 900 ensures the override applies to ALL - weights (including 500/600/700) — without it the @font-face - defaults to weight 400 only and bold text bypasses it. */ -@font-face { - font-family: 'Inter Fallback'; - src: local('Arial'), local('Helvetica'); - font-weight: 100 900; - size-adjust: 107%; - ascent-override: 90%; - descent-override: 22%; - line-gap-override: 0%; -} -@font-face { - font-family: 'Oswald Fallback'; - src: local('Arial Narrow'), local('Impact'), local('Arial'); - font-weight: 100 900; - size-adjust: 87%; - ascent-override: 100%; - descent-override: 31%; - line-gap-override: 0%; -} -@font-face { - font-family: 'JetBrains Mono Fallback'; - src: local('Menlo'), local('Consolas'), local('Courier New'); - font-weight: 100 900; - size-adjust: 99%; - ascent-override: 100%; - descent-override: 22%; - line-gap-override: 0%; -} - -/* ============================================================ - Rayforce — rayforce website - Bold / Editorial dark theme - Brand: #e9a033, accent: #5eead4, Oswald headings, Inter body - ============================================================ */ - -*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } - -:root { - --bg: #0a0e13; - --bg-section: #0d1219; - --bg-card: #111820; - --border: rgba(233, 160, 51, 0.1); - --border-hover: rgba(233, 160, 51, 0.25); - --primary: #e9a033; - --primary-light: #f0b954; - --primary-lighter: #f5d080; - --primary-pale: #fdf0d8; - --primary-dark: #c4821a; - --accent: #5eead4; - --accent-blue: #60a5fa; - --navy: #0e1b24; - --text: #f1f5f9; - --text-muted: #7b8fa0; - --text-dim: #3a4f5c; - --max-w: 1200px; - --font: 'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-heading: 'Oswald', 'Oswald Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --mono: 'JetBrains Mono', 'JetBrains Mono Fallback', 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace; -} - -html { scroll-behavior: smooth; scroll-padding-top: 80px; } - -body { - font-family: var(--font); - background: var(--bg); - color: var(--text); - line-height: 1.6; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - overflow-x: hidden; -} - -a { color: var(--primary-light); text-decoration: none; transition: color .15s; } -a:hover { color: var(--accent); } - -code { - font-family: var(--mono); - font-size: .85em; - background: rgba(233, 160, 51, 0.15); - color: var(--primary-lighter); - padding: 2px 6px; - border-radius: 4px; -} - -.container { max-width: var(--max-w); margin: 0 auto; padding: 0 32px; } - -/* ── Nav ─────────────────────────────────────── */ -.nav { - position: fixed; - /* Clear the in-flow release banner: main.js publishes --rf-banner-h as the - still-visible banner height (shrinks to 0 as it scrolls away / on dismiss), - so the floating pill drops just beneath it and rises back to 16px after. */ - top: calc(16px + var(--rf-banner-h, 0px)); - left: 50%; transform: translateX(-50%); - z-index: 1000; - background: rgba(10, 14, 19, 0.8); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--border); - border-radius: 100px; - padding: 0 8px; - /* Don't transition `all` — width changes inside the nav (e.g. the - count-up GitHub widget) would otherwise animate the centered bar - sliding sideways. Restrict to scroll-state visuals only. */ - transition: background .3s, border-color .3s, box-shadow .3s, top .25s ease; -} -.nav.nav-scrolled { - box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4); - border-color: var(--border-hover); -} -.nav-inner { - display: flex; align-items: center; gap: 8px; - height: 64px; padding: 0 16px; -} -.nav-brand { - display: flex; align-items: center; - margin-right: 16px; text-decoration: none; -} -.nav-brand:hover { text-decoration: none; } -.nav-brand img { height: 40px; width: auto; } -.nav-links { - display: flex; align-items: center; gap: 4px; list-style: none; -} -.nav-links a { - color: var(--text-muted); font-size: .85rem; font-weight: 500; - padding: 6px 14px; border-radius: 100px; - transition: color .2s, background .2s; text-decoration: none; -} -.nav-links a:hover { - color: var(--text); background: rgba(233, 160, 51, 0.08); - text-decoration: none; -} -.nav-links a.active { color: var(--text); background: rgba(233, 160, 51, 0.1); } -.nav-cta { - display: inline-flex !important; align-items: center; gap: 6px; - background: linear-gradient(135deg, var(--primary), var(--primary-light)) !important; - color: var(--text) !important; font-weight: 600 !important; - margin-left: 8px; - box-shadow: 0 2px 12px rgba(233, 160, 51, 0.3); -} -.nav-cta:hover { - box-shadow: 0 4px 20px rgba(233, 160, 51, 0.5); - color: var(--text) !important; -} - -.nav-toggle { - display: none; background: none; border: none; cursor: pointer; padding: 4px; -} -.nav-toggle span { - display: block; width: 20px; height: 2px; background: var(--text); - margin: 4px 0; border-radius: 2px; transition: transform .25s, opacity .25s; -} -.nav-toggle.open span:nth-child(1) { transform: rotate(45deg) translate(4px,4px); } -.nav-toggle.open span:nth-child(2) { opacity: 0; } -.nav-toggle.open span:nth-child(3) { transform: rotate(-45deg) translate(4px,-4px); } - -/* ── Hero ────────────────────────────────────── */ -.hero { - position: relative; min-height: 100vh; - display: flex; align-items: center; - padding: 120px 0 80px; overflow: hidden; -} -.hero-bg { - position: absolute; inset: 0; pointer-events: none; -} -.hero-gradient { - position: absolute; inset: 0; - background: - radial-gradient(ellipse 100% 80% at 30% 20%, rgba(233, 160, 51, 0.15), transparent 60%), - radial-gradient(ellipse 80% 60% at 70% 80%, rgba(94, 234, 212, 0.06), transparent 50%), - radial-gradient(ellipse 50% 40% at 80% 20%, rgba(96, 165, 250, 0.06), transparent 50%); -} -.hero-grid-lines { - position: absolute; inset: 0; - background-image: - linear-gradient(rgba(233, 160, 51, 0.04) 1px, transparent 1px), - linear-gradient(90deg, rgba(233, 160, 51, 0.04) 1px, transparent 1px); - background-size: 80px 80px; - mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 70%); - -webkit-mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 70%); -} -.hero .container { position: relative; z-index: 1; } -.hero-content { max-width: 720px; } -.hero-logo { display: block; height: 64px; width: auto; margin-bottom: 32px; } -.hero-eyebrow { - font-family: var(--mono); font-size: .72rem; font-weight: 600; - letter-spacing: .16em; color: var(--accent); margin-bottom: 24px; -} -.hero h1 { - font-family: var(--font-heading); - font-size: 5rem; font-weight: 700; - line-height: 1.04; margin-bottom: 24px; letter-spacing: -.02em; -} -.highlight { - background: linear-gradient(135deg, var(--primary-lighter), var(--accent)); - -webkit-background-clip: text; -webkit-text-fill-color: transparent; - background-clip: text; -} -.hero-desc { - font-size: 1.15rem; color: var(--text-muted); - max-width: 580px; line-height: 1.7; margin-bottom: 36px; -} -.hero-desc a { color: var(--accent); font-weight: 600; } -/* Keep the three hero CTAs on a single row: nowrap + compact sizing so - Download / Docs / GitHub fit the (grid-narrowed) hero column. The full-size - .btn-hero padding stays on the centered CTA section, which has room. */ -.hero-actions { display: flex; gap: 10px; align-items: center; flex-wrap: nowrap; } -.hero-actions .btn-hero, -.hero-actions .btn-hero-ghost { - padding: 12px 20px; font-size: .9rem; gap: 7px; white-space: nowrap; -} - -/* OS-detected download note under the hero CTAs (filled in by landing.js; - stays hidden until a latest release resolves). */ -.hero-download-meta { - margin: 14px 0 0; font-family: var(--mono); - font-size: .8rem; color: var(--text-muted); -} - -/* Buttons */ -.btn-hero { - display: inline-flex; align-items: center; gap: 10px; - padding: 14px 32px; - background: linear-gradient(135deg, var(--primary), var(--primary-light)); - color: var(--text); border-radius: 100px; - font-size: .95rem; font-weight: 600; - transition: transform .2s, box-shadow .2s, background .2s; - box-shadow: 0 4px 20px rgba(233, 160, 51, 0.35); -} -.btn-hero:hover { - transform: translateY(-2px); - box-shadow: 0 8px 32px rgba(233, 160, 51, 0.5); - color: var(--text); -} -.btn-hero-ghost { - display: inline-flex; align-items: center; gap: 8px; - padding: 14px 32px; color: var(--text-muted); - border: 1.5px solid var(--border); border-radius: 100px; - font-size: .95rem; font-weight: 600; - transition: color .2s, border-color .2s, background .2s; -} -.btn-hero-ghost:hover { - color: var(--text); border-color: var(--border-hover); - background: rgba(233, 160, 51, 0.05); -} - -/* Hero Stats — display-sized horizontal strip. Three statements, - no chrome. Replaces the older .hero-numbers icon-card grid which - read 2021-Strapi. Reads as a single sentence: "16K · 0 · 7". */ -.hero-stats { - display: grid; grid-template-columns: repeat(3, 1fr); - gap: 32px; - padding-top: 56px; - border-top: 1px solid var(--border); -} -.hero-stat { - /* Scroll-triggered: fades in via .visible class added by landing.js. */ - opacity: 0; transform: translateY(16px); - transition: opacity .6s ease, transform .6s ease; -} -.hero-stat.visible { opacity: 1; transform: translateY(0); } -.hero-stat-num { - display: block; - font-family: var(--font-heading); - font-size: clamp(3.5rem, 8vw, 6rem); - font-weight: 700; - line-height: 1; letter-spacing: -.04em; - color: var(--text); - margin-bottom: 14px; - /* Subtle accent on the digits — kept low-key so it doesn't - compete with the .highlight gradient in the headline. */ - background: linear-gradient(180deg, var(--text) 0%, rgba(241, 245, 249, 0.55) 100%); - -webkit-background-clip: text; background-clip: text; - -webkit-text-fill-color: transparent; -} -.hero-stat-label { - display: block; - font-family: var(--mono); font-size: .72rem; font-weight: 600; - letter-spacing: .14em; text-transform: uppercase; - color: var(--text-muted); -} - -/* ── Section Tag ─────────────────────────────── */ -.section-tag { - font-family: var(--mono); font-size: .7rem; font-weight: 600; - letter-spacing: .16em; color: var(--accent); - display: block; margin-bottom: 16px; -} - -/* ── Why Section ─────────────────────────────── */ -.why-section { - padding: 120px 0; - background: var(--bg-section); - border-top: 1px solid var(--border); -} -.why-header { max-width: 600px; margin-bottom: 56px; } -.why-header h2 { - font-family: var(--font-heading); font-size: 3rem; font-weight: 700; - line-height: 1.1; margin-bottom: 16px; -} -.text-gradient { - background: linear-gradient(135deg, var(--primary-lighter), var(--accent)); - -webkit-background-clip: text; -webkit-text-fill-color: transparent; - background-clip: text; -} -.why-header p { - font-size: 1.05rem; color: var(--text-muted); line-height: 1.7; -} -.why-grid { - display: grid; grid-template-columns: 1.5fr 1fr 1fr; - grid-template-rows: auto auto auto; gap: 16px; -} -.why-card-small { - background: var(--bg-card); border: 1px solid var(--border); - border-radius: 16px; padding: 28px; - opacity: 0; transform: translateY(24px); - transition: opacity .5s ease, transform .5s ease, border-color .2s, box-shadow .3s; -} -.why-card-small.visible { opacity: 1; transform: translateY(0); } -.why-card-small:hover { border-color: var(--border-hover); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); } -.why-card-small .why-card-label { margin-bottom: 10px; } -.why-card-desc { - font-size: .88rem; color: var(--text-muted); line-height: 1.6; -} -.why-card { - background: var(--bg-card); border: 1px solid var(--border); - border-radius: 16px; padding: 28px; - opacity: 0; transform: translateY(24px); - transition: opacity .5s ease, transform .5s ease, border-color .2s, box-shadow .3s; -} -.why-card.visible { opacity: 1; transform: translateY(0); } -.why-card:hover { border-color: var(--border-hover); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); } -.why-card-large { grid-row: 1 / 3; } -.why-card-label { - font-family: var(--font-heading); font-size: .7rem; font-weight: 600; - letter-spacing: .12em; color: var(--accent); margin-bottom: 16px; -} -.why-card-label-dim { color: var(--text-dim); } -.why-list { list-style: none; } -.why-list li { - padding: 7px 0; font-size: .9rem; color: var(--text-muted); - line-height: 1.5; border-bottom: 1px solid rgba(233, 160, 51, 0.06); -} -.why-list li:last-child { border-bottom: none; } -.why-list-solution li::before { content: '\2713\00a0'; color: var(--accent); font-weight: 600; } -.why-list-problem li::before { content: '\2013\00a0'; color: var(--text-dim); } - -/* ── Features ────────────────────────────────── */ -.features-section { - padding: 120px 0; - border-top: 1px solid var(--border); -} -.features-title { - font-family: var(--font-heading); font-size: 3rem; font-weight: 700; - margin-bottom: 8px; -} -.features-subtitle { - font-size: 1.05rem; color: var(--text-muted); - max-width: 560px; margin-bottom: 56px; -} -.features-grid { - display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; -} -.feature-card { - background: var(--bg-card); border: 1px solid var(--border); - border-radius: 16px; padding: 28px; - opacity: 0; transform: translateY(24px); - transition: opacity .5s ease, transform .5s ease, border-color .2s, box-shadow .3s; -} -.feature-card.visible { opacity: 1; transform: translateY(0); } -.feature-card:hover { - border-color: var(--border-hover); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); -} -.feature-card-header { - display: flex; align-items: center; gap: 12px; margin-bottom: 12px; -} -.feature-card-header svg { - width: 24px; height: 24px; color: var(--accent); flex-shrink: 0; -} -.feature-card-header h3 { - font-family: var(--font-heading); font-size: 1.2rem; font-weight: 600; -} -.feature-card p { - font-size: .9rem; color: var(--text-muted); line-height: 1.65; -} -.feature-card p a { color: var(--accent); } - -/* ── Architecture ────────────────────────────── */ -.arch-section { - padding: 120px 0; - background: var(--bg-section); - border-top: 1px solid var(--border); -} -.arch-section .section-tag { text-align: center; } -.arch-title { - font-family: var(--font-heading); font-size: 3rem; font-weight: 700; - text-align: center; margin-bottom: 8px; -} -.arch-subtitle { - text-align: center; color: var(--text-muted); margin-bottom: 48px; - font-size: 1.05rem; max-width: 640px; margin-left: auto; margin-right: auto; -} -.arch-diagram { - max-width: 900px; margin: 0 auto; - background: var(--bg); border: 1px solid var(--border); - border-radius: 16px; padding: 32px 24px; - box-shadow: 0 24px 80px rgba(0, 0, 0, 0.3); - overflow: hidden; -} -.arch-svg { width: 100%; height: auto; display: block; } -.arch-core-rect { animation: core-pulse 4s ease-in-out infinite; } -.arch-caption { - text-align: center; color: var(--text-muted); margin-top: 24px; - font-size: .9rem; max-width: 680px; margin-left: auto; margin-right: auto; -} -.arch-caption strong { color: var(--text); } -.arch-caption a { color: var(--accent); } - -/* Legacy arch-flow styles (for docs pages) */ -.arch-flow { - display: flex; align-items: center; justify-content: center; - gap: 24px; flex-wrap: wrap; color: #e2e8f0; - font-family: var(--mono); font-size: .85rem; -} -.arch-node { - background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12); - border-radius: 12px; padding: 16px 20px; text-align: center; -} -.arch-node-label { - font-family: var(--font-heading); font-size: .7rem; font-weight: 600; - text-transform: uppercase; letter-spacing: .1em; - color: rgba(255,255,255,.4); margin-bottom: 8px; -} -.arch-node-items { display: flex; flex-direction: column; gap: 4px; } -.arch-node-items span { color: #60a5fa; } -.arch-arrow { color: var(--primary-light); font-size: 1.5rem; font-weight: 300; } -.arch-core { - background: rgba(233,160,51,.3); border: 1px solid rgba(233,160,51,.5); - border-radius: 16px; padding: 20px 28px; -} -.arch-core-title { - font-family: var(--font-heading); font-size: 1rem; font-weight: 700; - color: var(--primary-light); margin-bottom: 12px; text-align: center; -} -.arch-core-tools { display: flex; gap: 12px; } -.arch-tool { - background: rgba(255,255,255,.06); border-radius: 8px; - padding: 8px 14px; font-size: .8rem; color: #e2e8f0; -} - -/* ── Demo / Terminal ─────────────────────────── */ -.demo-section { - padding: 120px 0; - border-top: 1px solid var(--border); -} -.demo-title { - font-family: var(--font-heading); font-size: 3rem; font-weight: 700; - margin-bottom: 8px; -} -.demo-subtitle { - font-size: 1.05rem; color: var(--text-muted); - max-width: 520px; margin-bottom: 48px; -} -.demo-terminal { - background: var(--bg-section); border: 1px solid var(--border); - border-radius: 16px; overflow: hidden; - max-width: 800px; - box-shadow: 0 24px 80px rgba(0, 0, 0, 0.3); -} -.terminal-header { - display: flex; align-items: center; gap: 8px; - padding: 14px 18px; - background: rgba(255, 255, 255, 0.02); - border-bottom: 1px solid var(--border); -} -.dot { width: 10px; height: 10px; border-radius: 50%; } -.dot.red { background: #ef4444; } -.dot.yellow { background: #eab308; } -.dot.green { background: #22c55e; } -.terminal-title { - font-family: var(--mono); font-size: .75rem; - color: var(--text-dim); margin-left: 8px; -} -.terminal-body { - padding: 20px 24px; height: 380px; overflow: auto; - font-family: var(--mono); font-size: .85rem; - line-height: 1.75; color: #e2e8f0; - scrollbar-width: thin; - scrollbar-color: rgba(255,255,255,.08) transparent; -} -.terminal-body::-webkit-scrollbar { width: 6px; height: 6px; } -.terminal-body::-webkit-scrollbar-track { background: transparent; } -.terminal-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 3px; } -.demo-mcp { - color: var(--text-muted); margin-top: 20px; font-size: .9rem; max-width: 800px; -} - -.term-line { white-space: pre; min-height: 1.75em; } -.term-cmd { color: #e2e8f0; } -.term-out { color: #94a3b8; } -.t-ok { color: #4ade80; font-weight: 600; } -.t-url { color: var(--accent); } -.t-cmd { color: var(--accent); } -.t-key { color: #60a5fa; } -.t-str { color: #4ade80; } -.t-num { color: #f59e0b; } -.t-tbl { color: #60a5fa; font-weight: 600; } -.t-dim { color: #64748b; } -.t-prompt { color: #4ade80; } -.term-cursor { - color: #e2e8f0; - animation: cursor-blink .7s step-end infinite; -} - -/* ── Use Cases ───────────────────────────────── */ -.usecases-section { - padding: 120px 0; - background: var(--bg-section); - border-top: 1px solid var(--border); -} -.usecases-header { - text-align: center; margin-bottom: 56px; -} -.usecases-header .section-tag { text-align: center; } -.usecases-header h2 { - font-family: var(--font-heading); font-size: 3rem; font-weight: 700; -} -.usecases-strip { - display: flex; align-items: stretch; - background: var(--bg-card); border: 1px solid var(--border); - border-radius: 16px; overflow: hidden; -} -.usecase-item { - flex: 1; padding: 36px; - opacity: 0; transform: translateY(20px); - transition: opacity .5s ease, transform .5s ease, background .2s; -} -.usecase-item.visible { opacity: 1; transform: translateY(0); } -.usecase-item:hover { background: rgba(233, 160, 51, 0.04); } -.usecase-divider { - width: 1px; background: var(--border); align-self: stretch; -} -.usecase-icon { - width: 48px; height: 48px; margin-bottom: 16px; - color: var(--accent); -} -.usecase-icon svg { width: 100%; height: 100%; } -.usecase-item h3 { - font-family: var(--font-heading); font-size: 1.15rem; font-weight: 600; - margin-bottom: 10px; -} -.usecase-item p { - font-size: .88rem; color: var(--text-muted); line-height: 1.65; -} - -/* ── CTA ─────────────────────────────────────── */ -.cta-section { - padding: 120px 0; - background: var(--bg-section); - border-top: 1px solid var(--border); -} -.cta-content { - text-align: center; max-width: 700px; margin: 0 auto; -} -.cta-content h2 { - font-family: var(--font-heading); font-size: 3.5rem; font-weight: 700; - line-height: 1.08; margin-bottom: 40px; -} -.cta-terminal { - background: var(--bg-section); border: 1px solid var(--border); - border-radius: 12px; overflow: hidden; text-align: left; - margin-bottom: 40px; - box-shadow: 0 24px 80px rgba(0, 0, 0, 0.3); -} -.cta-terminal-body { - padding: 20px 24px; font-family: var(--mono); - font-size: .85rem; line-height: 1.8; color: var(--text-muted); - overflow-x: auto; -} -.cta-terminal-body code { - background: none; padding: 0; font-size: inherit; color: inherit; -} -.cta-buttons { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; } - -/* ── Footer ──────────────────────────────────── */ -.footer { - padding: 22px 0; border-top: 1px solid var(--border); -} -.footer-inner { - display: flex; align-items: center; justify-content: space-between; - gap: 16px; -} -.footer-brand { - display: flex; align-items: center; gap: 8px; - font-family: var(--font-heading); font-size: 1rem; font-weight: 600; - color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; -} -.footer-links { display: flex; gap: 24px; } -.footer-links a { font-size: .85rem; color: var(--text-muted); } -.footer-links a:hover { color: var(--text); } - -/* ── Scroll-to-top ───────────────────────────── */ -.scroll-top { - position: fixed; bottom: 28px; right: 28px; - width: 44px; height: 44px; border-radius: 50%; - background: var(--primary); color: var(--text); - border: none; cursor: pointer; - display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 16px rgba(233, 160, 51, 0.3); - opacity: 0; transform: translateY(12px); - transition: opacity .3s, transform .3s, background .2s; - pointer-events: none; z-index: 999; -} -.scroll-top.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } -.scroll-top:hover { background: var(--primary-light); transform: translateY(-2px); } -.scroll-top svg { width: 20px; height: 20px; } - -/* ── Animations ──────────────────────────────── */ -@keyframes cursor-blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} -@keyframes core-pulse { - 0%, 100% { stroke-opacity: 0.5; } - 50% { stroke-opacity: 0.85; } -} - -/* ── Responsive ──────────────────────────────── */ -@media (max-width: 1024px) { - .hero h1 { font-size: 3.8rem; } - .hero-stats { gap: 24px; } - .why-grid { grid-template-columns: 1fr 1fr; } - .why-card-large { grid-row: auto; grid-column: 1 / -1; } - .features-grid { grid-template-columns: repeat(2, 1fr); } - .usecases-strip { flex-direction: column; } - .usecase-divider { width: auto; height: 1px; } -} -@media (max-width: 768px) { - .nav { top: 8px; left: 8px; right: 8px; transform: none; border-radius: 16px; padding: 0; } - .nav-inner { padding: 0 12px; justify-content: space-between; } - .nav-toggle { display: block; } - .nav-links { - display: none; position: absolute; top: 56px; left: 0; right: 0; - background: rgba(10, 14, 19, 0.98); backdrop-filter: blur(20px); - flex-direction: column; padding: 16px; gap: 4px; - border-radius: 0 0 16px 16px; - border-top: 1px solid var(--border); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); - } - .nav-links.open { display: flex; } - .hero { min-height: auto; padding: 100px 0 60px; } - .hero h1 { font-size: 2.8rem; } - .hero-stats { grid-template-columns: 1fr; gap: 32px; padding-top: 40px; } - .hero-stat-num { font-size: clamp(3rem, 14vw, 5rem); } - .why-grid { grid-template-columns: 1fr; } - .features-grid { grid-template-columns: 1fr; } - .terminal-body { height: 280px; font-size: .78rem; padding: 16px 18px; } - .footer-inner { flex-direction: column; gap: 16px; text-align: center; } - .footer-links { flex-wrap: wrap; justify-content: center; } - .arch-diagram { padding: 20px 12px; } - .features-title, .demo-title, .arch-title, - .why-header h2, .usecases-header h2 { font-size: 2.2rem; } - .cta-content h2 { font-size: 2.5rem; } -} -@media (max-width: 480px) { - .hero h1 { font-size: 2.2rem; } - .container { padding: 0 16px; } -} - -/* ── Utilities ───────────────────────────────── */ -::selection { background: rgba(94, 234, 212, 0.2); color: inherit; } -:focus-visible { outline: 2px solid var(--primary-light); outline-offset: 2px; } -@media (prefers-reduced-motion: reduce) { - html { scroll-behavior: auto; } - .why-card, .why-card-small, .feature-card, .usecase-item, - .hero-stat { opacity: 1; transform: none; transition: none; } - .arch-core-rect { animation: none; } - .term-cursor { animation: none; opacity: 1; } -} - -/* ── About page: fullscreen promo iframe with floating pill nav on top ─── */ -.promo-frame { - display: block; - width: 100vw; - height: 100vh; - border: 0; - background: var(--bg); -} - -/* ── About page content ──────────────────────── */ -.about-content { - background: var(--bg); color: var(--text); - padding: 64px 0 96px; -} -.about-content .container { max-width: 760px; } -.about-content h1 { - font-family: var(--font-heading); - font-size: 2.4rem; font-weight: 600; letter-spacing: -.01em; - color: var(--text); margin: 0 0 16px; -} -.about-content h2 { - font-family: var(--font-heading); - font-size: 1.4rem; font-weight: 600; color: var(--text); - margin: 40px 0 12px; text-transform: uppercase; letter-spacing: .04em; -} -.about-content .about-lede { - font-size: 1.15rem; line-height: 1.6; color: var(--text-muted); - margin: 0 0 24px; -} -.about-content p { line-height: 1.65; color: var(--text-muted); margin: 0 0 16px; } -.about-content ul { padding-left: 20px; margin: 0 0 16px; } -.about-content li { line-height: 1.65; color: var(--text-muted); margin-bottom: 10px; } -.about-content strong { color: var(--text); } -.about-content a { - color: var(--primary-light); border-bottom: 1px solid rgba(233, 160, 51, 0.25); -} -.about-content a:hover { color: var(--accent); border-bottom-color: currentColor; } -.about-content code { - font-family: var(--mono); - font-size: .92em; background: rgba(233, 160, 51, 0.1); - color: var(--primary-lighter); - padding: 1px 6px; border-radius: 3px; -} - -/* ── Mobile tap targets (48x48 minimum) ──────── */ -@media (max-width: 768px) { - .nav-links a { padding: 14px 16px; min-height: 48px; display: flex; align-items: center; } - .nav-toggle { padding: 12px; min-width: 44px; min-height: 44px; } - .footer-links { gap: 4px 18px; } - .footer-links a { padding: 10px 4px; min-height: 44px; display: inline-flex; align-items: center; } -} -.scroll-top { width: 48px; height: 48px; } - -/* ── About page: allow scrolling so content below the iframe is reachable ─── */ -body.about-page { margin: 0; overflow: auto; } - -/* ────────────────────────────────────────────── - Nav: Live Demo highlight + GitHub widget - ────────────────────────────────────────────── */ -.nav-link-demo { - display: inline-flex; align-items: center; gap: 5px; - padding: 6px 10px; border-radius: 100px; - background: rgba(94, 234, 212, 0.08); - color: var(--accent, #5eead4) !important; - border: 1px solid rgba(94, 234, 212, 0.25); - font-weight: 600 !important; - white-space: nowrap; - flex-shrink: 0; - transition: background .15s, border-color .15s, color .15s; -} -.nav-link-demo:hover { - background: rgba(94, 234, 212, 0.16); - border-color: rgba(94, 234, 212, 0.4); -} -.nav-link-demo svg { color: currentColor; } - -.github-widget { - display: inline-flex; align-items: stretch; - margin-left: 4px; - border: 1px solid var(--border); - border-radius: 8px; - background: rgba(255, 255, 255, 0.02); - overflow: hidden; - flex-shrink: 0; - font-family: var(--mono); - font-size: .78rem; - line-height: 1; -} -.github-widget a { - display: inline-flex; align-items: center; gap: 5px; - padding: 6px 10px; - color: var(--text-muted) !important; - text-decoration: none; - background: none; - transition: background .15s, color .15s; -} -.github-widget a + a { border-left: 1px solid var(--border); } -.github-widget a:hover { - color: var(--text) !important; - background: rgba(233, 160, 51, 0.08); -} -.github-widget-icon { padding: 6px 9px !important; } -.github-widget-icon svg { color: var(--text); } -.github-widget [data-gh-stat] { - display: inline-block; - font-feature-settings: 'tnum' on; - font-variant-numeric: tabular-nums; - font-weight: 600; - color: var(--text); - min-width: 3.5ch; - text-align: left; -} - -/* ────────────────────────────────────────────── - Hero: 2-column top with Python pillar - ────────────────────────────────────────────── */ -.hero-top { - display: grid; - grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); - gap: 56px; - align-items: center; - margin-bottom: 64px; -} - -.hero-python { - position: relative; - background: linear-gradient(160deg, rgba(96, 165, 250, 0.06), rgba(94, 234, 212, 0.04) 60%, transparent); - border: 1px solid var(--border); - border-radius: 16px; - padding: 18px 20px 16px; - box-shadow: 0 12px 50px rgba(0, 0, 0, 0.25); -} -.hero-python-head { - display: flex; align-items: center; justify-content: space-between; - gap: 12px; - margin-bottom: 14px; -} -.hero-python-tag { - font-family: var(--mono); - font-size: .68rem; font-weight: 600; - letter-spacing: .18em; text-transform: uppercase; - color: var(--accent-blue); - padding: 4px 10px; - border: 1px solid rgba(96, 165, 250, 0.25); - background: rgba(96, 165, 250, 0.08); - border-radius: 100px; -} -.hero-python-link { - font-family: var(--mono); - font-size: .75rem; - color: var(--text-muted); - display: inline-flex; align-items: center; gap: 6px; - text-decoration: none; - transition: color .15s; -} -.hero-python-link:hover { color: var(--text); } - -.hero-python-code { - margin: 0; - padding: 16px 18px; - background: rgba(0, 0, 0, 0.3); - border: 1px solid var(--border); - border-radius: 10px; - font-family: var(--mono); - font-size: .82rem; - line-height: 1.65; - color: var(--text); - overflow-x: auto; -} -.hero-python-code code { background: none; padding: 0; color: inherit; font-size: inherit; } -.hero-python-code .hl-kw { color: var(--primary-light); font-weight: 600; } -.hero-python-code .hl-str { color: #4ade80; } -.hero-python-code .hl-num { color: #f59e0b; } -.hero-python-code .hl-comment { color: var(--text-dim); } - -.hero-python-foot { - display: flex; align-items: center; gap: 8px; - margin-top: 14px; -} -.hero-python-cta { - flex: 1; - text-align: center; - padding: 9px 14px; - font-size: .85rem; font-weight: 600; - text-decoration: none; - border-radius: 100px; - background: linear-gradient(135deg, var(--primary), var(--primary-light)); - color: var(--text); - box-shadow: 0 2px 12px rgba(233, 160, 51, 0.3); - transition: box-shadow .15s, transform .15s; -} -.hero-python-cta:hover { - box-shadow: 0 4px 20px rgba(233, 160, 51, 0.5); - transform: translateY(-1px); - color: var(--text); -} -.hero-python-pypi { - padding: 9px 14px; - font-size: .85rem; font-weight: 600; - color: var(--text-muted); - text-decoration: none; - border: 1px solid var(--border); - border-radius: 100px; - transition: color .15s, border-color .15s, background .15s; -} -.hero-python-pypi:hover { - color: var(--text); - border-color: var(--border-hover); - background: rgba(233, 160, 51, 0.05); -} - -@media (max-width: 1024px) { - .hero-top { grid-template-columns: 1fr; gap: 40px; } - .hero-python-code { font-size: .78rem; padding: 14px 16px; } -} - -/* Mobile alignment for nav widgets */ -@media (max-width: 768px) { - .github-widget { - margin-left: 0; - align-self: flex-start; - } - .nav-link-demo { width: fit-content; } - .python-section { padding: 80px 0; } - .python-title { font-size: 2rem; } -} +/* Rayforce marketing surface — full-screen editorial redesign */ +@font-face{font-family:Inter;font-style:normal;font-weight:400;font-display:block;src:url('/assets/fonts/inter-400.woff2') format('woff2')} +@font-face{font-family:Inter;font-style:normal;font-weight:600;font-display:block;src:url('/assets/fonts/inter-600.woff2') format('woff2')} +@font-face{font-family:Inter;font-style:normal;font-weight:700;font-display:swap;src:url('/assets/fonts/inter-700.woff2') format('woff2')} +@font-face{font-family:Oswald;font-style:normal;font-weight:600;font-display:swap;src:url('/assets/fonts/oswald-600.woff2') format('woff2')} +@font-face{font-family:Oswald;font-style:normal;font-weight:700;font-display:block;src:url('/assets/fonts/oswald-700.woff2') format('woff2')} +@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400;font-display:block;src:url('/assets/fonts/jbmono-400.woff2') format('woff2')} +@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:600;font-display:block;src:url('/assets/fonts/jbmono-600.woff2') format('woff2')} + +:root{ + --ink:#080b10; + --ink-2:#0c1118; + --ink-3:#111923; + --panel:#121b25; + --paper:#f4f0e7; + --white:#f8fafc; + --muted:#8796a5; + --muted-2:#526170; + --amber:#e9a033; + --amber-light:#f3c46b; + --teal:#5eead4; + --blue:#60a5fa; + --line:rgba(255,255,255,.11); + --line-strong:rgba(255,255,255,.2); + --max:1380px; + --sans:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; + --display:Oswald,'Arial Narrow',sans-serif; + --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace; +} + +*,*::before,*::after{box-sizing:border-box} +html{scroll-behavior:smooth;scroll-padding-top:96px} +body{margin:0;background:var(--ink);color:var(--white);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased;overflow-x:hidden} +a{color:inherit;text-decoration:none} +button{font:inherit} +.rf-container{width:min(calc(100% - 80px),var(--max));margin-inline:auto} +.rf-screen{position:relative;min-height:100svh;display:flex;flex-direction:column;justify-content:center;overflow:hidden;border-bottom:1px solid var(--line)} +.rf-kicker{display:flex;align-items:center;gap:12px;margin:0 0 24px;color:var(--amber-light);font-family:var(--mono);font-size:.72rem;font-weight:600;letter-spacing:.13em;text-transform:uppercase} +.rf-kicker>span{display:inline-block;width:34px;height:1px;background:currentColor} +.rf-kicker b{padding:4px 8px;border:1px solid rgba(233,160,51,.32);border-radius:999px;color:var(--amber);font-size:.62rem;letter-spacing:.08em} +.rf-button{min-height:50px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;gap:12px;padding:13px 22px;border:1px solid var(--line-strong);font-family:var(--mono);font-size:.76rem;font-weight:600;letter-spacing:.03em;white-space:nowrap;transition:transform .2s,border-color .2s,color .2s,background .2s,box-shadow .2s} +.rf-button:hover{transform:translateY(-2px);border-color:rgba(233,160,51,.6);color:var(--amber-light)} +.rf-button svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round} +.rf-button-icon{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}.rf-button .rf-button-icon svg{width:18px;height:18px;fill:currentColor;stroke:none} +.rf-button--primary{background:var(--amber);border-color:var(--amber);color:#15100a;box-shadow:0 10px 40px rgba(233,160,51,.16)} +.rf-button--primary:hover{background:var(--amber-light);border-color:var(--amber-light);color:#15100a;box-shadow:0 16px 54px rgba(233,160,51,.24)} +.rf-button--cloud{border-color:rgba(94,234,212,.38);background:rgba(94,234,212,.055);color:#d7fffa}.rf-button--cloud:hover{border-color:var(--teal);background:rgba(94,234,212,.1);color:var(--teal)}.rf-button--cloud small{padding:3px 6px;border:1px solid rgba(94,234,212,.32);color:var(--teal);font:600 .5rem var(--mono);letter-spacing:.08em;text-transform:uppercase} +.rf-button--quiet{background:rgba(255,255,255,.02)} +.rf-text-link{display:inline-flex;gap:12px;align-items:center;margin-top:30px;color:var(--amber-light);font-family:var(--mono);font-size:.76rem;font-weight:600} +.rf-text-link span{transition:transform .2s}.rf-text-link:hover span{transform:translate(3px,-3px)} +.reveal{opacity:1;transform:none;transition:opacity .7s ease,transform .7s cubic-bezier(.22,1,.36,1)} +.has-reveal .reveal{opacity:0;transform:translateY(26px)} +.reveal.is-visible{opacity:1;transform:none} + +/* Navigation */ +.nav{position:fixed;z-index:1000;top:var(--rf-banner-h,0px);left:0;width:100%;height:76px;background:rgba(8,11,16,.74);border-bottom:1px solid var(--line);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);transition:background .25s,border-color .25s} +.nav.nav-scrolled{background:rgba(8,11,16,.96);border-color:rgba(233,160,51,.24)} +.nav-inner{height:100%;display:flex;align-items:center;padding:0} +.nav-brand{display:flex;align-items:center;margin-right:auto} +.nav-brand img{display:block;width:128px;height:auto} +.nav-links{display:flex;align-items:center;gap:3px} +.nav-links>a{padding:10px 15px;color:#b5c0ca;font-size:.88rem;font-weight:600;transition:color .2s,background .2s} +.nav-links>a:hover,.nav-links>a.active{color:var(--white);background:rgba(255,255,255,.05)} +.nav-cloud{display:flex!important;gap:7px;align-items:center} +.nav-cloud i{width:6px;height:6px;border-radius:50%;background:var(--amber);box-shadow:0 0 12px var(--amber)} +.nav-playground{display:flex!important;align-items:center;gap:6px;color:var(--amber-light)!important} +.nav-playground svg{width:12px;height:12px;fill:currentColor} +.nav-github{width:90px;display:flex;align-items:center;justify-content:center;flex:0 0 90px;gap:8px;margin-left:10px;padding:9px 12px;border:1px solid var(--line-strong);font-family:var(--mono);font-size:.76rem;color:var(--white);transition:border-color .2s,background .2s} +.nav-github:hover{border-color:var(--amber);background:rgba(233,160,51,.07)} +.nav-github svg{width:16px;height:16px;fill:currentColor} +.nav-toggle{display:none;width:42px;height:42px;padding:10px;border:0;background:transparent;color:var(--white)} +.nav-toggle span{display:block;width:21px;height:1px;margin:5px auto;background:currentColor;transition:.2s} +.nav-toggle.open span:first-child{transform:translateY(6px) rotate(45deg)} +.nav-toggle.open span:nth-child(2){opacity:0} +.nav-toggle.open span:last-child{transform:translateY(-6px) rotate(-45deg)} + +/* Hero */ +.hero{min-height:calc(100svh - var(--rf-banner-h,0px));justify-content:flex-start;padding:clamp(92px,10vh,118px) 0 18px;background:var(--ink)} +.hero::before{content:'';position:absolute;inset:0;background:linear-gradient(90deg,var(--ink) 0%,rgba(8,11,16,.96) 30%,rgba(8,11,16,.45) 60%,rgba(8,11,16,.13) 100%),linear-gradient(0deg,var(--ink) 0%,transparent 30%);z-index:1;pointer-events:none} +.hero::after{content:'';position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.026) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.026) 1px,transparent 1px);background-size:76px 76px;mask-image:linear-gradient(to bottom,black,transparent 80%);z-index:1;pointer-events:none} +.hero-art{position:absolute;inset:0 0 0 25%;background:url('/assets/rayforce-engine-hero.webp') center right/cover no-repeat;opacity:.72;filter:saturate(.92) contrast(1.04)} +.hero-glow{position:absolute;inset:0;background:radial-gradient(circle at 60% 55%,rgba(233,160,51,.1),transparent 35%);z-index:1} +.hero-layout{position:relative;z-index:2;display:grid;grid-template-columns:minmax(0,.82fr) minmax(560px,1.18fr);gap:clamp(40px,5vw,80px);align-content:center;align-items:end;flex:1} +.hero-copy{position:relative;max-width:780px} +.hero h1{margin:0;font-family:var(--display);font-size:clamp(3.6rem,5.2vw,6.2rem);font-weight:700;letter-spacing:-.04em;line-height:.9;text-transform:uppercase} +.hero h1 em{display:inline-block;color:var(--amber-light);font-style:normal} +.hero-lede{max-width:680px;margin:32px 0 0;color:#a4b0bc;font-size:clamp(1rem,1.25vw,1.22rem);line-height:1.72} +.hero-actions{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin-top:38px}.hero-meta{position:absolute;top:calc(100% + 11px);left:0;margin:0;color:var(--muted);font-family:var(--mono);font-size:.68rem} +.hero-actions .rf-button--primary{min-width:197px} +.hero-console{height:clamp(445px,34vw,490px);min-width:0;display:grid;grid-template-rows:48px minmax(0,1fr) auto 112px 34px;overflow:hidden;background:rgba(8,12,17,.91);border:1px solid rgba(255,255,255,.18);box-shadow:0 30px 90px rgba(0,0,0,.42);backdrop-filter:blur(14px)} +.console-bar{height:48px;display:flex;align-items:center;gap:12px;padding:0 16px;border-bottom:1px solid var(--line);color:var(--muted);font-family:var(--mono);font-size:.66rem} +.console-dots{display:flex;gap:6px}.console-dots i{display:block;width:7px;height:7px;border-radius:50%;background:#3c4650}.console-dots i:first-child{background:var(--amber)}.console-bar>a:hover{color:var(--white)} +.console-status{display:flex;align-items:center;gap:7px;margin-left:auto;color:var(--teal)}.console-status i{width:5px;height:5px;border-radius:50%;background:currentColor;box-shadow:0 0 8px currentColor} +.demo-code{min-height:0}.demo-code pre{height:100%;min-height:0;margin:0;padding:14px 20px;overflow:auto;color:#e5ebf0;font:400 .68rem/1.55 var(--mono)} +.hero-console code,.query-editor code{font:inherit;background:none;color:inherit;padding:0} +.code-muted{color:#617080}.code-key{color:var(--amber-light)}.code-string{color:var(--teal)}.code-number{color:var(--blue)} +.demo-run{padding:10px 16px 11px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:rgba(255,255,255,.018)} +.demo-run-head{display:flex;align-items:center;justify-content:space-between;color:var(--amber-light);font:600 .62rem var(--mono);letter-spacing:.04em;text-transform:uppercase}.demo-run-head button{padding:3px 0;border:0;background:none;color:#6d7b88;font:600 .55rem var(--mono);text-transform:uppercase;cursor:pointer}.demo-run-head button:hover{color:var(--white)} +.demo-progress{height:2px;margin-top:8px;background:#26313b;overflow:hidden}.demo-progress i{display:block;width:0;height:100%;background:linear-gradient(90deg,var(--amber),var(--teal));box-shadow:0 0 10px rgba(94,234,212,.35);transition:width .65s cubic-bezier(.22,1,.36,1)} +.demo-stages{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:8px 0 0;padding:0;list-style:none}.demo-stages li{position:relative;margin:0;padding-left:11px;color:#53616e;font:600 .49rem var(--mono);text-transform:uppercase;transition:color .25s}.demo-stages li i{position:absolute;left:0;top:3px;width:5px;height:5px;border:1px solid currentColor;border-radius:50%}.demo-stages li span,.demo-stages li small{display:block}.demo-stages li small{margin-top:2px;color:#44515d;font-size:.44rem;font-weight:400;text-transform:none}.demo-stages li.is-active{color:var(--amber-light)}.demo-stages li.is-active i{background:currentColor;box-shadow:0 0 8px currentColor}.demo-stages li.is-complete{color:var(--teal)}.demo-stages li.is-complete i{background:currentColor} +.demo-result{height:112px;overflow:hidden;visibility:hidden;opacity:0;transform:translateY(5px);transition:opacity .35s,transform .35s,visibility 0s linear .35s}.demo-result.is-visible{visibility:visible;opacity:1;transform:none;transition-delay:0s}.demo-result>div{display:grid;grid-template-columns:.75fr .72fr 1.3fr;align-items:center;min-height:22px;padding:0 16px;border-bottom:1px solid rgba(255,255,255,.06);color:#96a3af;font:400 .54rem var(--mono)}.demo-result>div>*:nth-child(n+2){text-align:right}.demo-result strong{color:var(--teal);font-weight:600}.demo-result .demo-result-head{min-height:24px;color:#52606d;font-size:.47rem;text-transform:uppercase;letter-spacing:.08em} +.demo-foot{min-height:34px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 16px;color:#53616e;font:400 .49rem var(--mono)}.demo-foot code{color:#85929e}.demo-foot a{color:var(--amber-light);font-weight:600}.demo-foot a:hover{color:var(--white)} +.hero-foot{position:relative;z-index:2;min-height:44px;display:grid;grid-template-columns:auto 1fr;gap:28px;align-items:center;margin-top:14px;padding-top:12px;border-top:1px solid var(--line)} +.hero-foot>p{margin:0;color:var(--muted);font:600 .58rem var(--mono);letter-spacing:.1em;text-transform:uppercase} +.hero-socials{display:flex;align-items:center;justify-content:flex-start}.hero-socials a{display:inline-flex;align-items:center;gap:9px;padding:3px 18px;border-left:1px solid var(--line);color:#8e9ba7;font:600 .6rem var(--mono);letter-spacing:.06em;text-transform:uppercase;transition:color .2s}.hero-socials a:hover{color:var(--white)}.hero-socials a svg{width:15px;height:15px;flex:0 0 auto;fill:var(--amber)} + +/* Unified engine */ +.engine{padding:140px 0;background:var(--paper);color:#12171c} +.engine::before{content:'RAYFORCE';position:absolute;right:-.04em;bottom:-.16em;color:rgba(18,23,28,.035);font:700 clamp(10rem,22vw,27rem)/1 var(--display);letter-spacing:-.06em;pointer-events:none} +.section-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:clamp(70px,11vw,180px);align-items:center} +.section-copy h2,.query h2,.capabilities h2,.production h2,.cloud h2,.open-source h2{margin:0;font:700 clamp(3rem,4.5vw,5.5rem)/.94 var(--display);letter-spacing:-.035em;text-transform:uppercase} +.section-copy>p:not(.rf-kicker),.capabilities-intro>p:not(.rf-kicker),.production-copy>p:not(.rf-kicker){max-width:560px;margin:30px 0 0;color:#53606b;font-size:1.06rem;line-height:1.75} +.engine .rf-kicker{color:#9a641b}.engine .rf-text-link{color:#8e5b17} +.pipeline{position:relative;border-top:1px solid rgba(18,23,28,.16)} +.pipeline-rail{position:absolute;top:0;left:0;width:25%;height:3px;background:var(--amber)} +.pipeline-step{min-height:112px;display:grid;grid-template-columns:52px 1fr auto;gap:24px;align-items:center;border-bottom:1px solid rgba(18,23,28,.16);transition:background .25s,padding .25s} +.pipeline-step:hover,.pipeline-step.is-active{padding:0 20px;background:#ebe5da}.pipeline-index{font:600 .67rem var(--mono);color:#9d7c4b}.pipeline-step p{margin:0 0 5px;color:#8c5c16;font:600 .62rem var(--mono);letter-spacing:.11em;text-transform:uppercase}.pipeline-step h3{margin:0;font:600 clamp(1.4rem,2vw,2rem) var(--display);text-transform:uppercase}.pipeline-step small{color:#69747d;font:600 .62rem var(--mono);text-transform:uppercase} +.engine-metrics{position:relative;z-index:1;display:grid;grid-template-columns:repeat(4,1fr);margin-top:110px;border-top:1px solid rgba(18,23,28,.18);border-bottom:1px solid rgba(18,23,28,.18)} +.engine-metrics div{padding:30px 26px;border-right:1px solid rgba(18,23,28,.18)}.engine-metrics div:first-child{padding-left:0}.engine-metrics div:last-child{border:0}.engine-metrics strong,.engine-metrics span{display:block}.engine-metrics strong{font:700 clamp(2.4rem,4vw,4.5rem)/1 var(--display)}.engine-metrics span{margin-top:8px;color:#69747d;font:600 .64rem var(--mono);text-transform:uppercase;letter-spacing:.06em} + +/* Query workbench */ +.query{padding:130px 0;background:#0b1118} +.query::before{content:'';position:absolute;inset:0;background:radial-gradient(circle at 86% 20%,rgba(94,234,212,.075),transparent 28%),linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);background-size:auto,72px 72px,72px 72px;mask-image:linear-gradient(to bottom,black,transparent 90%)} +.query-head{position:relative;display:grid;grid-template-columns:1.4fr .6fr;gap:80px;align-items:end}.query-head>p{max-width:480px;margin:0 0 8px;color:#8998a6;font-size:1rem;line-height:1.7} +.query-workbench{position:relative;margin-top:70px;border:1px solid var(--line-strong);background:#080c11;box-shadow:0 38px 120px rgba(0,0,0,.35)} +.query-tabs{height:58px;display:flex;align-items:center;padding:0 18px;border-bottom:1px solid var(--line)}.query-tabs button{height:100%;padding:0 18px;border:0;border-bottom:2px solid transparent;background:none;color:var(--muted);font:600 .68rem var(--mono);cursor:pointer}.query-tabs button:hover{color:var(--white)}.query-tabs button[aria-selected=true]{border-color:var(--amber);color:var(--amber-light)}.query-tabs>span{margin-left:auto;color:#52616f;font:400 .62rem var(--mono);text-transform:uppercase;letter-spacing:.08em} +.query-body{display:grid;grid-template-columns:1.12fr .88fr;min-height:440px}.query-editor{border-right:1px solid var(--line)}.query-editor pre{height:100%;margin:0;padding:42px;overflow:auto;color:#dce4eb;font:400 clamp(.74rem,1vw,.88rem)/1.9 var(--mono)}.query-editor pre[hidden]{display:none}.query-output{padding:34px}.output-head{display:flex;justify-content:space-between;padding-bottom:18px;border-bottom:1px solid var(--line);color:var(--muted);font:600 .6rem var(--mono);letter-spacing:.08em}.output-head span:last-child{color:var(--teal)}.output-row{display:grid;grid-template-columns:1.4fr .5fr .8fr;padding:17px 8px;border-bottom:1px solid rgba(255,255,255,.07);font:400 .7rem var(--mono)}.output-row span:last-child{text-align:right;color:var(--amber-light)}.output-label{color:#657483;font-size:.58rem;text-transform:uppercase;letter-spacing:.08em} + +/* Capabilities */ +.capabilities{padding:140px 0;background:var(--ink);color:var(--white)} +.capabilities-layout{display:grid;grid-template-columns:.7fr 1.3fr;gap:clamp(70px,10vw,170px);align-items:start}.capabilities-intro{position:sticky;top:150px}.capabilities-intro h2{font-size:clamp(3rem,4.2vw,5.2rem)}.capabilities-intro>p:not(.rf-kicker){color:#8998a6} +.capability-list{border-top:1px solid var(--line-strong)}.capability{display:grid;grid-template-columns:54px 1fr auto;gap:26px;align-items:center;min-height:150px;border-bottom:1px solid var(--line-strong);transition:background .25s,padding .25s}.capability:hover{padding:0 22px;background:rgba(255,255,255,.035)}.capability>span{color:var(--amber);font:600 .65rem var(--mono)}.capability h3{margin:0;font:600 clamp(1.8rem,2.8vw,3.2rem) var(--display);text-transform:uppercase}.capability p{max-width:680px;margin:8px 0 0;color:#82909e;font-size:.86rem;line-height:1.55}.capability i{color:var(--amber);font-style:normal;font-size:1.2rem;transition:transform .2s}.capability:hover i{transform:translate(4px,-4px)} + +/* Production users */ +.production{padding:140px 0;background:#111820} +.production::after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 15% 75%,rgba(96,165,250,.08),transparent 30%);pointer-events:none} +.production-layout{position:relative;z-index:1;display:grid;grid-template-columns:.72fr 1.28fr;gap:clamp(70px,10vw,170px);align-items:center}.production-copy>p:not(.rf-kicker){color:#92a0ac}.production-copy .production-note{padding-left:16px;border-left:2px solid var(--amber);color:#667684!important;font:400 .66rem/1.6 var(--mono)!important} +.customer-stack{display:grid;gap:18px}.customer-card{display:block;padding:34px 38px;border:1px solid var(--line-strong);background:rgba(8,11,16,.46);transition:transform .25s,border-color .25s,background .25s}.customer-card:hover{transform:translateX(-8px);border-color:rgba(233,160,51,.48);background:rgba(8,11,16,.72)}.customer-top{display:flex;justify-content:space-between;color:#687886;font:600 .58rem var(--mono);letter-spacing:.1em}.customer-top i{color:var(--amber);font-style:normal;font-size:1rem}.customer-wordmark{display:block;margin-top:28px;color:var(--white);font:700 clamp(2.8rem,5vw,5.8rem)/1 var(--display);letter-spacing:.12em}.customer-wordmark--fort{font-size:clamp(2.2rem,4vw,4.6rem);letter-spacing:.04em}.customer-card h3{max-width:640px;margin:22px 0 0;font:600 clamp(1.3rem,2vw,2rem)/1.15 var(--display);text-transform:uppercase}.customer-card p{max-width:650px;margin:13px 0 0;color:#83919e;font-size:.82rem;line-height:1.65} + +/* Cloud */ +.cloud{padding:140px 0;background:#0a1017} +.cloud::before{content:'';position:absolute;inset:0;background:radial-gradient(circle at 78% 50%,rgba(233,160,51,.16),transparent 17%),radial-gradient(circle at 78% 50%,rgba(94,234,212,.08),transparent 36%);pointer-events:none} +.cloud-orbit{position:absolute;left:78%;top:50%;border:1px solid rgba(233,160,51,.14);border-radius:50%;transform:translate(-50%,-50%);pointer-events:none}.cloud-orbit--one{width:520px;height:520px}.cloud-orbit--two{width:780px;height:780px;border-color:rgba(94,234,212,.08)} +.cloud-layout{position:relative;z-index:1;display:grid;grid-template-columns:1fr .82fr;gap:clamp(70px,10vw,160px);align-items:center}.cloud h2 em{color:var(--amber-light);font-style:normal}.cloud-copy>p:not(.rf-kicker){max-width:720px;margin:30px 0 0;color:#91a0ad;font-size:1.05rem;line-height:1.75}.cloud-actions{display:flex;align-items:center;gap:20px;margin-top:36px}.cloud-actions>span{color:#5e6d7a;font:400 .62rem var(--mono)} +.cloud-panel{padding:24px;border:1px solid rgba(233,160,51,.3);background:rgba(8,11,16,.84);box-shadow:0 40px 120px rgba(0,0,0,.45),inset 0 0 80px rgba(233,160,51,.03);backdrop-filter:blur(14px)}.cloud-panel-head{display:flex;justify-content:space-between;padding-bottom:20px;border-bottom:1px solid var(--line);color:#7e8d99;font:600 .63rem var(--mono);letter-spacing:.08em}.cloud-panel-head b{color:var(--amber)}.cloud-region{display:grid;grid-template-columns:1fr 1fr auto;align-items:end;padding:25px 0}.cloud-region p,.cloud-region strong,.cloud-region span{margin:0}.cloud-region p{color:#71808d;font:400 .64rem var(--mono)}.cloud-region strong{font:600 1.5rem var(--display);text-transform:uppercase}.cloud-region span{color:var(--teal);font:600 .6rem var(--mono)} +.cloud-chart{height:190px;display:flex;align-items:end;gap:8px;padding:24px 12px 0;border:1px solid var(--line);background:linear-gradient(to top,rgba(233,160,51,.04),transparent),repeating-linear-gradient(to top,transparent 0,transparent 46px,rgba(255,255,255,.045) 47px)}.cloud-chart i{flex:1;height:var(--h);background:linear-gradient(to top,rgba(233,160,51,.18),var(--amber));transform-origin:bottom;animation:bars 1.2s cubic-bezier(.22,1,.36,1) both}.cloud-chart i:nth-child(2n){background:linear-gradient(to top,rgba(94,234,212,.12),var(--teal))}@keyframes bars{from{transform:scaleY(0)}} +.cloud-stats{display:grid;grid-template-columns:1fr 1fr;border:1px solid var(--line);border-top:0}.cloud-stats div{padding:18px;border-right:1px solid var(--line)}.cloud-stats div:last-child{border:0}.cloud-stats span,.cloud-stats strong{display:block}.cloud-stats span{color:#647380;font:600 .56rem var(--mono);letter-spacing:.08em}.cloud-stats strong{margin-top:5px;font:600 1.2rem var(--display)} + +/* Open source and footer */ +.open-source{min-height:90svh;padding:140px 0;text-align:center;background:var(--paper);color:#14191e}.open-source::before{content:'';position:absolute;inset:0;background-image:linear-gradient(rgba(20,25,30,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(20,25,30,.045) 1px,transparent 1px);background-size:72px 72px;mask-image:radial-gradient(circle,black,transparent 72%)} +.open-layout{position:relative}.open-source .rf-kicker{justify-content:center;color:#966116}.open-source h2{font-size:clamp(4rem,6.5vw,7.8rem)}.open-source>div>p:not(.rf-kicker){max-width:720px;margin:34px auto 0;color:#5c6771;font-size:1.08rem;line-height:1.75}.open-actions{display:flex;justify-content:center;gap:12px;margin-top:38px}.open-source .rf-button--quiet{border-color:rgba(20,25,30,.26);color:#252c32}.github-mark{fill:currentColor!important;stroke:none!important}.open-stats{display:grid;grid-template-columns:repeat(3,1fr);max-width:900px;margin:80px auto 0;border-top:1px solid rgba(20,25,30,.2);border-bottom:1px solid rgba(20,25,30,.2)}.open-stats div{padding:25px;border-right:1px solid rgba(20,25,30,.2)}.open-stats div:last-child{border:0}.open-stats strong,.open-stats span{display:block}.open-stats strong{font:700 2.1rem var(--display);text-transform:uppercase}.open-stats span{margin-top:6px;color:#6d777f;font:600 .6rem var(--mono);text-transform:uppercase} +.footer{padding:80px 0 24px;background:#06090d;border-top:1px solid var(--line);color:var(--white)}.footer-grid{display:grid;grid-template-columns:1.8fr repeat(3,1fr);gap:70px}.footer-brand img{display:block;width:174px;height:auto}.footer-brand p{max-width:310px;margin:20px 0 0;color:#697785;font-size:.78rem;line-height:1.65}.footer-column{display:flex;flex-direction:column;gap:13px}.footer-column>span{margin-bottom:6px;color:var(--amber);font:600 .6rem var(--mono);letter-spacing:.12em}.footer-column a{color:#8794a0;font-size:.76rem;transition:color .2s}.footer-column a:hover{color:var(--white)}.footer-bottom{display:flex;justify-content:space-between;margin-top:70px;padding-top:20px;border-top:1px solid var(--line);color:#485561;font:400 .58rem var(--mono)} + +/* About page */ +.promo-frame{display:block;width:100%;height:100svh;border:0;background:var(--ink)} +.about-content{padding:160px 0 120px;background:var(--ink);color:var(--white)}.about-content .container{width:min(calc(100% - 80px),1000px);margin:auto}.about-content .md-typeset{font-family:var(--sans);font-size:1rem}.about-content h1,.about-content h2,.about-content h3{font-family:var(--display);text-transform:uppercase}.about-content h1{font-size:clamp(3.4rem,6vw,6.2rem);line-height:.92;color:var(--white)}.about-content h2{margin-top:80px;font-size:clamp(2.4rem,4vw,4.2rem);border-color:var(--line)}.about-content p,.about-content li{color:#98a5b0;line-height:1.75}.about-content a{color:var(--amber-light)} + +@media (min-width:901px) and (max-width:1300px){ + .hero-actions{gap:10px}.hero-actions .rf-button{gap:9px;padding:12px 18px;font-size:.72rem} +} + +@media (max-width:1100px){ + .rf-container{width:min(calc(100% - 48px),var(--max))} + .hero-layout{grid-template-columns:minmax(0,.78fr) minmax(500px,1.22fr);gap:36px}.hero h1{font-size:clamp(3.4rem,5.7vw,5.2rem)}.demo-code pre{font-size:.66rem;padding:20px} + .section-grid,.capabilities-layout,.production-layout,.cloud-layout{gap:70px}.footer-grid{gap:35px} +} + +@media (max-width:900px){ + .rf-screen{min-height:auto}.nav{top:var(--rf-banner-h,0px);height:70px}.nav-inner{padding:0}.nav-brand,.nav-toggle{position:relative;z-index:2}.nav-brand img{width:116px}.nav-toggle{display:block;cursor:pointer}.nav-links{display:none;position:fixed;z-index:1;top:70px;right:0;left:0;height:calc(100svh - var(--rf-banner-h,0px) - 70px);flex-direction:column;align-items:stretch;justify-content:center;padding:clamp(28px,8vw,72px);background:rgba(8,11,16,.985);border-top:1px solid var(--line)}.nav-links.open{display:flex}.nav-links>a{padding:12px 0;border-bottom:1px solid var(--line);background:none!important;color:#a8b3bd;font:600 clamp(1.9rem,7vw,3.4rem)/1 var(--display);letter-spacing:-.02em;text-transform:uppercase}.nav-links>a:hover,.nav-links>a.active{color:var(--amber-light)}.nav-github{width:100%;flex:0 0 auto;justify-content:flex-start;margin:12px 0 0;border:0!important;font-family:var(--display)!important}.nav-github svg{width:24px;height:24px}.nav-open{overflow:hidden} + .hero{min-height:100svh;padding:150px 0 38px}.hero::before{background:linear-gradient(0deg,var(--ink) 0%,rgba(8,11,16,.88) 66%,rgba(8,11,16,.55) 100%)}.hero-art{inset:0;opacity:.28;background-position:center}.hero-layout{grid-template-columns:1fr;align-content:normal;align-items:start}.hero-copy{max-width:700px}.hero-meta{position:static;margin:13px 0 0}.hero-console{width:100%;height:auto;display:block;overflow:visible;margin-top:35px}.hero-foot{grid-template-columns:1fr;align-items:start;gap:12px;margin-top:38px}.hero-socials{justify-content:flex-start;flex-wrap:wrap;gap:8px 0}.hero-socials a:first-child{padding-left:0;border-left:0} + .engine,.query,.capabilities,.production,.cloud,.open-source{padding:110px 0}.section-grid,.query-head,.capabilities-layout,.production-layout,.cloud-layout{grid-template-columns:1fr}.capabilities-intro{position:static}.engine-metrics{grid-template-columns:1fr 1fr;margin-top:80px}.engine-metrics div:nth-child(2){border-right:0}.engine-metrics div:nth-child(-n+2){border-bottom:1px solid rgba(18,23,28,.18)}.engine-metrics div{padding-left:22px!important}.query-head{gap:25px}.query-body{grid-template-columns:1fr}.query-editor{border-right:0;border-bottom:1px solid var(--line)}.query-output{padding:25px}.production-layout,.cloud-layout{gap:60px}.cloud-orbit{left:80%;opacity:.6}.footer-grid{grid-template-columns:1.5fr 1fr 1fr}.footer-column:last-child{grid-column:2/4}.footer-bottom{gap:20px} +} + +@media (max-width:600px){ + .rf-container{width:calc(100% - 32px)} + .rf-kicker{font-size:.61rem}.rf-kicker>span{width:22px}.hero{padding-top:138px}.hero h1{font-size:clamp(2.9rem,13vw,4.3rem)}.hero-lede{font-size:.94rem}.hero-actions,.open-actions,.cloud-actions{align-items:stretch;flex-direction:column}.rf-button{width:100%}.hero-foot{margin-top:34px}.hero-socials a{padding:3px 10px;font-size:.54rem} + .demo-code pre{min-height:190px;padding:18px 14px;font-size:.57rem}.demo-run{padding:14px}.demo-stages{gap:4px}.demo-stages li{padding-left:9px;font-size:.45rem}.demo-stages li small{font-size:.4rem}.demo-result>div{grid-template-columns:.65fr .65fr 1.35fr;padding:0 13px;font-size:.5rem}.demo-foot{align-items:flex-start;flex-direction:column;gap:4px;padding:10px 14px} + .engine,.query,.capabilities,.production,.cloud,.open-source{padding:88px 0}.section-copy h2,.query h2,.capabilities h2,.production h2,.cloud h2{font-size:clamp(2.7rem,12vw,4rem)}.open-source h2{font-size:clamp(3.4rem,14vw,5rem)} + .pipeline-step{grid-template-columns:34px 1fr;gap:12px;padding:16px 0}.pipeline-step small{display:none}.pipeline-step:hover,.pipeline-step.is-active{padding:16px 10px}.pipeline-step h3{font-size:1.3rem}.engine-metrics{margin-top:60px}.engine-metrics strong{font-size:2.6rem}.engine-metrics span{font-size:.54rem} + .query-workbench{margin-top:45px}.query-tabs{overflow:auto}.query-tabs button{padding:0 12px}.query-tabs>span{display:none}.query-editor pre{min-height:380px;padding:24px 18px;font-size:.66rem}.query-output{padding:18px 12px}.output-row{grid-template-columns:1.2fr .45fr .8fr;font-size:.6rem;padding:14px 4px} + .capability{grid-template-columns:30px 1fr auto;gap:12px;min-height:130px}.capability h3{font-size:1.8rem}.capability p{font-size:.74rem}.capability:hover{padding:0 8px} + .customer-card{padding:26px 22px}.customer-wordmark{font-size:3.2rem}.customer-wordmark--fort{font-size:2.5rem}.customer-top span{max-width:240px}.cloud-panel{padding:16px}.cloud-chart{height:145px;gap:5px}.cloud-actions>span{text-align:center} + .open-stats{grid-template-columns:1fr;margin-top:58px}.open-stats div{border-right:0;border-bottom:1px solid rgba(20,25,30,.2)}.open-stats div:last-child{border-bottom:0}.footer{padding-top:60px}.footer-grid{grid-template-columns:1fr 1fr}.footer-brand{grid-column:1/-1}.footer-column:last-child{grid-column:auto}.footer-bottom{flex-direction:column;margin-top:50px}.about-content .container{width:calc(100% - 32px)} +} + +@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}.has-reveal .reveal,.reveal{opacity:1;transform:none;transition:none}.cloud-chart i{animation:none}.rf-button,.capability,.customer-card{transition:none}} diff --git a/docs/assets/rayforce-engine-hero.webp b/docs/assets/rayforce-engine-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..4e9299e2b69d45044634e57692028730418e8873 GIT binary patch literal 235016 zcmV)PK()V8Nk&EhmIDA+MM6+kP&gp;l>-3K=?R?yDgXum1U^+Jk42-RAt5NX+9+@e ziDOHXa74jxR19PT#lA$}@afymUOnvoBqBtm&FZb|U+#X#C^R`S)-z`G;q7)|`KUkF z|ChFAUHV7){P}rz$ZxiOQ`g7+{pI}U{(m&@cK+MW*Ztp9f6%|!fB64v`~UO>`qldx z?uY+>?w7pZ^B?=aE&c?5XaA4;m+y1`x7Jtte^7t(-}V3h|K;`n|Nro1{*(WY|Ns6! z0squL@qhpJnE&VdHqQnk@?I0*Zwc`-|YOG`EmZI`VY##kYC7un*W~u;r?6xzxZ#Aue$%${-^!7 z?Prpv-TvSI5Bg7FUtYee{~P>I`ak|Z>V8$e&;Ez`UsOMy|6Tt-{9pQy++VOC>tEkL zi|NsBu|M@>@Je2=C|5N{;`>%i>=|A0n@c)+oPy3bh@%`uipYW^$-4k^ndJsaK6DmoBwM6r~VuLH~+8pKmY&!{q6t!|AGH6<&W;i|F8Mq^Zr`@ zfPc9E`~M&QIhJ-&vW=8%qh%W?*+$AasoKy!;z*!(eS*~OXWU7&0!YppHc_&Tlx(AC zv}~hg8z|XE$}w`*k$Wi8;f4?N$7W*I1|;Pc0xKXWr->DH@*&kfC#Y^C0D zcquh#=VdRsC2)D;&mMU5$DSLTDRteQnP-GzR|lSOuVt@y2W6|P{&K}3WB@aICMAt) zNcqM0o&kq52pt1RlmXm{CK~v9N6pw8t`9tU=KL*3W?AC3Rl(<^t%6CHY>yxt`4ML( zSJFO_^pB){Bk4&kS0lL{#>zd;WvYvma91OI=H|*aYH)#s3oCYZWuQ1UTpozm7Hg|? z>s8^pDy|PadE?I>dA|(IGF4>5KKvjtQ!};4q^k;lB*bAX%1pKa@0y zTF&#wo;!EL+mlKF)JPF@{&fQ2^T(OTW0}~r8z|2B-e`a-Ih9hf*%zD7_Rkced&u7^ z5zN8{5HNwpd>Rmm9=XRkSoxJBevXllJco5?$iJbsxm=QN19TO00@|bB2+C~&U2jSb=pIIqT`umZj!>>zEdoq)|BHf za@(eh7Ry~jpp9WEO6DR}%y?iE+;p}IO~CI@>(d(Su{%Pndg&}tV{BLOpVLauvYh8u zjAtg}q!;14=m{w}AFBC$VjChMKzAA*_$u08ZxxPfxQ@h46#fF!_pQDLPUWMOp5H`8 z<1Xo91FX0J04l1^Do)?37S883brbFb^FmvX&fPt#PfvX6{UE`86w)b>J+7c)&L!e9kIX z=_#km>QIk1I^QT-MVA`9+8ec$sWqGaMe86Hqz(Y*qZ2@6A?lG92Cj>c`iu{do2LI< zVBAXx%5QS%C~%;;x#y;1<_>aqWe4W%Yh*0p-iOS&L0;q)u%qSp1;`aL8hdMvkc$} zF;$a|i$t~aikN@Xa)&o=hgLR=2ElW6mjbFBN8WhZj7RpqKd}|&=XeNaG~42&24N27 zn+ANf-ejv|kR4Czrx>HqBzA0~w76qh3V)xY#lKCOAtnH;?>DiuaMmPR*BHK@)cfT` zU#b%|<`CM5L>#5>_^;7qn&Fn3^(A=W`pF%)uMJM{?+Ufe^H&U~M7ibg$0Wvs10n5L z0V&t{9wUVA%(ivlDi*fB9f!NmHiAIzIKCI-Sl1C7!`@nV9mS0odVyDFk-;_n0a(y# zN}DcJl!jpE97mXUntNs&HbJV}TQT>nSG|X~>b)~7XLp_5;syQ%9CjcaTv@R{ud<3% zX{unB+6w|N_ygcLjdr!V)4iA%NEU>T^zEn<4}2x=&W+wKBmR`7_AblNmBp^VS~(oQm$4Mq8Oq|^b?W~LENHM^btZQzT1`3;Ju@(mn2WPye^cpu zEGqsZ{hO#2Io~}2x{g(2L8CW4S$uM}G$f^%%ydzEZxv^Sq!r7P`G3?fXW$v?g8J99 z{BP9LqM}#FtZjZPuHx;I8Tt8Mhaq9jPr6F=4EnH(n2t- z(rc@==fi~NC=TQLXaLT=`5(gIG|J%hi2mR_PpphIt{_`Y^TMgme%ix1SKa_(Mfha5 zYVSqYO0Vt*Pfw*O1L~Ckw(6`Zv@`NN7zP;EwP<8L|5ajXgCxFi3ydH4(P=;UgnOR4 z=5xm3=F7rykE$DQfn;cVH^Edu%TuEAC8Ja0N#gjA8s%F)|K+-Q>0D!F>o3zFt4;u9 zC3+*z)vqBMSn#T58`KSJ(JP`pUglIT0fFkolA3@l2ij#~7JIDn*-&=`FRKe*3a&Xj ztZ*0VY|Z{hLz)xS?UYKft765VDZH%jErE15ctj_gg)U-(mDv?i9bq2Chg z?B#ju3SR~-C9SmG*VLZeVDpJH9NLrS#vpss7#KpO`xvH&7pvXzma0Z17Rr6!YyTM~ zy;}Z%#nSQ{;ogIY49Dx^kzzP3{yjHD{vkfIeGp7inWNyzn-fINYm$QDST*tUnq(?= zM0AK=plhAl!?Wc4PXy9)yi_4m)TI%I3Eo)LLO`J}R*DVd`tuFl$y(@ab5PA^ao$0o ziYd<6G9`NEX0?pktG-q%nh+yIt(s;*>8BDkIO>600`6|#-F@_c;3o_3ud3O057ZuN9WVoeSPxf23lU#sqVK9R)BAyH(`_umbt!*4dm1Nh2M+@_VXJF8s|L zb~h+MRpbYgW#^jnahY15Evl*02a^BpnfDVP>4c!XgRDrsq`q^wOa;u=Uyn3qzyqDE ziJ#x!eMfj2W1mRx@ysSp^;CW!mk5}FfblfWZ4u zK1x5k8wTYehf&mq_P%~I1Li*Fb|=$ay!UJ)S^Po4UtnR1Q#}2ENCEkO!FAskZiuW3 zC0EbD*BjTSHGZ1bMK9FZ(KlWs!=suj>}m7gChWLOCm2W)m-z>nfGNnsdIBN zE6IrIkkMs0$ADOlwjZ$3|NJ$>-icyzb8W@8z&wh`pJdtv2navjNWS+}Qh$wp>DeRx z_EhrIQh{rRuBr||Z?*WBuAaS5m2I^7v)JiK^6WfQ=cHK@f9^VS4H&|{FrrKKqwh|D zWX#v{X!X;{PU$v9eYD6!C&t#TTfNW*uQ*}+(T?z-4Zr{7`o~oVA8s*0m}l4Ti{p3{ z6r23Sul`8)PbNo(@zC$A6GMDI8ck|9|G2N3^3l8kkylhAniOmDv-<)yd5hvGS%2^< z`sw~3_KSN@E9PO0r0R8(=%~BzVEONlC+VQ_Z}>{z(RLk&sYI96&s{NCH@+dr*AZyUQxs_+KUOojFKnpq(lM*m&0Jvq+%A z`IZ}qLsx=Yag&VYB4}GA!siQ|+e;TxPbYZm`&m;O zYsrOl+6QKFae8KFwsn(i^=FU_-OsPYH$kj>G4}}{@2+2CLIV(c-RXpKNNzI-5U&xF z0Qo0P;=uE}Sh{kA5!ManA^kqF8_{P^?W>`-U3rPmYLkR(bx9zo0*<^1p4&lH=YffB$KZUcX&f zpw4JWszJ(+<24Ka<7N{iYJuT@ldBc3Szn4e_>SI_ zz1RtlFOw@}ig91bK~&2k{Yg8@o`ay$U9`hrUA#*LCBj_ilSyqLI^g@lLnn!A#B9w} zyf9mmC+$CKFSF)5VM1ekD8uehLt%#s5*2H!!+yPfPmcZPtGGVWV1`;|CTX2P^1ieOIb5?u=cjfa@Oeeb zm}itJ0j>7G@r!6XfKwtKx8k6WOdp8F;A5YhE4LzvYu@$7Hg~f_XhIjr1ZOLcA>l9m zxx7rLr&;+@jebzr?Ti0JhFj2sKb{PrA8uz=Swh;89Rzz;&gw1-j7LY!`}r!mvoGzl zz-fhKkCU_iCWkcM+Q#|bre~0%F7~|Ja6fG^hs9LFb?(PT)f;&TfeRI9bj=o}sg?hH z#lM{`424L@yG&1WAo@)$VeJqb{!c-snJgzPqLoSm#qPc&L2#V@ zYL0I|UD`*)I7A$6JU13$%A41B6C5=7Qts;)PwIM|K4%-ha}f`~Qx> zMT%&sM&Lx1&YDwK9R3~zi3!)%dC-Na`!e?9*eBRGIDKd7hw>%dXF_h9HAIZs(Li@g)N~Visc#SyCf@nPk>E07 zC-v<|eQ6OV2W#mT%PY|OBV-8NHRWUVL{<*C3c6`vUm^TU$nn32VIi(*F|nWyKa+7U z|8Qfnl;PKtzg;KjOHmqVxGmb{tYM@Dei3m49OtFiCO~W8y+671-SyYJcG9aO|J7wq zN=aM1h_@m5BP1XLE>XwUzy2Lo6S+RKwzf*(5~dv5vhm-#E7GOQH++zT(QVeUqjlCC zL}~2y0J`WM^L=&@&-j4i;}G`>qZG{=#M8VLIq#OSQYTHtDJ)W_Sv~7o(xbKOnGp6? zNXQma8eQKT#rp3(88YM5rE1Ui((RgMkql>k*C32e;oC>pt@LiD>J%hGddT&8e9TvJgwFp7Gzut{-ujHWTCio>a_@AJ)ivzf_-lb%e9(ZDCM5p<-}40^1h?>e_-h*Z)v8*3$46sGw*#d z<2*+vEPr3StObDb$$^rTopK}RdU3Tm{=}d?9H)7E4k>qB)sx!6%ratcQ8y|aungLM zukRcCYk|%eGBgO>%EEk8iGw?wksF5nj5}I)-3w=Nus6RI@F*hC! zqLpZgf=WM4*}Ja8d)FXqGrgMyLB;@B<%m6} zp?5kix~PFPii);Khc*P9@lo5bncf{A)BK)tiuTi)2I4~VcKxQ_$lu?qngYCo!wE^MzrCpoueDM! zCVq{rbsK_{GrCfne#E@GGoATAurS*2j_u|@r4ngguI^2pEA;gkdXb;YzlS5+p2><( z2KzU$ArqSL`KngxpdPjxrg0jwfA(8G|1zf6@NJLvwm_{kFZRx*NU%4lDn(JNcvo#f+z&rby8 zj;iZp58*gx)B=-S2?=vuQWwBc$B0%YcH^)8+lWE|Y#Bc8h(bSzhu8x$qq7TLr zl2C~cRo@q=hv6U4$*!?Pay9e+1$N+vAY^Y>BscOVmmOpmGejvmCH>b+*}1qLV5j{o zf`85RbRIKWx6-5KK;X-X)R~WplY~4zY%mNH2QF8&t#0gtMBj6;(u@K{R&goeA0eshnT=E_Uz-?N2(`5%8TCmvANT}Rcq@uJ#sf#i-}MK#A8VytZs&n-fD z_Detq+pq05FK;dw^)epkteiEaIVckO|JwfzXn1UA02)w+3)2(kzwrj|(5`;lB^Uh~ zm}$57aj^d0lgxMc3x^fjVoT$#;qBT_AV;FMOQ|B`A7)R})+B=_X8K+!=a;g^?j~2? zlK~+YOF5Fo7f6Gvu1T>M$|qC$Xu6IjTxkQqKu zdrvhp(hxSLE59HeYBo23V$EA~<@v5(7$F*v)frH}q$3o6;h{B6pqO$){tZFfTghvK z$xo<=@bD}kS>DLzmNxU@oQ-Va%HB%D4~?`qFc1d;_I&xCbUu@n3tcj)b=u`01PM5+$)xUVcN7g85KV~m z9_Zv(K~{h3wt|WNp7 z55_p;m;F`bC4%K3`BgTj^WgSj;NUtqqZq4Z_Lsz#U$`E1)V1&VJMm=iyW5$6LL83q zGJI`c4s5wOSJ{=D;~=b~&=E=AdcYGQo*jS87g2f6u#^eAxpiI#WSvw?j}s*!*hous zf)!@SQHK~e++rTJ+h+xI&(iA5$D+hL&>x!r6Vq-*pEv}gr_~t^JGd3W1j{bm;*1Kp$vD8iCf@D zxDp2E(bWes=5_1ZSp21nC~iWZ$FZixdl=sA$<%w?Js<#f?u@&89-{ZfJ}&@g6Wz~> zw!iCqL4H%X_Sx>zuLm3a#U)SwRbJo!MLUWBewsQRl1#C}BnS2EVwIRLRD!3I$K*-k zG|R2ZDV!*lVgK6~MNa42>08yE&dnCSxue%Y7zf*NNmwydTiWAyBDl|%LijawOIs+R zRllTwf8R%B7m3rWXxYULM(BgBDt)I%YBoNNi13H}_MIGs@nkgN5&6{+!O+rdEBIr#ly; zT*g8aenmRm^Y?Gl+??1AcW}DJBmc(-@*n(r9S=$>X=k4G-pCz0SJ-w>ze?@kU)a$?uXp8jMF-lf zN%iC$-RKd*=Ols_jCwr)=>sv|sSCDQQW7pr<444yXe?2J>P{H!b%UwQ2$ zy^~H3#*Rx}>E=x0a|s8AhzMp1J?N*d#c5Y`?pG@@TBAu1_LHOk|D4NlX(NP&Fje#e z4Wa4gS9#yOVIy@$+#=Ug zr@Xhov;bOXM$MtLqY?!gkW?k^D%R0AXA8J!BxV?G5nSp>fWT|-9uW-BiKHRKjg%{m zKmPZyvVbrP$#sLnJimN-C@mvL@ZQQ$AIo2yfG=ggM13%y$0_w(6W>2%zEnwM zV|Io6F!Fw~^kVk!kw&L58S(12HPfVQJPM?I7NaD zsnWgJUb5$l@&RDv5sZ9HF8^R;?$31q-&lLZalALg3;hzP(8Xu=l-6kXgN2(-C!?5w zzV1hcXcP!gkXRw*gyeQwFk#HJt-1Ps$G!2j(ur;5DFgGehRtc!+QSHIKGqZ zL2%X=@Dw2~43XebWgqTtzrFA5RO2>EogJjOPB(h7`3l%T+auD#9lk&AQEZf7#iO_X z-kqIJ+$#%$mk0U;wDOjTX(a~wVeF)kVCQ%?#Lt6}7MQ}l&xbOI_pL`DES9STZ~m^M z4%2hkurZJ=v}$q}Vt!ry^R*@tH$NFsx3>cINY2-_mj41B8l;+0czq5ho5mT@uVu)T z9-Z9fa9(=9j>Ml7dCMS?Qj~fUKjvG(V!oY0`)h^4?}45TLk-r&>I2?`>Ieiz>=whp zHBAqGGUXsM{U_(VF|RQq&mh#Z_u~NYRTpgn1s~Bh{Kx_#vA?rfHb=(nG|;>Cqo((e*Siv%*^&a**A4Z`6 zr=Pg^M?QV%F+M60_X87v9FR|xwMhxr&;9(#faeq;n|iWT2X>MUAh#z87LDA01Af1S zHMU}}+i zC-X%ASF5|aE&h;>2qFBeuHlpNdvzX_N0e(jA_4hMS7sFw1fB~NS+xA+`Q;QRzMYNL z9jJDfYhS*(M`Z4!0y2fw#_mEX5dku>TBM45lA1%RF{R$NZuAlxdRrAFz*gUFxh-L@ z4!aWfK_}U%sm(cRc>#q?MR=N@+~FCgi!{A3tV^qC0LOTN)Lh5Xi^E)C6+u`Fwi8Na z)$WDjO3T{=8gtrZDSo~BBq%=La^Cc>xCiaYkvo$4*TB*Sp`hulI87icj}eDx9$1S- zTE?PvsGSVZx6j!!vJKu8@1dm`YLe$(n!mw`<~9{-9U#9FH+YRv@ZhGd7*bIBj3mAJ z`>)z}i9RyiiA_&usD9e+abDt;$C(F3qDzWp_k3(#SR@!4_M*8>Xv(mW4NK0G$liNd zD?B475(?P4F$0#=UF*#PEEQ$&o)>8!MYcW zWPbCfAGvzh^H31|{`8MVc9wgq-?75BfZ}v9I^&hIb4)*<)aU`2QAL4YAr`zN(J{eH z_0hKIq=_h@e?XY!>Jrq>&c))1fKW|uWANoeUvNK@O>f%68lhD>G6#|+GOCca7yxg^ zH5DnO@_4vtGBcDisxuToLxG5NQwTNJ4tW=lv6pnEjG~DcjnFgmiw&zx|Jcf!TDvwt3-duL|_a zw6lmuQI@=5%=z`}HyCk;`OxNcVziU(vTj?{ZrEoj10YnBM?XCwdk)tX z3q4MejFw4Q(M2EBvw?ED5>6r0y;Sgd#z0=?c8)OEGI)SOVR~`e?G!CJoBcYR1Fvqd zpK?7l{*~eM&*_DFudFpBTQ$a>g0SSKg73NGWNenrz<@HYohxG!KJO;aEH_iMtR3Y? zD4`Io(VTK?KKHyIKUSxPFF)lZcC!Xvq=SFdw#cz`oWOMoTV+S<<-Jmpsd*i?kR?_@ zs)7fE;%sRDkG*+N#!-z?rXJ^YC)l zq#J{iVN!;adZI$4)~^6I3VCg9RkzSBdLoOQ945-(*W-Q8sgfqF+VpdGu1I>44A5cW zZbe^7e-zyI3XO}Ah!Y@y!*2a6P^I%C#&7lBWvp>xp`qyc$LlL>`$3@SM<9MRZ}&l6 z*FyFV77a%jHgjjK7)U3B*Z;V(vW34D5eD3<%iI|w3<&nb-ZoKsIba?#<1nRH*h~Zk zChUv;=j8s6m$%hsQH`so%Acoir6yEPo4b>nv@o)lt6!EB&>vf%!VBI&X^?T}Emg_o zLsMS!m+LW#(cUBQZoGfz;Ct&>uOfHhrGK$ynB+k(|PuF)PV=bdp@L{MObh&?1WYA;*)3Q%tAs8 z>e#rPgMWu6zgtw)&cJ?&H&@L^gWS!5*i2g;dUc5ujOZk(AKbtg8?@qR=O48dEDY?^ zkDBiuo=KNsWPykeLQ0Z<_|K^w@N?J5X*~Snbou*o!_vJhfrJEpKbf@88!0cLT+A){ z6ES=xhq%Ba%R#Jtx_A!BI!Y4;qh;3U%?l`LW~neESsdeMQW`q4fI?$MbtN}5DG}TA zW$tB{8VWQo5=!Coo%BoL@!aYnwK&Zck3bLnl1>gQ}?S9D?gQ(ko zEJOdj^L&A%s0#)~&$0u%_1YMZtfct||DcGt-s7xG>Lu>Z#ma4ySsB#o_ZovB#F540 zV5%uSv12Y!2_kUDJdMqa_dQw0Y&1T6NiFQ?sIKpnuRXE)p|tH{qyp1-=+e5*tt|YU z?f(5AvTM+5HX!Q0ue=tbFxOJ~hY89$CL0d5FW?E4!?j zZu@8GEm&o2^>C5iplB3RN)@s8!RKo-_r=h~x$=T$>U4r5kMN^osL-U2Hpm=E)(V_OCzS0`MO=yob`Kep7M^t z)}KehPf6^XSE=Sdvta?2Z>{gS0jBIU{_R7}pHzOVZ1Y`;omXWiz!D{6e0ERAsVQNf z%V2Ys97!1uB~q?py&(;<*cV7Io%6UERDxi5RK#u%dl6L#!nR1e|in-vh; zbPhCn-9M!Eet_w+uy6mB%{|y68O$A>Mi9#qy=FCWdkghCuWs4UPtCojjsE!Hh-<&m zwQtvfvl|6DPmvs4_D)S_j(d4$LwS3!V@Y{-SXT1dTo=F&a0;Abj9vHS#u7L25Z{C> z1%kGN=ut!av(CfWGc8Mcn_yGu$Y^owENyBKUqq`jkor%#Z(qA|H2~ZlyV)z^QU=TD zHnucQe3Sk9R8<7iwo`KEBf~K~0v(kJ}vp7SG zgZb;ED1C$5@7fukf1!VTsg3oxrAP6{QDcBbtcux)o_DANVlTz|VwVvr#k~$g#CJHS z*ygCzzv@#I6uDYv%nk8~InumBjUh(ezlyiMcfooKzpG6phaUU<#gTIz-$`{`!F@Ga zyhl&~ce3=K`Z{I0f2Yq|$^0RjstL|54%?A^&f(RRLCZK$nr{BnQhB1?XHZpu6Le~+ z=$)d4GIWZvsny~z|4Yjl%KQE|e1CL)pXamV7pWJglX7fWjKsUxx>UWZZ;c1K!~cG` zvD|ZjUbjw)N-WEc)J5dM<8B*wp+0-l&@~El(MjXFTXRdwK0)Yy8u5VmKk1VJANx|y zKaQF5V^|$&*&d_>hcY$LwmMwTjE)$tc!eX@#O{}!fuTd*?R^OagX4R|OH0Wmev-g58i65Z{>B7Y z*`9^!X|U3^J0P26hwEr%mmr3f^CA&4aS zmgSE*U%nRI^2ZYysb7#-K zyti0g^Foc_9Dn=rv{zQUIRSw;w9*}wq+fyfw^FWBiP_*dGi-r?j*Au9dVfc{ofL#HBks{%Tjz$tQ{vjgR#IihxsgwS@l24> zZlkmjdzbZ_Y;(>yZLyVLvvkz8y17(#g;t>QnJb}V@v=AHByS_*wUJclKzVEEZ_Cxq!a2PcXf;oP`yKfO)#0=}04| z!Jw0hysYcPgwx{$m5lw>1c>3kANSeYyzle24wg*+XO#Ld39=0(R*o#ca0?CQpXt)b zK&D3n19shg-c7vlcC&dG9VwsAEcWSBH$l;^{xjltzU&fZ+r+E9Zd|PU)ECropgDJm zOk)MOm{)f?!^F$U?d7rB)ZX}BPmBtDb|U`tlF~nvUqr^?4AjLAwWlB34TA=rOJrqN zv1c{Ag@b-~K%D-&uZ)_&4Bfe2H18hh2e$t8sHV1z{VY<-ujXtn#p`HKs7C|t{C5_y z19QTy@M&U{8k>7>RS40Cpl{*8s|`J``Q75@!2Uy<>VxEx>`~W z-uZ_tt^8>kX1iECeT#PfEms6xcL=wuK|+Q!;L`MH|H=*2%(QGLAX~`>^`Iudy32vh zW_8F+#-;||E zWW%pXkVPZ&)2iS1gs3^l6xDsbJwFf~f(xo55vpEJ6twEpd{BRtS;(Ph&+(I1di$}5 zi^6AckSsBN^e?pfXga*&E&lM!B}J85q3ko{Tef<@S_xMpE zA8=li&JR%Fjc!rbofmKYN4o(12PW~_e|p{t2=DQxF|Sa$9nCa?lw(gaW_-0F#QO-+ zG`swqx_h)MY-H9i*X=qeoA0LcwhWH#yip)}l8bt@1LEuQAG(udarcY&p<*sGr{M}F zb5Z1Fqk%X0R<#ey9^zRZM{b9YD`EYt|H;}}EJQo@pdFJ{!feBevY=o=mk9@C0&%t0 zvXm+-83L@hl-+D(y+*cfJ>#d(>IRcY;iy{pWGFE0SrmH%H?3F!O#}BBXl9L<-F%hT zMW6+^Y})mx)3h}i`m8;nDx>(jKfEB_A?aSnKXpBSMO3|&?g3E9@108d0HZtteq*2? zo`t%4rlt-*{slq^me>o3kPY*CK%B;}=`oS(=hvQhX1SUbSMy&wVU|DFyP{B}Q0A2t z*Y+w-fEm0^?P7T4Zr8RSaE0lR5l3n=Amcf}I~J^AJ@1lbnY6K_^|Kecdbh;-GLzRC zDme@)>%D$S8J!@l_Hbr}H<5MH7GhDcg4WrE-FfL{suyE7C_`wi55N>Ou80)q`^^rN z({_H1_a`QEKqlRgV+b>MqZCsz{aRA5u{I;kO;l&9ll}m;3f}``X8X*+y>^L;TFA4Z z@$@`j9oRLdb^G9X`gSgfYJ}}2VEEF-9;x9Ze~A4Iu$RXB!H8<5K7 zoRpsIOY})kCSfjxg$eK8jIqLR$J)iL>z4?@Pf{L3p2NT-Tr{R1M#hxQ+rO}LQLH>3 z#~X{?-}BTDq(s%~yuPT*n%1~5`Y?0sH#9`+O>cvyCo_bS_7nGoO*C#{5�+*g)(V;>4Yml`yTh)UQ(&f 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%0Dp-ge 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

;q#vZ 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

FRC7@@4a*vF9kl2mhO`OZw z|KqsEu1I&}$3#rT{w`%>4oI&#lt2L%L?br4 zYgPpQjY5Yi&2FJIWD)?mi~0%h5nR~Ma9rPhyxKS&Dy&#cmym1_eRbn98+gwCMac88 z!<+=PuNa`}cVxm+^{_gER>ThUyvyL?mB@tVD1lmtICVCBSs$fSn@&S3{UE>P@yB+c z6(`Pep3J0C`)M-qZLJ>vL$TbE0uh@h5ZHK0s|~p`8lwul31o6tc3uhFzG<}Yz00+m zJRI0P!YMqDBx|q=Ucg54BE;phj-9`;Gxg1x$j#u<>5n;W)IQ14r&vk z-s!sml5#^rug$9M1uz6=r`rtF{vopO)u&7vP-AJZTLT1P)$n@(@1@YN)dP_hJDeT^ zcpBqwHK{}AfyFe91-6%xp50tI#FF>|+pA1_3It;7EQ zkQIn$n|G*3_aoZigT`@(VND+>C;cIvB);n}c2@OE9-0}1*h$I*tjTYuZphkI+0AT5wYhjh-*2<+`8;X;B; zkEFzihse8-x$errC-E$<`ceYL70CvlB{jtnQoDVci&U5ueQvqUfe}raef#y9b%RA6 zc=LzUc($}VXlR47=sqDv+jew5SCL-!2QY)_0xM3Wt=ezHj+11}Ico8jpMVNv)7|h} zm5Ztu>%c+Umas9d;5oIWojBIY*}i6Vd7S(+xh_<6ygF2udGybTw+(zqAAguk*wp+~ z#wEE%f(Wq16yo|>*{*+K!_BT?-!q^7`PEFnpK*8s7Hb5pw?h;UMADzOispaQz1XPe zU&`Ci{9q->L!^j0x?&?3b4|}~}OsQt52aJ#sFJJ&P zN7X}0zc$hUqsmeQJ5*nS3*JL?nYC1TvcIFGhj(<`HxT<3T*6{!Eem&h*x)y2 z>@w~?if>J96wik(>l4>N{1>9g;UinlT@s)~N3LmS{AMMm(-~U*iLUD-mt$}-z7|2g z6E`DPHG5Z_`G*|Zr6^!^V*)_ooZA(&M7I4b@&x4v=5 zmw!FNx7k?W_jy&j6GeBQvFA}_dY}=4kw-p_oHBT!!~%-CbP|GL{|aN-B`x4MhtV`I zOr0P~JtrBgESTgw#s5`;`nUT(4fpvP&^SF0=(5%u-7~#QA7}k}0-j@)>!1^3w*5lU z@n{(s0$3bcypf*I6eS=Vy`@fW%ENRKsoJ5zx>cA|N@!P4dg6X_NMc z{51U7xb%PM$)x%D_m$T*b5(-&dzq|7*xDpDK`QaQlFao^-qvf8Oc#?}H-7z9(xdsn zTHW<)Si%Cu>=+Grs1}}xPGm+=c+5R6Ke$gsn1Qtx;e`^0xSWk25I)m-wdEPB`p1~uij&QBA#PPtXRoi+ zJr%dN(4s~Bn;s_<1m%g;K+VZCWZxvo08)ab;&|9HI8UttGDta$jPZjc1t0_< zcGt~dbuf)0|EGf3E90NlhK>N&OPvDP6d-kE2~IwVH#0+SnV}6-kMH`)&}v=xeOZsA|W@}^^z_SCo+81$FRBnFPxRX zOsiii?W4>2lH1!JZ#aTI-VtZZo6r8m&)LFK=dkS+s6B9zpoLSZIxB^AJ-SlC;KVFp z!Jcu|fk;{OfsFVcWnjhmc-~eqKv%5w?*HM7M_!SP)kFN^#A1pi+6aXS4>7<|^^8B- zIl`wgO!sq4#uErOVUZOBcvNn9g`DdWJG{q-o@X;VM9LLA>v!Ku(88kA{Q=6|W_x{v z)ABaNkchk2&DE8+DbLc1L2r5m~s;^&869F!_1Kz--oK z_dpFIQJGvg57fawUdDfj)U0FXJm&l6F$@4<8mLn2CvV;?>S6agPQ@$-9z$sUg%-Z^ zuWswW=4)X?mpmxLPbGXBMeRz-D3QU}jFYsa{d9B&hpa&c1wyaO9928yF;{i1I2|hq3y?5)sp!^UR9MDmelK5+Ub2nYV@G$m19sh5Q?0Dix+zP)gu%4%GY_CY-1O-wBmPT74wv*n^QlXPT$p4Jpw3(LP({< zaPbS}mg60KsL(8(vfcKwyFunyIo}t{^z~nnENg}Z%c+d62w`LTZ#X2t>F4k{iFk4lwf&gdDOk81#m)Q?r{7q^ zK9gvjq(tWjubphj1hfVI)gi;KNxm0L?y^ql<-)YS*HoyD4t;fV^ZuGxX|U={SY~xV zALwuZhTG}FUfJ-C;z#ZAj)ogaxIM8oN=i(xmLY?W9Cq_rsCOUfHQgX`vb>B`&|WWZ znWPZQ8M4-~xjO7JsfOi}LB(zp``^^TOhV>Y&dg=KTsP@#|8JdsrU}Fu?`Q z(#lp#a=Bz>Gjc^P0+~p;QyXB${S0aiLPA_^0 zA;_ba4_tm7glEC*;~sGFfv&r7yP?D4nVOv(eN)x6?PoqX0Q=f1+LK8lxFD_fIy&(1)7Z&#p=98Z36daaIq4A z{wq!tc+;$Z>XYgR1@gl><4}){;GUOIC?Dj0T7NI|4Rv74qgM@QayN=p;PBqb1c0c?T6IpH2#n?-+k|LSa{z2JOt~0*41L}D5#W~gzo(Y8!3{&zPh>3$p z|4q~?{Oth+tF|^3tB~|Y#h!@{<)(CvBLAiCO5;v;E6N_ ztTokkk^ZJyQYL6b`)Y~xe*PQ1jI4~=XLmcax=@KF?@1sdN%Qg`DHu0np zngSq!xW_Qvw929TCW7R(RrMk_O?v9qd0DOL7rh}g1oH;=-W$l5 zD2DDO-bp@H(!Uk#z{qs)o&RL}1oI#}SS`NmqC&3Lj$GM+c8lsAz3?o#O$n{dB;#&m z_cZ@bwP;mji_;GF%A0?px$$NA01NwQm;(=ugMS|KtWiE#vPhR5%abY6&iV=(3rTg)ReCa{2c2qxh!2{tCuVwALTk`aiLsA#E8N;Erzkgqs5yW2+S5@tBHi^ zQpBwy2DiXn6g3?ijI=$EuYtr3GiY3$=_w&LNB`?@-9V970%Uphe3c^aHHi5&15!^4SQ zECx|CgEo!P-dAnjzLm=-5(I$(`R(FJp`TbnN$@J7k?*0~2Uw}P8@ui&r3nMQ!X*C9 zU^;V-tl?*j0&e}NJO+Qe!bsUC(fTduyd-PJ8yjDIaj^P-Go?_wN5`)JKa2pHn$LTZ zn2<;nO;yfT9hb21m;oth6jT_!8-ILV_uyA~A^l+rLwd`iM_+WqOD0aZi@9fxtFr?4E zEgK_!$CQOTFO~wii&8%KdH>r>$r6bVkwFzy4UWe-ib}A-kmybH()^JQK_U6p`4QRIkzDCK|E5cA(TIlK)&9}&NVx+=gn_Ezj|JT$;AFrLX&WW%V| zZ@yD@uEga38G!69ZtKp`$;ekG_H}*h+N&Yg*#8nEI*=3z}x%QJn-LJyr&1sRAn1R*RlJur}xrIEF`de#dp0`zxi&hduNz zA{`X?$dBpk%xf-`ZnIWXi~aa>CkgWD3+9^*H#-vWzsdZTY9}ZHt$p&ppYbCG1U{xR zTFbnsI~5Vz7)XF&!gsy98cP`}BMOzLT8CERtKbTc1nm?fy`==qYFfT~MmquiXh6%M z%bl!dbL@Po?ovIfMQ7haCru%2d9+&7 z>Z_>EMV<%46Uhy?W4hjX5V3_n#IG`b-31U%TtYS%4U9kD*6zP0$^53oRKYl$NNnYM zYugWi52r$v`Fed z%Oi}fT$mNsyQ=O4SdeKTXr}B_lDruu#Lgn97=Deo^dp4Aa))X7tr4b~{&=$N6Dx9< zOZ4vBy3=I08)$BC!mE|~i>FCV$$&H|+L*Pku*r+Q6w0}ljX;MwxWyD0b<<&;JTYv- zeQ`Ew4=qsHaI{PSRCgOgv$Zfy_q~>Drm67P3xUxV&{5B?4JnP5RZ2Pj?Opf2;ID*J z0v19Ba#d_0keL5xS0rWxL}6@3G3?Q(}1mWo@})B8EXmPXPN$A!d3H#fZ0`pAa)4H``}ia>$puG7M!2a z>04CQ=IR^IxmR0=X#<+{!OqmU-C3uI4|w++IUV$zV35nVTOYcUSvQ%b*b@W)mZ;2% z8^>HSL)^7uj)SS?wQ%JW_vHNCA8zyMz&cnFTsL=C#TkCt{Fn_LD#>T(Am=dt$DJ#z z4O4X;w08pG?1>(qfB={P3AYz^og2~htYS&!nhijRuIPtntm0FyTBwtq(MU1a=s4|) z5cBixs|mcF_{ifaR+A*>DIJcbr<;ecar0c@$9BIFWxS`EJPc4%`QR~-YU-H$k9^8U zrmRt+LfUZ&DQ?|GFlP#Unc#A~m9395gTNmyEF~jDwG0gsZ9s%_LljE9{3V;EqS>|B}P|`R**oF~_BjDHZgAHCT zF7q=~%b_dblojBPnONV&>}R;RHq#~dzjl(25?p_iDt7;z*)fOruxV7C877*P!-z;t zQa$;5$2zs>U%W@WE6zy#;Ab=)$0V+?7R(7c*_R$Qo^}YySD;DV z2;~wuHK|Xrctz(>c>NQ4X38=)iSnt-S7-M|VHIO)aH3m|hOl<_<87QsyVq?ZJRsDl zorQrd^IDvI)*#YCNMREG40!k?Hp=EV7Dx}_`53gF%Jc!%b!Y$!Gm}!NUuW3_34<8@ zq$i3H=*lwgZQ%K`01c(dlQ#mrJ7ZLp3{QM;d_LyPNvn7(clt!CwYc<|@v9^jl|Rdz zZMs@P9i)Lk3S2>Dkd0i&Jw!h_^Xz^ze&-a@j+5ss3A)htpGAcIwXPlsZO#0oFQ*5u zjDA(Er=ili=Yi5H4;a<(X)Gg$Zcb2tj6F>OyFf`dZczmJBY1_u*l1@FJM z2Tqr}P^W|1KQV~KIqU&xz0{a4AC4kGFTkBEro3G~nR4N5W&5~LSG)?fm5cumy9Z*D ze>-N8>);_vEytPQD@JcI)vYD)bx+Ez5f6;_$&i+8J2h``f@`h(val{+Jb5uYKN<^C zhx1KoMqr`yNwv`q(Ra%0iS9TP1wk{zo*CJf%XxWz$ld@uK*Yb?7-ph|1lm;TbNr7} z1^(kr(AYLI4|0v0k#Vwb`-IvVw-EAGeK+!3TvJ=xoQ$t_TzSKuZP0U8W&7B~$Zx)5 zPIvMsIv8K%uwC@;{Gx4cDZ|->m46q9BN3b^X9y*TNkUaM!`bO8P%navJnPvVBKKMh z3NwndOtlZ>w<(mIIyvl$C&n})OC4SdfL?lQGvz>r7Dbit7JDEr&vTVI- zBBbbtZr0}Cwg>&gwhYZ@K^i&gm@*sh__GfMCy`7HWMeK zwp7ePl1$2PQfmcoT*JucHZAZ$YIaVniDF{4TF%PKvVOJ;mvV#1f+!Y@%Ef$mc`0-u z@pD`GTl6kB0Mgy&gQjzE9CXxZt5!`5$<)5P$aE`tK>^h=orLg=%H5 zgD$kOTT-NPfy@`@3xMqTa!Key7 z%`W4*94Kn;4|Yfh&`E|f$BiH4EPIs6`Qrfb}oy7twZo>?&?A@fCjc; zsm+cs2rQC;0t??aDi!g{6OvOd?h#n#_UKB;%(D}NZX;6-E}E0CXD}(hY4=bT ziSwZ){fx#&Z2S}B-n}4|Jqrd0mV9|%nz@j(e8(~XO-18l*T;(wMLDpikAh*jRSbt^ z2g$9T;hB1p{hci3ga5R(u)S; zgaBxkMc&r0eV4JQLJH>q?IH#SzLLln`f+ggj)L%KcJ2y}y)$l9v0mWIu;P?4>tXZy zI;NSWjI&svL~ChF4Nh-`D?eNEu|%iICnfDUqN#e{A7$F2Rv?@_kQ3M1*Y~T@a0Z&X ze~nr|WO2m2)cm01Xr#cpSasKFs{#9Mc^T=i1ZXsRJtuhz9LOHY>v*6s`SBsGch(T` zV&D1o-bL`W=N@nW?O1cmi4EevBWzy+%<|XdS>|8Ap_*!P0mD?Z0?1;N7(z-LQcyqMPjDInpbRMi{&SIalO7%aoKdST zlfAK3tE$!F`I(?8_Ufu)LbA5@yao1-#OMWA$)`PijS|B%i`7s^e?8l$2tizP&OLAL@U$Y=yn$Lp;BzDyXCwe6 zkJ5E+FFY6;o90Z@oWGF7lCwh2+;f#%LgJNOh*%`{&kQtMrHb06L+?Fa@4D>}PfUbv zsGD_GR;j!@kPVF@IBvF#LyiGO)@u0oe)n-~O-CYJ%s!AR%8#ifbIVgSz1V&mX4tNv zAv0Mx$=HUNp&>NPrvdeUB$~LgivJd*h8CHvPA~em!-e%peA%K?e>V0rnG|7i z8G8?Q4krQ=Y&YNtCPlmF&-3>mebGmMc>uhDy)~vkQedH(Ob`|6!YP(h4eqV5&;}fe zdh?X?2fI=<``sagTMTGJc}y6d@`?S*lrFMT`Soy()n!l{)xqdu+zZpa1_b2VP{ZqB z52Ofu1~d{&o*7rbHMCkuVCvxI=6&io>!g#UcN@>$@l@R7=*99az}a5JLtDx-lU4Q` zmgNC~oNT(RP|AE%jCj1f`s=@^TNAw1_Nb*=s1EBxuSrw?G^)qdCIo>`4XJURi&1L{ z(@IKb(0QU73t6HDm_&!O>)~+vSkS31vaYBA-uEh_6k66I)jk#2hGwHzy_3(&!XQv| zMQ*%d-t}B33rPpbs4`ZGq^fpgw^Tv?0Rd%|Mrswj;Lep09lBm?n<;O6sVL71rrf-S ze;!AB5#QGq7eElp5QSo69Rf?XiG?}N;jWlkYYQravH76=0J=RlF-Pv)ORH?XJ|V>=JDyx7l(`qEdpYhVWjdOf@>~;r5N0~ zW!rB&G#YM;Fa~lvtqhrEk;XGF!1JFA*r1Q}r#}EOnz9@PZRJ?gSveWVPRb}1HzOwv zADeE!M$O%8l!&rlXTm)v@Bm_R5MbMwA1vE~O57w#V|?%`G<`*GbfxPEsc-jXZ>95u4zoW6Yme-!EO!rQFliuGk+GO8T z>A^Nl;gQ>)okLmAD)=_W9Xd^OKi}FvMS4+!8HHs)T# zJDcVf-2allG$o2LLyMM!?K@Uy{EwZq6G`6X46P|*G zyFbjc>aCT}-S}J4D2MVU1;^U%ezESM~xJ{{y$<9%iz7bz8mXL|t;*h_7tkO|3oY zue4hn{!PBD(SC>Ed%@i*x$#kgZ{*^czvcKZ(_N;cUP>$rOhSCvjaE<7G(wD~ehN48 zLG&j#Uh1=}`!L4u%G6I~+(6%_DQ9A-T|ZZ;+=%aw3?g{sr06Wkgnp{?KmKAn3*=kl zbU|f6>!EkVJ)PMJ&6B5!$5yn6uk8c79|XhyFaCx8h}RnFX1M$VBy3G_A0*aPrUw!w znEkX~z)lirs0TuP{}ST&w7t$47qpdX$Oq3f-w-38X!3&rk4dQcOBhc^R1b*6M)M;r z(bW;;hJ5<;kN8EoPO=0t7THhWU>wlaPS0=;@;zMxNc?A5?>xHMGl7sWiLSC#)RtDS zoxJ61I1=N&b}SN9iE2m-MdPkcs$Rv)4`YOf?u!h>86hO0{)O`x?9i6BczZjEdmP7w*a?*u69<88{+8E8Vu93* zBPZuLFf4zyrMqBvk#NMf+yt*7Y$K+5fGdm_?sAbqpP_+kQ?*nwK?aKHZ6Cwwl0q(53YJ`;87#Z*wv1j+=Px#5voLS}?Yy#nYq z*Oi@&Xh_gu*gXltg@1<$ghF4i3d05zh77L0iia$i`DGpM%G3=JPH4JdmhP|exl_>f z^v?BJe!w@Hhk}5^qtupBBal%AqJamtJc*RCDr6mpgJ_mwrkTFnX8jg4jG`798xIn30{4 z2XlE_VD>^JraeG=Le=!8%K;&pMokq*r7nSBf!bdVLpmUc=}s~?v!1a;qf?e4BDP4N zBW)G-ze{O*3+V91bgN!j5Je}UV6c1+ce~Rni#>n$L45lty=a;}XmJXy`0+p%shhZG zLIbu1vVH}ayow*!z2rf!j#?jgL}ATSvs=!K5Zqb-9Eb0-H9{rNQkQ8Pii0Hoc2>f$ z)(vW~iJG`|m6Og;NOJ_PUO9tbTc4FUW4;E{6;1QIqGSoW>el^B;-gT^!S%bg@a(l4Pl?B{7WPo@ z6myyviNKIB#YeXVyy^mhZ{2+j@>e}roW6+q?ZQx8FaNRgz}QR`x-UuX?nr-@#=_tx z+`5h2AbFpq;RzdI$FWH)zt7m5!ZDO^{ta)?Zco`KJKze$VBsV>JL;|enHJ(hqi_)g zHS#GtZlbOdz`Fa>T;FKLTy-c!IS@|yRCWyhP>LX?2>MN$`rx6zT4n8Ny}$6`0)>Wa z*pA|GY3MTxi+7i3D>Dp{y*vU&#@z+;3CL zfZ(Eh2lqi8(D5;|cR?jGIpCZ);+CeXj^JC%^mSxA9(QGiH>DkQ{$LVQ{ylqH>FK3e zOuU(LBreaJ6T8Is4yysUWjlLeCQ3XhG$VY=60Rv~@%}g=p!%TT>Nn zhC=8bBfDRL@@P)wLr}SqfP$=odGUjRE>N;_Ad~J}DV2;rgpI6YWVj$&QpMgy%J;7* zy3|;1p^9^>$Zv&l?+$ccHGs3g?931z7+db38r)>l)JHdCBm$Ojr;B5?L7zOE=su+J zxNDcM%d*0jDR|RTF8?~SNn3k+Fq;Dis!*_6H4X$*v(n40unyRqmTV^G7r*o`8AjWq z8<2|SeST#NE2p(724))ZIZu~2a!PUOW;U@+vzVUDp|$EUlJVMbg3!vKhYk2wQM|8C zl#y&r`CJ;&faSgzDxugqY3I>RZx}s@lER2#*Ffov%05v(#OZ(NxZ%m+)oQj3&-t2I zq9(Xz4U_m441g>h3jP8-(XA|&&sf06oPS@#I|c63a7<2rhsS6wdzy@^FHZ6l}W!k&2-$W^k2YRsTz;@ znGIZ(mW*V4vbQsD-3G41=zV~^O~8+XN&p%%G$Vu58jW+Y{X&FXwr}GgpyxgNz!{Y! z`IqWLdj20A7Jp75)_BgcWuMExRFs)oOPXwL|PLxA9|Il&~1edl)sLdv%eCNz^4Pq2lc za2q1}o3aydCuH(&iy7PB4=%$%*sPuJ>7K>`JglRqs)7r`drw1G3vt^`)sJi}>ZYu! zzLv>P{fU!EZpx1w-OOj6ZnrJK0C-D^OkY08qni3#w09so9Bqk=FxP1sDj- zDH>vZ?XED{zsJ4_0WKr{Wfcaeu+y?rLhwc+!w@h0SF4$h+dU(|r0B_a7&=u<-l_*> z(Vozx?wSUQ3U=s+jb|DE!oqN}GVAhQMifFfq#ey|9c$KEr-Rv$T3-f+L_;!D1el(| z%!1c4PKco_KwHnD>Z9)ji7#|jf1MCBh#&ifR+*DzarBbIzjhCh+^~0lCgZ55B;hf{Sj%4j@%eUV-tK9^@PpWu>CfpCx21L zZF5Y;YL0vVn>P5;B>$DE>ml&$MKzdh3YwN${#gIPxr6F&*TdXxS>1*F&TKB~^B+{j zx*c0Owx)0a&^6JN*PJ#3mXWEMZJss?txoeWqA(%FCapBZw^g&MHGyybjfB=-IJ@HOyH7Ymf=cLU)?+lVaN6OQd|9Py}iXOBunLBA3npG8Jx@~jRnbwb$v*4W~&*5c5>2W+vppanh)yLey z=&~k0S+Uzmqcvfe)^=wPJRr4Tj~Z)g60rJjgurl&G5V} z9B}CSp&9fAQ+w9Rcaqh7`}R8`tr~MLOey*80JyQIoj@2!#g;81ET{lo%)EutGCJdi zaSfutWy?J&8;9hSu{M>!+iWDufF;^f!Nq=%^4q!4#hY0PaA|>j316NJAGcFr8+o(W z9#xk!n7#-RtNc>2Zf)09^NhVm6x?7x9D1XN=E(QUC9d>S_p>UasN)JRNT4+R$Bw}Mlz5k8*Rk@r8 zTMrEFzbViKXX!W-S>~Tw`h`C{z$Wqls0s>w(uQpLgve@#&7AwM0&$y#D2slm#66%U zb#?TMwODA&LRME(5P{!II+IXhXo@jykC?>{=IC(3DOKU_iU8E4YZW5&WqljWMKHE8 z(Qwg!p#U4hn>b$%71w_KFNjB>J!m_i1va-q_ zZs*CssXmj$+gv9U4VUFhc8yE3v{iO;+MKedUK#)c{5)i%Yt(RX1D3w^RY&*6CLUw;vI&R@Xy_p(Qe zfddfZc`_7C%XAJ4b6mFak>0`l1%q}Ox6ZF|SohLB9waHO2`7Pc&_&_$J9{c5ZZ2}M zXVb4^Mk$bExM4YE=)iMZ(Nti2>u00KBNAsP&N*-yf<|ht>kI&Mh0X`inu}gx+rW=s z^CvE1>|M{dgqb=iCP&VaDD`iBE^@m{q3(aT3o(YoV)&R3bDoS1<(}aWwFM@K5| z$ne^)z>bWQlC|7$4UXVIuYzxh9L@vQR^ypvNsHswE=)L5gm!t5V{@s6n@d&`_#{*% zSE;)em-R$J-9F)u4Snp+a4@q?=@>Su9?*q0{5o60=p!FqS6%`A*3B8&^q5kV zjIbsU$A<$e=Qf4}LBNMv;#=CTWY_s=Vz-IX@`$uYNj$lqN%c_trRuxbR_kMDaP_=oI~078PHr6tzO)BIGOlZQab7a+BViA z3w%PE<#vE&zMCU%Jx-86J+{13aEMLGkp|+3I}Ii8dv`YT#S|`S7k0I0Ri`D&gkJE9 z?IA0Q#WC$!Yk7i*1y#I|ZNamwWCOP2Ez#H_qF^2Y=d))N#q0s;bbOlR$SgYYGYoR! z^M+cs<>ok5W_F{*lGGY3MT%zrFj~?b2s=;M;P$sci*Xn;jgu194BMtl&m6oX=}_MG zwxt}S^&Vhoib14!#+ZZNb{iZs>|!}{rVj~EDF!vZlPz)>pMFP&xM%S>;L1}3!Udc% ztv$_IR4X(KBkBn)r|QR-zW4J#VRu{E1IgBHFSDQ|Ep<@Lt@9`GfG-6@AW08 z_W>A8%~R|@Qds>#@%alPxg0rPx#CpjJkB#?KL8QAJQa6K2+wmiw%8MdQxT+F6xG(S zhkL~%Vue-M!PX#NL3+xW()rti<|?{Ymx$aV!ix2a)WecQnISf0(Pz z46#-qyH_|AJ>fcmrmGK&Dt;lXz6U>w@tdoQ9rM-x?)M;|Kb_y~iESnKuW+P5Y46vo zhFI*ZNB=(ujl|wMmWyJv)R8nQ%G+}#!-86<7|h4HWQ*{iX3;2?cZuUgGym{d)Zp!= zny5Vd0!4qpwhX6!*yyeg5b6;YlWA^~nW<0W$T?D`Ec8sd$&OZ6HK0Z!taKa*X@-VXe`KUOR!B0;! zQ)p8Rkto5s7fzhQ%l+1NJ_wd#K)zJFAOZq^HzJdh1aIG<2S8O=^VvMEDWaYvKtu8L z(bZY2UJ^(4#EqyTqwXeA#b)&mK#7LK1ih^vMGn@AJoFiTT1C>!dR0?s;6J-?yU8mL zR3TVIR_hbQ!UT}i!8#qGx8p+TXZJFa<|6P6`%zjgT$nhPW`YCSt+OQ9*_b<+25bKEcrf9%ef9gWBdMu=H zGTY6nD;St^v~Zv@{Q@rBo>)3E7;Mpl(*9Tg-ZJ#BMbOKHOsUiY&@WQufeL%L6f|N$ ziakUU_3TyQ+z?&Xq`zB`$DY3~x>>OndPjI5T(pWRjGO`#Dr`m%2%BLGp zU$kQjYCt4?10@9VT<6`Rg%B!JZ^=gyX)l4J{0H>(`*+pmJ@@y8rgP9ScPc>f(I>ud z6}nzlU0IPk%=mq{syBXV-j48i6U!%bVj9%_D!lY}f#jg%O5@`oZME7yrR`H6kKiJ5 zFrwP*C`XX<df-#^jCDd>`qZ zkX(?n4(|WoKe9fo@T<;wW)~i^I|Tl9`TAsEac6>S(#Xo`U!Zse z$CkbFm;2$q9_Op{Hv-6G%%-sXqui3=Zuj~LZ_5F0In=ha89Wjbg;@XT*n;!^ZrwKo ztXX@t8DU4v3r5E@)XEv0Te!)4+>ePnP`OVcdD;|xlURSJv1Z9C=|W6&@BIJ8ElJt_9UGcQ>g8r9mq?d!d+{RnO$+7CXY??X?@t$i*my4)?`?9d?46lt z%l{dt-WtTFA?|b z>64}~-Ce75V);CnEu>?gf5veM zEY;GV$H*Nb7B%Fb73ew1rMD+T+)VU*iNTHu3eiKA|>{5~#v-X~|e;F>Etd zL>g-+h$iXRKo;84B}~VA(41>elq?19Zhi9$apf848%33GLnCA0Y%uK`NCM7tTX2cu$^#9VWrVJYw5!JbJHQ82~?vN3$;24 zdM0EQvA!~hY^Bbt`zr+AcWUl@hxp>{4?{XOmG~0LWbx1eP3aXQ;<%Sd!cZCTj5X?y z_*+go(IuwtZ32&vd*pfXV3jj5Op`ojLxP`1mzbbNsjtZw4{booyeGyG%Eh~D;C+Qt zzQ?h|q}ATD!nB3z2v2VG1Ht?wGj`s;+JSuk3qoxUO>@SUV9LbeA@Uc4BQI|R1E8~n z-%Dg>e=tNZjMUM}TB37_#g~btmXUutQc2iJ{oIII&9jjctzBB1Mz2iaGE1ib+Bx-} zks;!5cc!Gl#6XiG3^ls_r;k^d(o-xU!odG!$`Fa8-13R@GT~9Jbt>;v#$DG)Ct^hw zI^D*m=a|(I?P_tpB4npI1};qfwpq3J%KYWC0R7M$h?8W~lC7i^hobs);O9|10knCh zj!gqx*+#>e#y2}zCeU?akC5LlHg}7z;m~k8T1^+fR~#q|cQnPbi$)_|-W7`v46j(o z9#lC-c-9R+!eGHEEQc?*IO8xy5*e6Rlz}1i(2}<-K=}PZqgW73vi6d&r zz?s^|Y>N^oW%bvQJgCQrP&YWjd#>)WFK9!tdB`X1-O<(ctZQSeU;-qVn(yW?!|#}} zuOoWA-#Kt3QiMb;?PzP<*`4qyq1Ua+$q?^8g1<-5Evh$GAk`Q)2RRz#Z;WYcT63PO zPs~0-u}y95lQw*v+o9)z-vw=#ITCfa;|HSCJGJ#AbSMWp+RdzcTgv10xN(jU83vH$ z8Dwpo3COBxq-A3Uc^aM+lP{1F`4bSH9z8YTkM8J(Omg zbEI)SrE##H??wpF4gSqd-tnj#?ez_;(1>Snn8H&+d_1V@gQZB_Utsc6>~0 zcE<)UQ;QRZ@BvOt^O{^=BQmZ4RFO`E0%GrT)JbzE=2Mg;vHr4lXdlnosRRA95`lz{ zcm%(^A<|?~tIq{8|MPL!Ub9h$%gv-h8yD#dlTrnZ7||(8ACHyF=%EvoeSXJr{8-k1 z)S^v*9(oX!{E-$%isX4p@0Inp3Ye|(i?IpZ5nLFQj-EVpdq2K7X-_?V-otdox+VLsK0?SCvcbv@as{(;wdPVt;L`00Zh2mABU3 zzulq8J@`mK3obYVjP%UUGK&7XfQ)1jL478<_ROT!5JPuqANRYlYKQ=+r;x+l86mAP zg&JuER;FM^>!rwd?1eh67Li*Wc|nxBBG!(5Wc|UQOZ&A z!xs(H0z=STaGs1G%$bIt0{(d~TZYV{nnq=>_!%!qTUlsF=4>=LZw!eKBSS=P3X^et zGL<8C(e+P>s`s^YRhhDHEO)idDb=`O#LuCH+Q>nS0~W2R8&|?wo(q?URvBeX<62N+ z(|um?Aw_^B&ByN_YXpf;Kg|7gz>_!_nKTUR;MT-}m_4qd^pfD^sV(baKiTM80#bJ* zwi&u$v*KuYg8_nG#&^u)C-vg+zDaIHpTaM^!vqQgDN%VU_j8T{I^Ofeb0RoyA|fcV z7!y8j3Lqwg#>+Fz(;!!oS^S5D^ruyYUfXQ{?-o2*Yj<#4+9)YIhMIF$33Id zl!8>l2i!{233y2Ck_9k(6#~Z;h8>y<(a>{z$GBb4Q0AK}U>c#urHwbD(yX$g(wHTS zCy76I4}4n>QbbT3d6pwUwQ285Z;h;Hl5cjgXZ2T!tz>xlTIL`3mY7u_CtUg{Ln?N^ zwjK3Zy6DRRHtAD}rB^~7??MToF~6HBnLuI+N7iWy&@Rz(H=*iFCi2be!NDp(W)id$ z!T-Mc#Lw`L`B=VG_dIqqg4!nUAf}6aZ$WZ;=A4{OPb~~1A?Rl;Oqjh)HC3OPS^fY0 z;~I7J;4OM^Ok9V&2h4-?a!14+o)H;VD0s)e9Kyq-Pw_ttd0vqX1ki*Lw>Fall5ScS z)c>9V1fxgQTPgu9_ zgvJj-s_DjZv$~A}lQFZFjaL0X1>3Z-OR5Z|)AiVr9P9aY{=S1G!tU@qv;NPc`rs>k z(-0|_K{EXkPW~?#n^h&oHNy+q0ScF=8`cz_SAAWaZfB9E`x98y>Oy>sm56A}F>i~` zDx3Yt94i=Zd=rW$Q|^T$@gh;zDX+pdPx_4+vAb%OY-{!C>;yZ^l=A_9Cfwk<`(`9& zfDjt;+oH1YG5f@4oZt8j76T@9WQe(daKPMHG~bgFd~gxxREesZDrov5NvIKC?ohYC z$hxfx@0+OBh^Pgi~E>*mGK$Z82OycP{J(=&I_>{5xsepA36s z0lG5)#`~@>j>Na<+WM*)xD-WsR}^C2E8fiVBWe`?K}-1MNmk)uPr^du&X1VXQh$iN z!ti`xeY&O#W@uH%?QR=!&y5LNo%0l^SA}a z_ukKa0mh$u6Ep|G+D;HL&76d(S;VwW8BCt(Pt5{>q+TIZZu(n;8Mwwz*zjc9a7rT8kGqdRDQ8sR%l-h3ffi z-Q^;yh{!z&(9$9_ELr3^ic&~Y6Sq`iuFU%{N)II)UJ9362LlCvbk_LpH%?q&W|Ng3 zJ%MlwVT+FsN*>I*W3QhcD33Q zi@hAlsXt$x^-tl6+mNXzwYB{~F6e(pvYXJ?o;&!FH5WwH3hyD?!W%InqqB9#a^KN_)T znO1IBdwjtv4jG2})f-I@0ecXz1V2W|5X(2D%l7}G6NunX*hni-s#eO7%jHuaLf+84~5KU8&Hbp_ff!uM$JBCnHfY|LCNmMQcf6yf!G|3 zr74ArCuRR=z@+V^k(m!IBVSzP3AkoPG78@0fn@|m=}_(8{gE0VmZ}PJkjY}hu9a$| z&RHo~06W%~qli(F1nf?LV^v>k2p<`L%~TEx=qAk1RcI`J9%;w0%+Zfj+5^cvroAV2 zJs~4jJ7?ebYeXPB%=+2tTHDJ`FIO!#46_>_kJg zhuHq+5hC>iZZ}q?7xf)gB907Ahp6r zo?od70%^%+wvb{_n{sxgg4GIzjhZrI3<|U_ncq#r3^e+`4%JR9Oy7<<+pdb(hz|dh z5I52(1|e~AHL}Jg{h&)4P~2Ns@}O887~-BZth>J_E7v-s6BUpAWpp4})7C^w%LbCTQKaEh9ZeNzh=Y}q9)QcmLBU)lBeV2#1Zt@V)D*g9Q zFF;P(UWY488;?I7IHKDp=UAIM?Idh4R@9b7<>9IY9^sq-;_;Hpz*ZTLtR~5(gYVjN z4+#0-J%sTP|o=0=3$l%Hyw~=K3bwlMciAx9rLumJ%d0rR< zhvvzQ)CizFDFp!f3VUbIuwxs2wwk ztX%?Qt-nS(9cBv6u{sA`B>hwf2t7u^AazjuR;# z8ccZ%nTc+zD!GQ?9`;LFYZy@H5Mkyih3ek$;8(sq53P?5xvq7H;rQwr5mHSqZ_+wXtod7_kd6D@Vk$p zM@OS{Z&Lnn0PbLe-}9+O)8mr5SY4~8ce{0Em#HCg-zT$9^PhfmM?qUkwYPX3KnbiV%}7InZVLFrb65 z!%&B_KDUT%Rm>@F`)9uDWR$gmO&ZXJNnWcQA^8RiDT+Kn-WGlA%@JuwT>PP*G(x|O zy&}I{%8;w}tFI7sqp?c|Km!yECv62IYyr)22|c@8Rc7-`UO;z$`W{_Z*HIVbX|C#5 zsTiqGjP^HA*@yXKt(k_Fz0*ofV!V%6k+Yc(9UakoOlV#v<_7*h${J4U7L86?g{H1b z)^;9t3w(BZB%tb~Xjb_gK>4w+3$nk3P7@;N(Vejg!!t11OU<8@OJceSJj6BH#BsId z8U6By8H(MpM74203&iTP9FcMB2sreN>TSn+H^crj0}(JnVFK7CIfx0OA!5OKn6goY z<#fc-xWFxPb0#w40W^Pgu#G;fVq+gvAEutG1J^>JK8t?W{k4gsjr&_k{1W&qcg5aP ztu>KL!=#Fln%b-4m9we~eXyZ%><*j%V#xSZ2mcEV>tDm0(L`FHTDw+10jOiIZb(`@ zxIQH6zwsf2>YrKl3K7I##`tmmS2{)mEu~G%e&!STj1`y?4OIZ;u*hf-3WDU^5+b*< zw+x%d1rk~T)E2YU4=>4l!mtufXY3QoFohaFX!f+WKfiv&%#g|d^x+V<=mjOsuL&-n z)fofJGGISEWlxsJqY-NapzuIVEVq?Jrwkc5>|^v%+4rb5*@8ur= zai1n9`7XcuWW}9!!;&VP4x1OmCsI)GcM795>9wIPcy$5g+Y4^^B5Lgmpp0X8GurAk?69J0Q2H9E{iCv#o zkEaK{_kax9JGa^-KH0DvwSh_ORZL%QD)9N7PLImfAQzjcZI3YfA7Wtvm9VICL>e`*MlElcDJjxys{VgpFx@tqkRbtk5b70?6biGD#pd zj-jnQ7gDcJ?mvTd&G1{LNXPr43tvml7rH54MqZCUn!m>C(*ZkVPfcpUYh zk)GHf3Gu{Lm?|Kw5sS4wfw@fey@O0`r>dsD&qcNH{;;R!zv406OT5h8O*71>r-z-2#UpBm`n{~Z}s+u`aP^I zXC0`3cx&j&E!r)Dj><*)A&l!+BuA5JKbaeMJLM9r7OGSd08bP0heDtIwSU#!r4A)!nVSyZF6P3_?oxcHgbUp=C0kbGPRej}!HVfpqXvb~y0+ zs<-@}$GL3WlSZ*^jCotTmFtclHMIDUx=W$0J4lQ4&;+587z_3>ctuhW=VXPXbz^L~ zI{XcNQ+@_?l7%4DSRz(AEdZ?WUBKZfzh;p~xUXfnALiqXIBM;iKj0v`LV)i2EWvwn zA)2LRk>hVv6Jrx#Qui8&b!7DD%v0P)I-_1k_Fq{ zjnr<_!x|mA95Zl%P*EIXosXmrdwUmp_Zcu>sB#*-)f#7+VZyZZ}Rp z+KXlrx_WZGzVq>h1xO7W%uIR*$GLmEr0?37GSx;^S%;xZ`SnBZ*cP2)Z3qdttg_Ll zmK^1c*XE1T0e|g=_v`ihr!osgrv02Wr_9%nZSSTCZ~SRIErIf>Xw9grW_0ctsp>KQ zC}S5|z@EQ_CF}{jhHVG4XVp>)oEz&hoiP_3(WH*$3?cD43lCm14a={wsn6EGD47Ny zygWSL!MdhH|7pxAQPl^g_Yj1Aj({)eip2Ar684D+mKX7qJbm+4?kFbgCXnuc{ADpD z+_qr+{)*qx_(plcO!;O?e?%blHU9y|$@(2E{^yG^33Z1EpM+&N{}K6Tm;Yj2idnog5fH56~>#*@i&h^}Gm06DtJZ+?Y7xIJ#hJv7_&7K;0w#;2kT+iP6i!%!+;Pe!8!= z#zz%rAcS=OVWEHPj>*6DjY7m{bT5PBQY&86tE{v-)76ku7ZJ}RNWiQ~H##Pvu+)<* zP&-n&*1m7cM-l0qwj!gX7YtSc+j+2{Ow>|*j;@cyy(LoW4C6f|xES?jJ1-EC{&K(R z`UdIlyUI4Kz!(c<_Ajy0Qx3kRcV*o0DCxU56TYe4GtWRwE$HfGM#?_ZVHe55ACiN> zFX-jB11gqhZ58fH|LNS4yI#!=kpoo%-bQ+T4{Fi8Ime72H#E!&K ze`|gbLJtv~6IMA3Eb~>bvC$0|T5f#_7bzp2P)iZpACe7hnz{kZ{S^Qg4n=-}S#G%u zn8ab_%jn$*#Y@!UcT zoL*tzR2DMd5ULf%x1hs0+Ta>0s(}1rr6Zg%>27DG zzt~(sw^qeWd1g;7<*6F5-sOt+hTlbqdbSu`!pENkUdlzbT?eBkczefaqne_^J#zpr zK+wM^r6hP0i>=!72LdrOdG8F`Dn#w!fxrc%QR9}-#Mnw+&mp(~I03kmYyGIkwnuz- zw@QQ8PPU2pGmyw<4xWNHN_YeDlmaGhv-Y>VB_;EjMUAw53ywa;5eAf74;s7MiNXrY zZtZeuA{&ybyBA*l+bOO8NS7w+OCrFvX~b{g)QO&=M26}FWi20*(CQ!h9L*IY~ISD1$&9SHr6?x@BkVQxZ{#qNor>_!~4y zm0HA(YG^s&q_Kl1IWMQ)7mfP~AgLJ8u5W^ohKxSHjoz_!w~vNxtxguXjjveSygtBo%JsbZKZ5 z0GNw~TMNDzFHR#46_-SM$XYp(LcIjA~zn0%=Y2i!0SBI8|ler!Q+x-X2 z=yuG@wtgCV?&x;a)tWF^-Gn31YnO**yHq{URw&;sxk_+gJ@N>?LGiKviN8H2^$_yY z`Y0_jzO_JPM-}TT*a$}@UAv=TN$Mt;!Xb@=oavbQQYEealK_TmIBH|0Xy#Ed0+6!3 z2sS>(2x!<@)nE`!4*gb1bJ%80`6ES}%a>SO7!cr$W*JGF`e+9_!nyuagLC`GsVKdT zW}WecHMf7uoT!ZbfOu##>L7%po$uZm9>V(FV#31x-r0?sjt)6CC`%OS=W6`27=GyGIN8p6S0SJv&;)BsdCZ;$M1FZg4F(rNb@XIrYb6h^@xdUjZ5d2*@c&Y1-VJnjrbb#p&T2Q`N!f1O7G!Xlg$WI6!qHA-}+< z1dwVc9*+FujWmE)n_#HX|KTxBJA4LE6T2|rk?})7&m|DFRn+yUuni53WNg{jk`0E< z(5$%cu&t{ss<7uIK3=#~6VEFvn(qJU4}V+tn1fs~%T-k@6^-ddwuSa>jX;pJwa1MS z8zZN3Enp~nLU&mt`HmOZPW>72FbV_58#l32BK|iT1XJp6+td~u^E)ucYgUQ~#L5*! zJp)M%@+wh8(rlqA6}ZK3rO}n5`pN)O4ZDB|Bn#X`__9_ea$0PMj-BoN!dxKI(@VFv zr%|%l=S`@PZN^@7kZam_H~1d1!bFO!&Da{8ByfQK?Y|?2Fz2Vc&{tl$`uUro4e$H>ph%%JMk2gNz)<2J?_-)WFV0&9w#ZwF!z26K|8epMx8%_p3 zCRQGplK-T&8`jzs=Tf+dxLhzQ;}z#rspH$#-Q0gqVY7&Yymf#Hj4Jn z81FpHYS`oHb*Dj~NzQS`M3ZEv>KfO}!YaPQ`dX{Ka<|UQrw+T1uQ;Td>p>o-1Zz-2sM zadd+C?Jfyxsv=*R3{N^rwp+h zn-_oQ$iNrfVCn$v>sB34ckSHQ+sa?r!gx+U#uc#Mu)nu%lGOmv!6M)gy@5FHY+^|X z@d`Fj)waor%0Yco5b{_P3^^x*&^{j%&SMv^-FngIi;NV*Lkh~uusNBfKU z;jv)*Tl(z|{Ixsn$1&t7>}Qt4lKVuDs9F zAeACXAZhl@&PK%o<9)YbZQkyJ&JBsN_t3!j%XL#=40WXOU1oWKuF&Lt$a!q^wPZRN zGh^Alx~`bi$m>E~af#|;s={d!MUXWLk{dbFS4tx4yQVOnN3Mx)@ZDD9D>`MtdMY)L zRg;JMK*ylx4@_X_;lPXTz}{K(OnfnzHwC(%O7E};b5XE18!-9;6N5$F8tFbl5GWTs zsWtOt*9BoANF!*AS|vF3twl6y>bvqAW#4Jh#E(oS+6>_C~ZYCmKQXKTC?&X((BN!_od`3Av0P z3=8PmO=={4`N6J^G2DnsS+I;;vIE6^;dq=u!EE{Zg#gVDKIg{yzwu<0K$bLf`Cj7nB!)3TPVK{tP+DIJiYK7^N`j+FV`;bJml*&? zpyi{VClWIO9aY3~0*IjL?_TZa9!AOxy__qysbVosvIGVOD}w0|oX8yk5pk`p@mX9l zuligP^9%@?>@0ekoh7)$TT!w#V1L?&sewU#OyFqqbuS@h5I&QqkHG_D)dobS0Z52A z_(~(H$qkKOjKp7E)F7|Mo=HZ+#e6$NYEAQ$&hkb;1>!7?U$Z&2{<{z9Fp?ZDS5*~> z2q&s!jZ}5`n+=S<6A=)Q3e1fp$hZaHMc=)j1M{D2V0(wY$#vjd1)FN?2&6df;0*@2 zjphMhJ*5o^gC<98aH7$#tJsTZ7Dlb-Fp=O%LLOa;t`}^ofn*B_y2{JG{;pe z!ECDECn+b_W#znp55sn(HTAAjyTQII1H|Ds^wx227cb7UV|L=Mtj~#Ywf2Sf4lmTj zPhzt@uO<}g`Ni!NppnxqDHjs$-=~(IStzy$F>lp*_*)|Zb8m^)Y+3!tEEv)8Q3-;-bbuT z`}`i^rR|feLTMc2dbd2=flG8w#GRwpO4$IB5l5f;Pm*k8a=I6NWxT9_6%AW;=rx=$ zlaa{Kl?>Pr(&bUwEKT%OvI4OKv?S}qMDT~7JVK0w6PH?SoHNS3pa zF)%e7uUOL5Gx-Ylc%rI=CTm9INp@Hdilk^?W9#tsBLHH7@>6+flok^Z`wJDOC<>B zqPiXVH6BXWHT9(Oc=%D*I-Ep2r7~TsQfiF19Yae%sL6pH6u|tq8@A&-=hfXsCr4t&qhDfk@d5>D2R{67V{jh?`$ zT4G#ZaOd!_jCEW;wln1DQ?D{QbeR6tnwOZKvaDXv*Za9f$8yM+2+X6@!KNXNvb0`s zwnoQNdw%MF)k{sVUR7B`g55}#0b|TK`aI?@kC2YJ)e9bU^#dpw=Er);Wls&PMolG( zQN@|B`1&iNnK&O5guq)u^{>Ay%qYUdeOj@;P_(D8HJ7>SW?g)!>SZA!!DsR^yt<`} zBRc1)m6R|?lM+e$aVayedi&JNyg8%X*in9Mu@#>^l2s~XP%6FP3p`bd1z3-K^SiHEhlP*%M~lGKiOMMUE~Q6&mYNc~zuI;pXpX z{qw^`BrDGVVWp9+?F$;Xm4JP;=0sj5euzfNdLZa;tg+dT{C&W4f52aEr7j~pQ`9Ut zSIq2SyH`646qGtlP`SPn9W*8Bpv8lwFJ)i#mw$M7D8XBi>>tWEP&D=3CO))|h<{a5 zWgGAl95PRna`bLza-ha08;usQ07D^;foTaH1B@?Mf9%V{}KlW zi4Kn|=(_8t4WM2H8+^GK=G+UfL2Zjh&}dg){tB&dhWur!C2j#HsX{l-Y>gAZ02l4_ zAWvlbU!(!)pe?9}Vw}6| z*}<8NA->ql7<}0;rb$O)WB~z3E(!HWZ0)_)mc4Rcw70t|sY8j;L5@V)T%CewhWpN^ zr=MyPK)3T66&tG|F2!Pss`S}u3ExwqCt{TS$>$r2e_Vs|AMn#yK!^@s`cAmk&Klet zG{?eYn@;&IQpoHmnVN4O^@?!DX_s=^4rbm%hIItHkNKch2YdGx{^U2hClXQKc}X5k zNQ{v!PyH}qjzXCPVjkfzaJBaIG@aYu+!SiN+ev8I*=*y2;o|vSI1cISX4s=3uD58> zgUDDC!4XIVq0Uo)$e2HbN@-#-+OIJd*lwmhW^uRcDkMNdxoSl+kV+aOX50Dwq|gwT z%40y?OuPNZ1VK|(`@Q20W*{r%`ez|ulYf(SHAB|?qux*ilow&_T9F#bXD+V2dR!*zPDLJ&nwUvkw>17Ja@6VZC(B-vpOtTH2@ zSf=ee#dq~M*)21*dG2sT@sY6W2MzLM7fQy_dUs)j}8dvk6} ztKXIzj&+!v!fz?YGmoV^sADRBjlPP+N zY=w$4xCD>qqTSu>i{1zd0a9iKAsA|T+w`NuD$;GUE9lpd&BY=w#?m;B0n5j|{*EeZ zdaD0qfn^Okk3^mcyACvVgYo!%?aWOZ>r-Z$f#ocfYQ4JGmP1z}tq#qBrk!~>1S#V8 zVU3jgsmY35bEljV^s_WFD>N4g73QjP5V0%D3zOSaU3%7DQu4lpO*WF@kd{yb7|eZF zuZVT17;cm?dtwVR7>qq{TJbabMOU2zdGxI!d_qkRI>l_0fV^H#vNapc{{mbqam_U* z8mGJ1`Ptg!f>?HD8ufal0I)6Jp4Hg04`1N?b6sj!lycFD^O=vacrO&Q*=0s#0Oxt% z5d*5L#!?ZA05qI;{Qo3sHTMPg+ux9jS2;Y)BZONKlB&NzuLYihyjgB@JU=M zkg^o2QD<%X8`G_7j|OS%lqFe?cwXqZLl>#8p7(BAiqI-@b!_3bz_93H;u9HG3d$Wu zDXq9Qk-W_jqD!$eB%{v#3GHj1#MTBfmA#1NcPD8VUFz6!cTed4*z9x*^0u6 zsRgwTE%I5o3FP4kgRCitKB^!FeDR~mW&=x6`D*ml-tMn*fW?_hOlr$PM6M>#P|mp8 zsw+c8IHirX5S3_rutym@eDxkVwHP|d6$47hwFX{Gv@`J>3YGG6jKK3ZJ3_d`6~LRs z06#syC((JFJ}Dv4f#=G?f9dUq-jGK4LIP;k8(&Rwq`3OV8{iU+@Q{Xzc=pP?DKtQu zkyl+;&QGJj@ZyzA#IM7(?!u)Ti}{kZp~gmPR?OFiE(TI$nl}t@#LJ!S$Yu_-Yg8Ya z6b!b*Z8Varzousr@Q+(>md?b@UbA!!?emAwO-zhiIQzmqp=~F=HGUXMuzhr7T&9ll znLaBgsBWKV%$hH`8D4FiP`flE3A17M<|&zM-CZYQqa((zcQ`{s?%WP?aMSa#J}znh z9drLl&#mHToh}avP{GhP{VdbMT$6jveQxuN6w41kp-5#v9z>a2hw-#Jp){f2vZi@6 zd6F>}0GC+~Sa}qR&>IJ#fhk$9q&DEE3#xa%mubNG;+Cu9W*$Zw54xF;;H)QVCjNQn zG%wjSAB?PfoqG?9hUoR*e(;=2?Bw*vRl)O0hx<2A&wna*#g7ux{-2=Y=S1zX=rOG- z>PF6mo>V`usE8BLRrvuDtG{}D&Ew@q7H_S$iR3S$scz7mtSWLF$lPJ~oFBM~0#<&_ z^vHm0NE$ZxK;KpC^$`yC>1`2A#z*L9W$F{IhZ5s;3=#os@}W`yMQ0%26Xk<;dPl^RCVCP5`iSKHC0@kh ztB@9?GqZoynN}cJ*#G`$B*RAH4%E)8`LtALh%NuuMD@nwy4N+e$f0_27SNIMSnq=^ zRiQ(XN~NoXXgzvP!`#W8KJDjieR&<*TJL)L{huSsVATTRp zvahop#i!6Sg{U;*(yyZ2O-unmuY^o~BKLctP#RR=n{b!%Gc@(xs)=`V!O&airWFVI z*uwu67-YvW2(p1iFl2YCLa*)N-nST;(-YT}$IAq~wQIpR(Gm!2K- z!!66MO-Um{WUTT$c4*JCF4}Z&Q$HSO1dlh;ww8Km&|>ZlkW+bcw7K$q2W4++3UZ!87<~p`)ZaY6&Al9(m`5*ULZ~OIKe0^U~k)$s7L^3O($f+jgP4$x=vw%*O))PN%%-%iKsRA>?DADO};LX)a z#;!<^)S?9-louj0U|w}by(b|r@Icuu_9z}nVM)d*kMJHPop4{`7Iu=*`3tP@7viKZ zrU`6chj4y@F#19?a;Z6qdQIme3&44sOb=Cd4m3YCecz>uOx0O8aB`TB`p0zB$a{d&}`y zE_9SWDva$Kv&N0T?vd$LRVI-ISO>94hG(xE)H@tIfJz-FnPG?((NI*#@>;b4Kaqt%SI%wYYg8vu=c4|%qS{K-co4^;AQpla) zC)5ZNJnv_ISDwk@OTt_o$d3)i9^*zt_uq1$9B1?M@Nnf|e<<`Q<%NGuiO|tJ(KJ9W z6C6VDriwwvZRtePL7*t!Rv^rH(uA@@8%D>=R_BCB;!uWh`i}RfKz*uI<;!u?6%TX_ zOoZ7+tPQZS*0tNYJdid1noENZSI*{I@X;RX7>%V*ep2qz249iv*PH;czP8iIUY1TA z5dBDj{Q*{EC_K+_d*l`yc$)(d6DI;_sLi73^veR*33t6##XznpvyO-M&uFTDl-e@x zB$GZjE+rUL!SxAxDLS?Vd9ARNg?kum)~o9R`)ulRbFapx;pyM~qQQ@w7}BmuOEsI8 zX(#E2!SeYwtGxtlm_Pa8^%09$!DQtDUUY*LHT`B6r^9mkNV+7r)p zns`Ry=9SL81B8>UAvO+GrchcTJ5zO9CWU>Dem>P{*LsuZ3`(>$?S=6khem0NOkD0U zPUrO*w83B?d!X==1yr_-w8KKTE(XDOWTM%_AN>mH?c#qs!v` zxV-=wj7}x!-kUoPO#iHWuoB8*ZQq!vC!yWqQYhX^MI>>M`e!PNnJYZ7FuDpr~CjV4^}-AuRDSd(0X)_KEj zU?lf*Cdt~MmE{Q0dvEo(MIr8zaW#2(%2CwCKp-yP?=UIjEZExlhD#Yu?Dir$>cjsi;-{Li3GEtR z?D_7_p9E>WRBB1GGBd#-BK8Pa7t`KH62tR5tvwIxk-Fi$U9`T;(Up`)^r)DPo(X-} z8xC&3X}}tRz8WjSMc(9<$7=RvSC@!T`sC?e?o8CW(1>8;*sOwYA>$-(57Dxnmxt)|txkL=Y$sk)74rbLJOw*Q)4;XjEBt1AiW)qRc)4dm{1cmMVKCF zKh=I>6e*@Qt+cpy^|3z+iaUOLOg4&F%ppL@FI0j+f*`+w7G07M1m`N?StUidPl52d zl&35dx~V^KbE!1rUe&EUof+-^Ts9AjNHZMd@OBYpgbQPSxei)g&kNLwPy&bCi@NFU zGH9|nZ58XlrKy0#WjjM??AKlvG=DJ8Zs^y4dSFK9f+KyYT{U~ue%-~ty}AbFG^1gj zM9?FMbH(ZW>Muqc#hd)fn`t}x|I7H7(C66Zj$G6 z=<;jAsKzVwvSlk;3l@uR;9xfuI`R}77t;Ad)vU%@iFgvhRG zXg{P7E02!B`C7Sr+8C6nmgMnfMz)nCV%7PBPu+vLCLIi@E&_|+etmkHTuyC==}fM* zC6$$GpNAOK($|5eA1^4FV#DMUJ;Kd~o!*M??C1mScaEPW$D+JqcC-~k({m@NHpc59 zyix6PTAPO+A?&Vgsa0Cv*M;gh6rHIFpG$9owhd_mp1(8kwnT=rZ&s$QKf;MS`Nv|b z>6$<7dZa&8HHVS5aUk{OY!7YIGGZM!ESA$3UQuZ?)w6kP$Iab{#&%{!1O|-^hNvF# zhxq=|`4C&sUhb?5$2cGtBZJMvQOKmx>xPbH4rk?dLW-{XhVBqU`}J0ZW!*D%edQel z`)#|G8EsZ)%ZMkc%JBj)5DQePBs5MIb?c$ABrlBc*D?19r39<4*&P-b)=f~hc(S94lsAol z(b%Y=`A4aX=!o5igc5<^wLK3Ofg~t9gC{U%#Gr29YN1KE<3SZ_WIaWQK$|Ny2_>=x z-b4+BX7G#qZynT0#~bd(bx0w6Duu2%waNdxEvk4;u9*ABISFoQt)b0t7_`;-+PSVN zl^)duPqKi7wnV;X<7RHunj1`v@Kz>S2@2a`jQu2v&J+v`BKeTu|8Iw=Wk}RSvt|2( zx29XHlw^T!V2)CMV-U`xj3gRWtO2Pti7jb!!r!JH)DtU>UCDqPX7uCgAN=6o0aF`G z;dR)*Pr$?s{wIPlgyOaTAFD&b|!a9o&9Kr%G3r7asgr6EF0;*cOZ>0*{Q|624RIU<#+6UHKiqU80z+~ zl7BImpR6Q_6rL!)JG(T5+=%!F4Ya)Kh<(579bh~(7hQp8k;}) z-llXK`SjB$41mmZbTM$z6r0g2WnArf_@na2J!ZchT)bqPB?3wb3ja$;SvwAXfo$=$ zExc$3!Rk(e8v=``6_a(`E1q!Ji~dFBzwgVga_}YZ+nNSGiVD%uHkC%n(*nMI)Wcr8 zLKK)DUW31d^pD4yuaesG4(iR@4INawuxpW#DUB@Y0E<|<0~BJyrk#UU$(j1TSu6V) zPtiC*i=|}eb-b^nO{mDTu!_;n8;A?{M8#Cjg{14YNF_&_jO?|ObA0Naj8;>wie6h{ zx}XNM&7r>jgN1k=_>Qs00$l}|n@zxQo|3C$)m;VzDj6+1I^%lfC4d4-lB^+Q-w(<&ScK)9$yyN6KvguraBG5jEc7(Yv}mLpzv_QIu~{W)p4buTOTbj6N`ZDUhXHdL;f^)RmDz47$z#SSHKs5BN^1@Z#k2ovQk!@WO6PJm4g=@_gI!_G9_rr0B6?r|Q&Y|Fo$q5)KLYt?iA( zk#9kbn|wBTrATm~$jXSxz3Hh2M3Ek@HCW&tNK^&GLXRgOqvB~?k@%+mjBD@DYH{Bh$_L}QUYvN>CayZxk^@6%kWA6KqO7~q3f^`RoniAv(&(!rY0aYlI1`NUX?yAjiOfO;x===8 z7;Su&W73H2hk|CJF@iw1KZ7Xwb|d6=2T25`GO^~tp?9yNUME*vxjckb)k2=*2eqPG zd#&lpfPEeTpzN{(nJ?ZHY@z)7_%uY#tR z380>>zP5Qa%jH`5mK-z;)>Sy^)1CTL4ROWq1lD574M+7_yLUa~J;0oswy6sX8P&@H zqZd?VxoxE+(XBP23JB?wd+xZtk;>rX=$s27(!>$$5q?YQAroM!h_CDKWg4VJqQ6qE zgIw&;E(=I7(znmx?k$9y&XZ{LlBwMKmzdo9TvH$Yf5G6lM`4B)2n`^%%al5~Kd*+h zTd1qi6wwQC#Tt6@lMAX$KWW4DD6n!-DQetf)5WU4x3CnI!qT{j;)Xr$pdWbv{@5&;mrnfl8amvVMV-2kf0h- z76=%zd>pq<#IZu?LYC!r3B=_oyk4>klygF>UN$=nGI?0kxaN8ROGxi%Zw+bV!?JBc zLX3{L7zVAtwh5pO4KY7MkvXos&Mo-pqbWV%7aHjkJbgy)Xv0zD!F{cq8H!bVeLPCrtqRv;$S?(zJqxVj+O3I!(BNbhX2_mLS8#e!w`;EQLpfqu zw8s|30^x#qQ!zw|z^T9vx&}g4AlmmAuP<{S8v1M*asL%xn_8v)QCAW1#m-M7Rm}mLHR)sHmw}Uv7|U8>xWF{tOESC|;l1zmSBv`l$rQdPY;0Tg zf9e=31n<_A_3blW89J%cn06$5xk!?P2v-&*tC792*sDYukMw+H+zb>TB6(XJeO4*Q z`}m}i&Fy4l(_t9lB}A>u&s?o%J@u$QzbzD*W89|~2mze>L9yt+EqnW<&V|SmZ-<;h zv$&%Z@$=a}2md#C7D?H}?~PByO2bs|{H5ihAGT*4)v-As>N=oTvm zukg0F5=o5v^2mxu>1J%O6a%m0VuRhPcCf0z^RJBIe+|5bW;F5h{ zqqf%1suYZwe}%6K41OflE^ofQ)X@ERqoER0A8v`aM}2k>jDzBzT#jQl=vC|IXcmLw z*DCo!>gri97t+21HFNC#ktzMS3tSftyLKI@&nT#i+Xa1owCBio`9?utBjP;!LB4eW zJw*)%)BASrOGwLN&>)>ORSm-|JtoZYP z8r8SnEDc7A&4BgsRU3a>L3?x41UOu0k4L(b#t1Fo^|)Ufzlg6{HP))d9fM^hrD?|I z;Iu3uCaW!pFoffkKa~3z8Y_#JtdX^)_izNqOFHiR9H}_HZ{#|t9R|SKdz}wy6AZQ- zb!d;*KBL>I2XI;~c?4xeE`_7$yo+s7q*FPbxf8?&F%xB?Wm;{(iSQX;QBb!5fK9!L zjD+Rn2F%Y*C8OZ}eRG_tx2~DBhnKL;#NbGV$P{K)b_H}FWUZS6ifN!B0YzE9n7=GQ z$iO2mz)BcXH%2vH3U7tf>2?$8ur7~YiQKW&Fny`D|d)(ozLG4H4QYrMXZR@9#W2|gFoLxdfBg0iU zB?zB5+ox3K(yykpra-f5-@1sF5#%**o~mlNCG@ej*r3Mea^>l&GmesYNX7_UP*|-RLe2 zqKF&%Y4ns|vcR0*CRU*+&5bJ^QGd8yZbIz&o`^Q>#qU1Tp)Yu{lSJVAkI2!5R3G{7 zYnAa&(k=qsB&>w{7#K{GWdA}|t9ZulaB^C&$UwIucDgl&SdC&VxjJU_KKc%6y780* z;@QF=HR3AbRFY0w?<){gHnQRU*?EmJ-lwplVCSi+e_Y_$IzU}=xR*5VTPKcues#b0 z3cXzot7XY~=}QF|3()}&+r4zOa@=ugxDv^7ox1fZ)IZ`BCkAKRHuK@YJq)YKH zb&%v`o?YSh%WpJ(%wyo8u?4Jcd_ zp@Gbv!PMXLQrC(+>9Th0Cu2|PWXb#nVvMoK3sTgu=K)G| z$Z}~?xB(l7+e(^SI>+70X@q?V*_x>Qi3=<8x~MA4!Dg>oCQpP!#<;e)R9XDKH79kz zwVw)am^UoL+x$@Tb;8@k*IXX|tCwiK9L#)G*!nhcl%9LRaNISm^SKItsf6hk5u!C( z`l$f`idpIIogU>B)xX(?n^4*8LiPmOZ2>76pVC^)9QmE^eX}u@|IJ}c(AVj_x|bRn58`EkU{W{fe9DWmhviNZfTEiEKqB75)^(0a9NL9c~q=GawI zi%N*dGRsc-XFe%d-UjaI_uTAFvL?P=7l?aSR@B3x@furU_ZWx6bdY;!eAMOk>ltUGI>;1~s9%8~c9 zh1ggNp!_d2pMBPfPeyGF@9n$jblF2$@dn>;{|dV*VIbro99kKAe$5Cy5ULUA2)`^U zXZ8>AJ2gvizOs0H5J?CH4HfJ)DExHS1s~VQZ#l1iL zQ2U9tjQw~$!~q5!`5lB3Dc#e~?G~G0XvvCRWyM*!rEP2ba^3QuoOthi3Yw1`lXK+b zj_lGbQJf=axV6c#iERjj=4j&Tm6O1)YLrI@t)cHviA?Fxil;~;EL$156g$M6RP_z^ z+MuMVRpE^WMlZ0E#Z422K9atfbK6P{+`@Uh_o5uk_dycfy6Ub^s@$ZMip|&jAMvUI zfe2U#HSr=r0DmL9=Kja3lfmL0^3Bbui4$bA*Foy~=8zX@<+ozU35ozJR8yPj3s00+w{(%Fkjx?QZu-l_MBG^jF!k6it z3HCm%OAm*$PKe+IIuf_+8=_6mF5X0hgPI<%)}BWZj7R}o=mriP4m))q^6(AjZ{?)6!eTAQ#Z z_mw#5x#qKkC70peMc-s}3JQ9rBTo})LF6UszC(5}@GBAdyRoR0t%`gl^a=n)-z!|6 z9T1S%hoF>VH+l!4`^BR=mmxzL;G4euc*}S0KG0P$zKm?rdB$7#J^m|GjlnfJP@HNd zHit!1BV_M=v;7SiSqxMQIetF9o7b-|kKPwD74I{?auDr6v`Ldeo zbQe^jpA+z`OJo(w87W}TF^tZhy9313<_;Z(E3JTZ$h2oDh~g8P%{07|(3qUq)f1G< zwG&upVYYGl9>0nX0aR#&z@a(5GHQto_sOx@{%{MPcf!DN(I&lZN9OP^#Lv>}x4w~c z!V748Yt)-Dz#4+mj&b;y^r%>kvLu^U=#&KoGTx%zRIC8|WI3=Pz(e9)DvzIRkvV^_ zpB@>yu7`3VLQVi-k81<;othwBE)Z_@E;acWO=TiF*)&mfBD73CZz-x{3 zYP``tPP!$x>^&CV#)$UB^q#4LMI{_^g|)PJ@C3DaV*Rw;Cs5sC`EL5t``v_bBo(JA zc7qT;JZZ?&t`}@1|2iim$mDDZ)-uS9?Jf8PPm?a;y-8b3s7z~w7N|v#4C~Z zL}kwyY3hzm)I`K7heq}I6W!@cm&vT+cI#E3z^UZ|NVpH0dZO0UEoDab5c6Beu`tZ- ze)kb4PZoy^x43g^ZYqw$!N z!HDVmFVS3Q@#bhLOJ?U;ew=ETE!?m=PSxK|r5>{Tn}&gryZclBdB9eSnyx;#^B|W? z-Fm88O5oqDRIS9*9qt3-MJQR6}L8DAwFE_M0x1ktBBm0Yp->l(9Z@RR|oy@F;R=a zmkZdE)$;HTGJaos2k4g`6Fo#Y$+zOlkMwefLe|3hRF;OKg_g?+>u(pb%Y6MtylOM0 zlJ6#Ms#kx4(xI}kP2o20?Ek9dDiwL2U3{-$l~7&y*BJ1GCiyuI#~F=?>9^fTMHo3R zA_T6C{|l>s^Kt?L(cnRg`f%d@)7`PB?NQle_A7I~_^$!BWPYsg0-)8aAmrInYr$s> zXdHbJlpq-N=gP_TZ$-@QAg6X27PO{_R&i^ z>VP@}B6?cm3ZZrzG)=XIHX$0$t*kg_yT;Ef-&!Nfn8$5;*HyC_TE<}&uF z4856hp4G-BM*ZM1Zyw#bp5`ZCzhaIk%dOhsZ0!zj9ojk8{2uzcKY%D*Zy%xJ2m{!F z=~^r^S<{K|9_LxKuvRe9-=2Mxu|e0iVjvS+<1K|D)&x&oC3LXlgO>MXqypHCZT?bY zhUTp^p1`4E0Nx?S&>UZ2=d?f9kFrxACHHZZ73z2F#KbZ6;4lX_m^KL;56k@tK-ILY$O zC#m=9LeyU?g#Pmp@vI%OC!)>)pE1wj|6{%1mm<-EFdZI2juQpi%3jcL)}*BBQ|tmN#~cacoZDxOy` zxs7$#YG~9?rbes%Ap8p8!j5_E+Ws}lLC17-`T>|on%FNbvB7v=Mp$WWDjP3{ zmVM2f(3ulic3r>#wP&`riH~2X3V4|!UrYJD)rkum*Y&UmTB}^l0KJR@U=|8L24R!t zx)0AKy(q)4iwQ6M-UUW_$>{UbKSwIl{=*e3+!{fbJ6WqpS>8p99L0(;vEu1c7;Xlj zmX0<{xWH(y@RfA)#IlNd2R-Or9i5TdnuPnMiibfrwj#$LN9U(qs+139T_#>Da*|$a zPcELv2I*=xJ;6~l`jOkw?5~TT`{&FlXUZE~)KEy?+7^kxvovb`TQ)Ccyqv11GBsuy z`J?W#-pmG;P`ArXdlG-e>Jzw%0k{PoffkY{7jH7jWOj9?hMCwI3ziLT3nx*DC z?RlH$>H`^8E%$!EDB=3i;>cLLa@)oRj7+z=zJk^_e^BKJ=RN1~MFBJ3jmr8C0$}x> z_~9khu-NNXlh(cbNL3y%v=_27(nyGG{o?`TwM-vGK#;4-+%)3|^dpHUA>^w2o(87R z?lsDi849*>t~D!ql&u2lPdaU zg2WN1TgyBLr(|0I;rxoSJziHO3U)`6^t1uVUIhbqI2PRY4ap-L6SUIat^?aRrz-G| ze`OKcApV+J&o6Xq!aDzAp{7NdLJw3I``toi3^t8O$%7comQT95qHall8L!M`4Vhvk zlZ||Q(k5_{?or?zg`z<4LMPQe(mMw|(oOLqIyG8b|2vMBniklQytFTel_#!)lGW?! z4S%5tdGH3Mb#REbA3KofT5$H13aR1VT$VKvRNeZ31lPjWN_F;EjZ0T+c}s?KKM05Q z^FCqr=RyVPq4)*__eGboO~Ov-b!Ev!!k2a&vYi^^&GVwgCJu<}VKunDFHA2H%6g9K zam#1wvXZQ%WMeS|+D{?8zI7{p0}np|f?%s5$h9Krd!FFdw~e*Dz=DMsq8>5rFBwf2 zxWd*)Y5F?Xc#~t!gCBlPAy}N#f+`pt>p=@A#2(i0#oG0{e6!0A3>3wX6y&20Mb~)& z+k54n@j!8j=^p1m+7AdW&D5iCYhF_DA(t-V>sFFu!?$VhM1bqu{i;sPFn)dmEDB>y zuN%(C%=|L!)vrg^(rokgJ`lZy%&N@fwP~$iQ6x0jW9bp6*79!7INJBQ*Y-fy>Ana^GXYeOa zUjQ@Ia$_{Lr{Vlf;SaP6MFx_BOt4bBJ!AdJi#BY#1=xB1oX~CGeS*V=h=b-F(A7IO z%%bYPZ^B(eyhy#yut$2wc}k20n}JNoh&Nsk;+FHoW>i(aUoMExQGw{uZcc>tcjKyR zH8Rl1#<#e^Xp=~u>Du=pdvN+k-QoQN~A1_uq|CMiX~gyC#3xM#VXYRAGW(IcPX|y$-LD2-8^=H zvRC}((NbDruGU%LbTgod?Ww9DV_rU6sOagvCxp5grQbvZBse0F`Brj^`r2m~idTT& zsjXn2%?qFKWG_Fd3f(tF47i9Z*$vXZj;UclQMrD095bJ^)NW?5g~G7T-X`N4t7bgK z7O7%!D+g^N*DB@BG2e}i(vLD^u`sTN=M)|~C&u8JRX{2!(O)9xn^6k(*?T939sZ*S zRBQk{K*YbxJ_1CuAsa5IgV2c3?obxir;TOJ{Lg)g=vThWp=rR#zuJ0(Jjz9e4PO<7 zgkVqFlTCIMujGU#g4Om(Y)sfHcvti%Aa1654dN=sW0Z6nlcs&<8q-JugFDw6hr32t zmI3X8|7Nw+WFCL1gCW;y}-XR?xBE z*f8>&4{!0@Sw0?eYuq0$x<(*9h4_K4wL0Sa%#~SO?1-e1tyAeE*xOgdXi5MDNp&jH zwFAgdx)?p;F-OYfv4Pu7z96`hF#Im#{n35nm`HN>9L0FVAviHez~}GJZ|SM(h&U=H zTgn#cGG!os7ivuC@(V!u(mjs$yU@=i;92RaV3`b(qfg5bwG2q zcgSz+S@=kO`j)Sh!w2u5mfBLs*GxLPXJyd((TYd%dMqcsb-BKe21Nf@_O2%v6_N30 z&JdN8bK|X#nMYoW|AHUsVta|cfXWI?zB@va&Et@d{3!8JKQpagv{bN#4|P1~SFj?Q zk~;aMO?JA(Br;0P^{06iDvN3eYVL5MVE{c3X~M57xX81qv8(nj#20&Klw~<>*9^V# zPKFmXmZ)K(njVVkMt#R7e3{7ZxnkdnZ1N%s;2z={c_0!8mkxJz7hO(o_xqLpqp0y|WhYM`v|mxV9liG$KB3zNdp?Y^K= zpvh#PU>(n>UGMJnOE@laYcCWzwW=+-{wznYu{GXRka;SU-mKvJd!|y3JNsl8WLOWB z^f5IM>8L>>4~RRg0$NFZETmU*Ow`Au{Q5Ib3Mc|WLz1&6m8)!{!ysP3L7BR!yBKOG zP@MU;LzzXdNx;hfn}Y9Z%<=&lHQGi(&@VZmYQ1Lq2#kxQ$hV5YHn^WJZ8kA9iQI*? z1o7|0$yQ_wYDz`!-u;E?6`;7=Ni`sBfDqE}ueKTZVS*L+OY$!5s<kJrhHG4<7?ZsqO&M_^(>+>n(#gj1C4kA_K+Pe#_6ZA8&#(cs% zLksllVf}h}8I3?15s5D)HNk|6XOA9;+!@=9!Sr|p zF|o9$0%p^<%!J31CD=xI8*MxzJiZYSx#o}|tYfN-B^q0#ZQRbVq(*Q%%_6%Nw@37b z<$C8t(9G3T{j?B%ETc*~(%59(4e0t}%LX-l#R-kSGR2R|)~?xTZRWA22t>~IwR2R% z2%cvnG5HF)T}mJn&`p%P?x8vmH7{0O`^kQ-Qm*U2T7ePbH;bz~*93SW1^YtRbJa&Z z(qsERt)Z?NdlT!O!zNDu$QsGaN{MN=h@NPG0wU>ih6K9k zu5VVJh@aX7RPq0q4y{TUi)Ct^?If5Hzj=^OA{=ujr6se^s_pdd59Wlp7$=biEoydK zqB>MX?B33g-ERK0v6RdFlB*?fkp6skiK5=E$F*+Rxd{+r2QEa`YG)R7Gsz8gSy(H4 zRXSaEOPQct{Z(9iMeP&+u~Or^ihAoPxAsT1@xf3%@)dUf7(w4^!soV!-G%ap%n}c- z0Cu;#iIc;BTOJu&G1$%^rt%ujzX=|=!k}_?XEQ?KlO2{s6+?YJwAXT*{zLjpx{4q5 z@}d?v&i1nK0KTOxwU`M3Eht;E2BWq599yxd5YIr+U@yTINSkT=)rC?g#cv|ydGX_r z=zzA4h}I0q1#+jJ8AIsMlJ_+JJ-vV3o>=;j$$YGp>V7$a`O89m4>m?%&p&eu3B?cU z(d-xTqsH?scB6Ro_+Ak8w3$N)?+{dCX~yi2jl&st+mo$9B!Q!^&1g8+DZ%nNSefzm zlY^;aFA-jN(CK4c)1t?G1|0nSm{yNxa$TA!dNH*Ah{U!(sjN z@#jbHhgj`h;P9C+Lk9}ICZ)h7irgvLQosPepS76Ev|rP5xfSedn;aEtBob+T66qe- zUdhc>rDf0M2g=04jhhpp>}iC8L^)F{=_ux)3I063phaNxN}gu) z&gy^ZcLu73q)86qDX8&9Zqb-zOVx$OTck+dEz$;KdU~QEsL7J?hQvr-5Irkh>&Rhd z*2;?&%$O@&eQAt#|EdLo?WmX~L9s$1sK{wi%pOdJ81G_n5k#^^-Ip{%5vD6k&_t`r_ zt+u#;;p1NG&l)IfcOLZ5)pO9$O*!lL{D>JzF>)QFGa?p7-GpBeRO5SJp_eRQQA^T_ zl;Y@njk47`)0{BTS=#5<(7Wep-s$3JME6g9AVtM}TQ*bf_x||@p#BKRzGK~ITcb(w z1cpx8*wp7H1lT*2u}losc4KBC*AXF?vOmf?W{=-3CV$A7d3$t5FTCsV42lVId9;bJdLs1$Wnc|h}Z ze~2eGjFo`nT6ROXS-dcQy1I)nmc7{DVuGqZx7OiJif18Rz+>|DAIrTx@Iv;kn&>hs z*t5GNHPXJ|c@mUVH2fpJ%M4H9K?r9atkkSuz9b!zTc{1Ss7sxF!}TXTAP1v~hOP6BnIA658JOxI;kkU$>)F#caeZ_vpnLl?{lCY-{DX0g0WY1nPb)il?Cp zev;NAi0H|m3a%NOk^k?7b>}HW{doM1w-OH~^dR)}&Eyg8L!fS5 zFE|sWnqKWMdY8^{3PArCNsNq&@fkng6*a%C_cP+DW5*QY?XC2bu$p7DzKoQ@*mSsl zHh?WXk<_ZLM9kac)+1H@+v&5agGRyi+a<_`s7fck<_Ixoe}U^6rEjOloF!kWHq}VK zQLi)yro}F3sk-iLvEEuSx z>{fwOWp3Q)z6G#+;ie6Lg?gjf_d_A{4vi|l@f4Z-AcGR_M7hk)IOZhIhyebDWOQq0G z*NFOkUn1*2fwmkCkT^Ystsvr&vb)yOFw-GISTu>3deD1$$I4e=IH;dr8#jg>0Ajmj zTusQE^VGj-c#Ax8$=G&`8v}|`IM`Gpl}L$4#lux6wpM?@2@N;#{_w)LrJ%Ea4l)tymf+-TdH$I z0#`=$h(`b?&vXUzdWwx$m>SvMq@$Sw{!D6Wn-4%~_fkC(ard+DB4!f0QxCXt63Joy zL&kv5sDpY=f)ekG!n3mm-7^-AW%3jjZfQE;MGW%+vzyg-o93I3Z+$s2YeFdU+`CSt ze86079-J^(swjtOg~^LVK;e?QRT70gr>M_AsTiWa9F2gt+WPb+PB4`%2>F}z#ML0I zz8?i_q)^8b@GKW_fQAIfc#Cx}J*c8S=W~AP*eF(AV zk_VdNej0(-i*`!RDzUR*j=Y{g{jpsLM0dQ;xe&L?gdsg<*=H21@G&+-X=`|KXr8C~ z?Rn^*%SRNbhrUidu!H&DnyW>HFMYqrT+({YULvgq-@hf(@5xD_X@+!-!I3Y&o7?~K ze#$&^>Z(`-o)v-=YX)caZrJmbZaLkPvfkZj_1KYu;a*~V!NZk+p&Zuh3t5=0Dfu+d zvdK8Kdy%vhQ|LRSM6XdIih}*SF>$>O zP*jiry%PFFu^8V@XmeTVk{rjGU4vR5+~+5|e7~($3r4*)405LuJc1RB-ZP2W7YNz> zXwn*imVyVnj)}XsF;kTiVlnOA`v1r;8zgW`ZCbs~ZH6l9YSMqn&ZFwq2`QoNcVj2+NQ?({e6H z`!Hg!PK1vo0ZKVWt2W%XPINwJjI#paHzVx29SK9`?)mFnqt!eL@2CY&DXd^RWAaG6 ze+h8j+t@D69sgA|qMKuz@AF<_Ee8o5`3c?*_?`Vog&xq zyDG0xzGdP5Q&#L^hvJayW|ky;vW$x9CnF+My4P#J$rC7iZ$gE1UDbhhgp%$N}`crmm~y-)M$Q0 zg}Pp8^t7soaw#OpJ3&a^?myqPhenx!uBpOk97!p>1_^c9_R%|XoHQUp<5i>k+l`;m z6|D!v9NynSL2~b@YMRlhT*8L-gv7NHl36HP?JiyR+xsiFuy@s;5eK^&9OP|oqRjeF zq+M$pZ-nL=p`Ps)Wi%_ ztep&Oyu*gCEmku`MN3_fmz2{e+CowD|Mz1aT)%zO4pi!vy&hg@6eYon0#a z_op8;za8WPnBX>^)%r8by}?;eLzM+` z5l2PrSW$ouw0ql!F&EP7><`I(GS9*2e}Djbz#c2a+Q0)xO{k_fSm7Cz@iDjT$oClq zP#jSslM>5@vmua1muLXmoQ&imC8(C{sRJYgZ1#X-?`d*ef~o@}Iz+lLHD(L0biET` z3*y9PSPZ7xeqjwh9u%*>R=|vbq~uWoEuEh{4tKcey>-nmWr>N)ng62Xf^#G#mzz(- zzUGc~kcY@r$+JJNoOHYOm-k{qHpaS4zcPcw`FV_1v}^dgO>Z=z@6td)!(?!@-X=!Y z@VY$a58reZr(ERTJ+;2KV}U%pf{YWcNru9P3w#CxOt%WF64D5ZD{sxtUj|+D&=bxY z%$fUQasn}R9#s68x=atS2@V0vOcZ~42W$<(fULj7G_O=~v9OoPtI@9und(5Xka%Y& z(WN9xXiengpga}+KVQ-UMlfa=MvcYuS!`8y$Snt8Ah5>HYK7wlonmgn1ZuD>g`7=< z#i`Y2fsgR8>?YU=VqNmnfaw}nkeruC-{~KkBG-^oYSK7G8Ae6ZC-&w)x zboigcRK?9xiRSVYYdQ%dBJ?@S(f5j6OoO-XbgJbb9h{vx1}-q=r{lB_1!qda0@;HV zeWpo(>|vF<8P2WaLI)r>&Lu(Z?T{n{{{k*`1@#vu*~>dm>upB{3kZyy(i5(aTKhXi z!;P7(a%gJvkC{1qA_U^pSn#BLSMza}o0G_w zivMF29to|Vv{Fl1A#!K=TRwJf4A5OE`?h&Y@ba}%ZqS0bo-f9$tFH*i#J29C|1%B0 z@^2u7<-}+$mAW7C7R`cenk#3~$SNGi!~-tP_mFs7foMjMUg?O|;hOCMNeni!YX9&c z%S!72^eOcS_U_56@k4Bd6j0v4S0@Yrsyf{TD|`D!R(UjMUL&X-0`gb|2ucewY@@aY zEFt+^C-2oS93E;8DzEKQxh|MmxR2%-s+5Y^e00)u##0;KVx?&VsQoKUCi?c(IjCF-ow$N<8d!4<5LPw|OpDBkd5{Sfn;Cokb=j({%E{yb z-#{9^veb<#J0@6lH2ssfUg*vLZuv!Ip2`ad3KwRu8`*DHQ5o(EE^q53OX9YPB6DYX zHcN-=q+op_Y>?E7=zJVbb4hlVFZppkBEwE-?6Qw;hew8-`l}XJ7Mh7ED?Mor;*`%t z!+7tHwV=EBOzlCW%{L5V@JBpCqNV-wlI^j^Q$f)FH2Uc5$*cI#DZqa2`V)0Sd!#9$ zzM2$5>}Qg778rD?72o4Iv5l!=ny#GKX^5U~zfIM6QGU@82`*9IwAUGpbL)K9cmda0 zH}UX^b**9w_h)D>e&B_($5xV}l!BkYKM71uWHDt~ zI9?yoT!;Ljd697a%dou|YNKEroH#z|ufe5>nzVtJ31i7)V=DhGZ4|_3>iA6zW$vsb zkgg=x&6YTR5hM2h-w!_7+&By4edUkU#%f$FpIav|B*YJ5ufni7R|voczWL@TXM6L#0auP3Gx&5EwFT9TK?eQbtPzIQoR(+AorpUC0Vj6DUE z1s2VwwPhaXQtK%|*XwPsg|?hL0SlaxNzG)77!AzVRI`0Bj@wl;>pwN+js24-oUgTE*LnPUTF_3MHsr>W?u)I>~Zy( zLzeI*TNE9HqNqN>6*k{zkoPjC8%DQ#{VPh+Z&`?NX@9nEfieLHc0PL`RdJD@Y)8Kt zx+9gUTGSil)RiEyT+o|y|5N=87G$BiIqv0wd;@CPR*?OL*w<{R>~z+9Qj_L111A0L z-O7M*Aq#hz8r*^a@P7RDKGuxJ&RK zY+Y`OK2(y~ZV2m2Ed=wi_DDN+8HbXJAWzfx>UMi+E6G!sf68H}4uw`)gGGL0b`&PT z!3de|-1_bM8Qreo^)~Gh31q8}9-pFpFPy*#6G+{J^`DaD_JFGu+V&<`p>+oVYj6h; zK7RL&N+S>mC>Vk1|FhTr{PsH2_hTA|>C#AQBt|@PwXtHPf8UZ?`8!nzE=J2ojuZhu zNqGQnwms!P>hC&ME*~nF$y%J$=bQYw!P62^{1l!Uj=eHqsj7L>lyNAPoIkWH&gbjeKBnZdyQ1I>;pG!s)(g3I>h9kgC{f4e!wWr&QC zm;LcU&V5zNL7YkNox*LkaTFh=D@X5>7+@d1&|j$rJ2?&=1jSMQt@T-d{0h#Je%#B1 z8>I+V4CK>QVwCPE9+%m=jA)*g(^Mm|zrw9F)C0M;c0HfT^k!pG34{5P7r$0%nRP;; zTc-v?(z@7X?gDcwYP4QCV41 zef{8Rx+7`84|v0Kv^UN0x_#oDM`WKozmAqnVBv01H9W| z_0<{lb2sC}kBrBqdK+~X8U`@;g*(7R#r8xuRR$37ttI*=;PpAk3lYXl6JV7NK&0aN zKQz{o680J>L+KzUwDidF6)wj;cXQqb%Ouw+K*#h;c9Xh1>|bmK{mYY@nsIXefJd_(gM$tjGPvqnMa4oH5M#=uJdj1m z{EdP4XHp?fQD{NYD2)~v)SD|K0xt_Zjk<-)lZS?!Mg7Vi={ckRwuEg_qZ*-X)vWN` zTv4(AKj`SPmZW8@q!(+wLw!F)w_6-uhJj2WVT7@Z zJ22I=Jf*hHrSen6)kKozKaE8$3#VXi$iYxuLML=pT&I|dtlNcE0fd()Y>vT zQrQeB7?1m$a7~Lsozln2De=CGfnJ5BVT}ED>!(NZu=sf{#f0vX?^)b(5?)QY zNPzqGu`__Z^1xxBs~#cEyqnIwwfGEH=4ss+6;jh?23eyM@wpSEV*ZY#CX(gESXH9% z#7s(QBzUUfePXrt_X04~I7$%+ksE!G;S^ggrM?KHyK5cz&J!MoiCHzDU(HO4E1}R} zC`7hT8FvD}c5v(Ooi{DLvS!y!3`%?G#*zi4>3(rp{~!m=p>oc<{hz@Au|p?yU1r8slReA7a4Yy zv`2wtU0;hr&;@8e_4Gofn8MkOy%NmB@{5Pn`$9_BAVuJXh`4VJ9N1d%lG0GDbjqx| zM(Uu)Y&+H~6+?)_iK*Ub_p@e-e&yVdlAISJ;RQdINIdNG=Bs;^nGZvW%}BN`LN3JalS!V|YYcs`JJ2G|nArYBy75ns7`W1{ zs38PDtJx2{r-pZcVm5i?bAGFicoHVCyb{_G!M!Ln_s3M8!avw4eh(tR-~}gQreAL( zRh#o{dg_a^JygK6xPQeXZV*(>0aGUhgOIcfUdS*`1;66SvW-19zmT_4oV})r-HVgY z;zDVqeUIW7yw*{@k|by^OP^9d`|e8{rBdX22UnTW+*^|Jqu=4+`L_BwzwXXd$gR06 zyo)bFkb;S^AsO7X0HeRaFe+4Fy=;UEZJj`WO-qRcDzV~Rd$iK1+vFp#H=sacm}Bei z-)vi;E_7Pv?nG3cl=}+Q3+wl^n??QDXzy<|rh}Ee(UT^U=L=jb6(qH}xu`2Je-2nm zO|~c0w`Nkme*P16tH{0GcjT(U)?T6=8z&{xHGZ_&aail)vu_W#H>VM8@y9L2=r!9j zTu^x#Q7l{nL_U}9%sSJ2;@F1X*|3npZULP@$ z#Z(m&pj0FoWUeN-WGo9Bt#j9hAY-EGVZGa`as@LsqCDP&ug~e~{`i)1?y)kaTyTiN zt#}j&vwIqxhkv8Rtc&a(Z<1qx=LNs_F(V}?T?x$#hfN1Na*2j|nx_$QwfcwDi8}9* zDI7#ocxNL8P}u2Zn(2%1?Z3M|C(;3Z!3PVs0La%3#BddU&dGdb8V072FGCVZ@YQJS zh-qq;W3uy55lBQ>bXl!456J(gyya~F@O#VfqH92dS+X)*k4AbXqva&@Y=f$M!UEZ-odmv*GmOmq#2^vyN}ysU=-6r&Q z$AEd)@*nqf{u7S=weRhxDrDn$El=N&mRX03x*$DLI?TEKhp+iZG+D9aWz z@tg}xE@NLM32M3}^8lRzPWR3R7DP37O(PXey(jXX%L+>v9leMLZe>)st1dqc#XyE* zM(>0-qWJgh1Bolc{t6!JqHMCV;brUAoCv$G&vsC&I9gX%2e(`k6UYxMA#9bRTl--H z+@N*u=2nt6^Y^y81#hEtdLE@-yFlriolm~%g5 zu_fI@P9bH`?u>L_;N@Y7FZbZ<@m(nL#A~RkZiYm0_C~#ge2xp=rTVRO-9}$fk#2*M zPw7R#p8D8Ze+16Sg_tJU%->XfOGILb-`bdEild0Kh~H8Sk-FpfhAIVg`7@Taj5g+A z2Aafx3~i<{Lahn4W$c1WtM1u3J>quMh^M=m)M~A&IR<>3lW03{uJ|kHLpnCUA}Xq< z6BmTYXzURAcP#Oh_ zX?4Dx9ZtXs*m`JsTojr%*5x4?r#q04!eEc%ZU6JG=UH^3@?zWD$Uww!U4h04QFaht z1BaLUw+V%6t>~H0fg=hY1E%{^!8v56U54=nCQqAx7Yzh*$XN>l`bo zOGV;IChS}!rcgc|q>UmNyi8N!Sh`rvorZtMgHO!VYd%q8)gl?z&u_bG63`@viu^+0#QFo z_ozWg^<|z4=~PyNPK%teyWR*;cGdd+JdeG~z1VMDw~qvw*av+WP#nqc_>zMr2$PFs zA#=Jiz9t-*w^zdHEqRdcDf(+1p?K6fF!+Czc6=8~JU-f3L1yXw{(ie93XlhhInHL8ha!goNC2ouu`j{31V8OB4|8@sMXuXa|Sks07cOQu#P=`{b-)D}r$Puz3Fe z-DeU`mbE4LN%`Y`c3{s%WlRkx=&*K1xjmWbv4nFMA=`p<4qg!=N1@tKkEBal2w(JZ zFRE>8?)=*+_N>M!gsHFjAb+79dej29v^2jQFd;9Ao3uk}%MFx2*u7PtUoIjq;c)o4 znf+O*LZS<`F?WfD$Es%BTm~%4bK%6qn0I$rQ^rZ*?XkpCSOahELQiLu%Qb+7ADdHe z@fMJ;75(`M`o!9QpYu+065i%5qgK+&LUCv;AF`?A&y}dyLW=x>jrm`*IHixqs`VL=4yn(x9w$TNThE7 z{$6k3@m);}zT*B!0>Rnw>Nv@n00v|ub=UL4DlW6^ksfZ+AT2lE7C*=`Q)p@X7b1t$ z7aU7m8EmrO>#!35b#CcKXE40ehx#LMlT4lIFKd_bN5PiSC&_gWt$7=uq zZ;@z=5~<18tU8`=%fP66-nlC8+a z&7+^?FZ(cQbR1!_4(iGSFG=NtF6z7Kzv_1cu@P;ZQyGpwoP9j+|H~YHb61B?04OnP zeIr8_F=z)qOuM77&uXudGsTray8Tc4UV^RhNmn(Hd^0^vxO_>aY;yg`mh}x2mzn z**T3(-RC?_>h^K^uzf=l{#$ZL;Hb!i8_>-qyhaO`Y8A|9w6W;2<8wG6CcCJI-5u(`M1J7C3Tm;`RG%z!9V#uBdrVhrLvz3Vgn3S$Lt(~f}vak9FIZR|IHS~im`Qlk4zv`LSsit0?aJiG1w8;yE|tpsl# z1m<$sQ+sswmNDOnbqUwJOW+I)cHcv6&l)OWH$@sYqVop`C^;w90-`)39edve{l0Vtb@22m z%b4?0L8QMCJv4-icIKmUsG;@|len`WyW7Ej%l<@WV9FpuCa5UkLb{M_3`=Jf0DV12 z9PrS0|H1wlsQ2f&zLN2vEhYXRZb{DFAXCXfEG_p4Oo87H-vU@HnsUSbcrnsEc>?tpQ^p? zT?6$7zkZ<{Jn@d8?SR|C`7B^as5${p9OJ9XCh1$JjEjGbf#7w*T~&f-N@>m!yvRe` zbz$kSuukz#6hI#a))g8$F<-PE+VF?7hG){W?@ZeSZPTR~1p)=?lVs5VeOs}!|u z`ZSZfeYS1|i6>H-Aefj@noj1niha>$ECQJt+@jqa$ZdQI4v+_HPEAU(z`CG%%^jEt z@r!oLd);DA*W^&l5_n_dW}H*k(fvj_nC%47rrpy=G6uUjN7~vQ@7mttdW+?};-cDy z85-bS_8rt z?gHiVj$$l0er#W*Tk^e+`Oc}W)0>pZSe7(Jf<&_H)vb*S%1Tha$DCLHDKa)yB z6aXp+bi(o*(sT41lNCN2qgfXap5U>wcdZ^fx`9Tc-rhx(RF2AQS|Sjas?S8kP(mwk zTJe{2Ru$*!bn2zY3-g|Mfp|kG&Z|(vkQy&xc=w$JS8lAZh`9yMNz6&AN_N83IYQi+ zgyxe24I1^Z4M4g;N09VS@8VXM#!^GzAA}j0WZgiG6v%z=a;1>nSec%UZ=${uxlB)k zf`VwCsg`P8z@Cdp+3EJ3=TAXo)Pz^-*84|z0{Vb#Yx6N_J4%1b?*7h&4uko zQmM!O&R;vypi6+TlD0{83^QgY=YuVc?UzzVr~;HLLm*)S=g) z)LJ1|piJabYBCn3!(y$Al38Lb%PV-8yV}YOx62n%nUy{eyQ0NjAC)t#%;ZboO8@3~ zUN!9@`ou}hSa=*~0a z%+B0mN?1&flJj6&23u)dU&wAdc_Ye$Dp}~|uHh#W*SmUxxqjGp>`UzQX063K8_=rG ze@!i+)CJ(N70?u`%(0?(31$&lp!{oAiDaDJ7DUFqPjf|r6GmE5bMX)ic=zqJx*R>k zS3J7;T$*#lpfN)H)T8Ok*HM3#83^KNRA?Qq4y?k?go>7va0&OeD)AmkK7A^$b`D0w zo8r5|Vv8D^eys)1GY&|vf8Em?v<@a1FIqGqTJWcQqX&6vmqtgyNrGgPohA>qj`6<> zmV4q(mF7TlmxL!ikKe~tN~Jd*rF(pBKs-wFJxr3nVeg%V_(CYk`tK@4kW^)Rl$;q9 zgu4a1M3~6L(z>GRXsJT!3CoK5Fcd%TwF1U99Iv84^ERWcdX4t!8GqMMY4+mvEN#$S zyOhK`-fQ)n(m0Nlb(i$8_s!+>^T_||F2*-&Pa@6I8a4Y!`8NZ}mGa)A3VvDxG3#%)nvB zfGMUUJ&lg#KtE1sDz~Pqwdqa5#mVouGIp9~h&c(nY`A$s&lNR?Knqb+j8g&3suJIC zH-iTZzBa&*Kmo%T2J_RF^L%>qZBob%o3e48|EVsc7jqefe1@MDHexshk5LXNW4uDW zY(pQ@7gTi^&h*iGl@PzB za8Xf}SVn+uli#bM^i^YE3-#@$j+VUnXY_QJh$8Ibi7NSZjXrJg1b{(xpK>iux6sap zu8D7kD9}CV|pI^0gmIfIwj&z(g zE@nh##=X~z88q!)S$dXTIpnzS2atJg^L3LcS8c~4xqLB6%noYW{Gwf-FG<0q_9PlF z4}-vKFrOC2V_hpJOq2tP{V|f^blQLcs-G2Koy9;q0QgD6sE3t-#UOP1s>JWXLSr|v z#H09Pz$v)p-@o&^$4k9|`i8OJJZ~9;w&{8iC+4qZ^Lb7kyh=7w9x#~eV`_R8@5ccK zRopuDOih5SU)cph^;>uiXsVYByd8?pEYd79oD9viaaRI_Gxsg~k-Cmkr$Hf}75SEa zHuy-{SBF5assQ+nUfMg9Qkz}7a38xUkq|iKYh;%Q^%kOAgYoIGm6})K?V`pYeH~xbIfz-TSS&e^kKl9u2GZ1vuhp z&E|B4-Y^PS(G}e^d1_n^qk;l<{I4T~k_*6*K;W7(0bvLmNEK;)wT)i<-AP+*70h6% zD4iEWYTU-_;yAcG;a}MV^b8 z|4r=ue&aS|nz{*VbDHqgRTE58~a6Gmn*Cd>GpdW(S4 zx?$T)8Eelh&juDZ(hOr1%WSZs@;zPtstR@dikMH*6}3hS*IKm{*u|4IpR@812FiH8 z0ZSYRbL*N2 z#)6R`8B7~ZWN>ni`mjxs_u_JTTE*!1#{-2pa;kp1NNw84a50^vYRbyd|w zB~415RNgU71cctE%5cp#*k12@|0%Q}(hrt+>0sB(Y=s0(naBqpPNT&a!4+Zx*s=2#d-Gt>QOBUMA#?@2P7YipPz zdM+d@>FHNzuTwdY@g`jZjdIV*Enu5er#PBjDp`%lXY2ZAcdudhgOf+{?5f%&vZ)%G z2>OX3sEje#0QuBHyvO!!#<&I2d%q;-g-wGh!G%R*-!2@&r#tQk_U8el!!soCQPQ1AxNGsV z0m=K;V5!9T7nz)U3Zv`vJhnwP9r*wZ5t@fQB6^5LdIuex)}j?h;Q-N>_y^m;8e(HK zp=xK!y97;mkyn`|DkZVr++`;*gxEidH6j6HN&V@X7(RGrrd6l%fHcP5eOkD%29T=j zXe$kyQqATQlo@?lAx?u6*^J3N^cVg-nT+$?gX?QR(tfK8FCN@oB$X+1@_`SLV&y3p$6!3L7{DnltlD!M=H z4D_Md<%64_Q9-*>kCeB59k)dZU}r(lDFl6u_U?1YUc`?vNw{=>B)4sSnw)s_3i`a# z$ewoezzGOWyTq;R@IOu7a;yHM8y8_L#>re-upuD$OxD^mXyC{>4=hiC<;Pbhwy>~v zm9+QR!>YH21#u-i$pQq*r6mE7Q$9%Cw1HPyM6yS|mYKgUM#-27i zS1M=5V#&e>Fy5qquHUk`S2VbQ4DW)>S#wfN^c+gHQA=fiR?G$ zcAx4uzaP0sm^5FF94JgpbAnY=s^M3{=00qEH)A>^Vm1V3xC^>U*$>5O+x|8C%F1&O zwk=Yh3Fp>0WY&%9zzdr@wG(C55=?bDtkmHR!j+`_GrI7Q?xv%_w4{pK(fCEnv;Euoo~(w^;>wQm%U}W5`FyR>KqT=lQQF! zeOuCrc$9ku7DN9Y(#7VYyZ@ibeG_S5BvJ1BFMn1SI}+dW0XZn6v>_6Wdfnj)BbY|? zNBW$Kzlm;t><)^9)>l*l`O{uhCs_KEg~&5r!rVULHcGCORUfuPH7=#0TQljHeYK;` zxfs*Szx;=Q{vA2I2Cgw*3|4eUGFk4%0yI`>UTZW@9}*Mt8v!|aB(BO5Zj9(&O*tJ@ z)dUTY2%*`tN@H&?Cg>}Vs`@5AqJdS0RHB3mYD96PiW7+KP+oBmxp%mYC;7V=W^t?BwJRcLj2cT_64;HfGC092!`fd9WX zKv`vh71%~Gz|_HQt*N>Amf6n;4noiV46PdS<>i`h&Nm^s=Jrl1#q!7KV3MjPC+gnk* zGQBbprdlJoy-)eCxwz&RtXp}YE^RI)|L+|py`Ey&Tp^Y6oFOdi{XTQxRlj{pA(dxp zXRCfAc#I`A0iPA1EDUUR(pLzi|I08pm7rl707?trr^9uyI-T)Dvsn#fcPS7iUuweAi)zCqtK{!K9U?d1MBxeS&m5XvAAp<|cqwIz$-@v~sT^_&S%sj!rybzIn_uAvY z8DutC7WLj0ENprg)<(35mc!&Ad@fm(HGP>XT3>c|d7zi233~fyy$189x@-oGXXtKT zyg7S>NDx!VMs=51>KOOiyhpdv%g=31lcjSE(mD=fjs!5TU}`kRcYzqGVBI+QBl3J` z$kyo>^5#7I(?~;2RcJmJHZ*saw|Cj?lVQ@MF=NS}vu-Q8jlCjvWFoat4{tJs-;eru z!BV4qWBDYIPONstZ1g`ny9qt<)oPEgpWT_n_irX=di==Up z_Tgi%(uF&rE4LBR$QYgxTu%jq5W2Y3%~%BVr;aB9H6kMD0EH*Dyy~87CEp8 z)I)Y9AB@d=mU>PGakUZ!HdJ6@KzIX5Z$@AbtZjiwnPQCN++-xigxEJWUu%smY>V?D zj`n7V=*26(aYepB%AN&1s+nMY#!LjBl{}g*B{A)eK>Py(cdEwj^ziAZ8Hz`V+}(=zZH2+Umh(d91*`8T(7|#2vgH9QN&8y=K$=bSfIQgr))3-|qb-^3wATFGyA9*U9Z6jNt5o7G0(pi=`U>#NH$aHNSO9O{2kFH)@ z9HllpNA}AoF+YAT9C8jdv_N{yl4U|Yn|p+JS7KwX2dA=uzj5y%@z=JQq%M>gN>z8f z#U=Y+%=4}&epT4Pqs{a{VSW57qH@Z_k)2<%P`gNPff+Bd<$`!^XXRAwNKV?>`hJb) zBr6D}F4{QJ@)r|kU5if_n&=tzqi9+Rv}j$#a3F+tEJ3Ky8)HHJKK**#vmcCRXNl@_ z#6T)S$9e3rfW!jIw5sokcGxgBDuAGwAhH-Tak#}Pd)ViELH*jx@Z-4~dqlnFX!EO{NKGj7w*6v2pH! z(`Z=UA~@bWqXa3d>*6d)tShMnylzJ#TtouJyd z-OA!i9~nddvZtqK+BFG$x~d$WK0p#4h~j`gBn1oRrhjc{XD`j!^L*aR>$7*9O|iYQ zv1FZIsE?ktG78$LT9jRS8sSQL;WukJLRIPOJ(IXxRU^o#Brz#aacmB=Q1d4tjQz~e zvdCNC1q~}$84u{lkI$O^rlQHU7NXrqxF>{paJoZGJWU4pNpnu{E0o^Ym9Jcz7Xtat z6R=I6X~}WJPvl4x^T+46tzVv!8CV}Q$P-yJ0ziZ{nIh`Mf21Xt$^LMY%pv3ZV2?!8 zviDc>vZMgBblHFthC3+wxOm|L(c#>VQ=#P#$b06YRko6 zw9DhtmoFFJ=#XgiTN61$XGGBvX^s$``$MinPt!DrjtjVbyJ0i?OY(k%3t@1hejLV7 z8b)t_nnqFk@Z)-|NFp=$S4@~inIl~>whhZ^+-M~q71yFFlh=7{Wt5BvbS*gq|NDYS zxZvByWSltvy{>x8#Lwenr2lPu;tiye@VD9j{0KRBl_-wVZ@FZ>@&98DqM6v*#uPj6? zl-zsYzfb%#o6n#fBw}3tclt|XsuT)#FjvaD?`le)hGfvYBW{n@<2CN{i^ODN^Sdw- zT|ZkpG(L+r2HB@bS__yQQbsiNgpL;j6TM;bzx<4>c7X`;*h; z@3PMm#mCxW-pU1YP2ZtemBVcZyAjrA{@P8GjG;6ow_d}F^yUg@dX2gBS(Qa(9w z!Z>fnIENM)657;}yx9?g1mGTr9VNRa+Rh0iTk_$gt+3P2~M%VKK7c zA5e42(ZE~+bU#k9?$5R4nJU?Ykhh@3sL}^))y=8+#QQkg_t5)T@35AJRgeJB95jjfUky-p$*FXYUbcR~-Dp8u?IEFL7Gw*w zLcKU|C>YS?+5&6x=eQAAGH}qdoPY4-)%hc{AV&S9FSl zMc8K{m*?M{d1zid+Vv%=9o&LJB9{|mRRaq5KT7)ty1b(QAIy3Qa(iRvvx3*n8D zT>q6(_oA^v1>TBF*>Eh77b8(#ubqK$f}5mIq8rGlJn@lgYUY3j)3dG=TA?3TV~3?swI`G^LjFhNzm`-xw$a=;p{o%N+!^NWhN%1k*s^~ z$?x4c!pJGO*T+4t^6-;$ina)Si%8p9VSMwQK^CHYFdCCH_J|}k*Q&NrK~1|%+%i*o5u?B3%L7hT4=*9ag`q4wPTgoj9sUV- zC{y#?OW7pL7GQr_8Z*UJ_*Go&h0RHu_2KDn*Y#e{VC6wSPfG??ZRYE1T;D%+XEJkcGRj_=|HL>Z0f8ZsLx=r! zE?loLEi)Er_{O2h@sSw8_w^tOvTgjcKv1<%b6T%^iRPx62R{GoQ|x<;JC|-i-tUO` zNfk5G+~hwAwJl+UZh;iVhV^4w7tYTwr}n)qRyP(QNnEsVnYEEyr+ zQTuwpM~|r@*gJZiljTG*vB^fBLbF z=nj#-xu5LoFhZ*8i8IBXtX~n{g#+MWBoE3m6fn`rvo4`PQf|;jit|MpcJf70u7)2z z2~C9g`qlC=0$|zkAZvl8!&@lEWtJkn3sgTG_aO3nCNzTqWQ-OQdH{@*19l`$Hgr>` za)R1J0VMx71$LVQN~TzwIl*7p=PRrExzb*Ir5j8s3E{4BM5R{MvOyvX|9bjHa&=D8ZEvGemKQZ>S>oB$C{hi8I1H3+vL(K$fHs+n z!v0YWW1CM|Q(7FnVfTMZwClK8nSAOh_pHe)0@_iH{84Jit2cyEvZAxj3QUcEtR>uJ zVVGwmx^ZT<5&la$&8p}hQ2GI-eV=4qMmkI)%I(XzQk-cy1d(J_jrIX+S!h@>A_RLd zbt7jD7nwUNpDP%YjoLZY@iMiiS}$MDXXhF&V6W_pqW|(4uzhp}=MH=VInk+7?%DD zn=G@#`BNCXt04wzO4&R9@}fixVtpAMfT~%bEUG-B1HzoP z_j@a3lCEm;ks`g8gdeo-67r^%is4l*AAgf4P#Wih7@P$?ywv_Q@StAZW_hxj8>bGG z9vH#DUh4HeFtznZc3V5{_*1e}NpO%@$2ozN&b($(L?1h&$|kI|3l& z%q91SL;_DKDB8Z_p+Sr0K@#F2#42DmGV>?%;m84S4%9d2NQD^oLezppv0&GHd!avA zc+Ni^Ul)Y7fDRnnaiZdkbFc9JOdAC<1LNYI!mZ=NR*zR>dpVL-IMr}RF-&1|F9c|wf zE-)_+e^AWhr*6IRwc5$ys<8owmi?Af@MwlWHF=k>lYk>pfq_Zk6zGZ(?`o(fODLI7 zWkhhqb5II+t2lFez<*bfcrpiNR1(*qISLuE04d@b6CXs z#+SC~Y&Y+3FRvdknassX&|O(1B7( zH?A!*v(<*o`BjukH8(b4%5ZsVe=2(3hfCiPAw@rNykz+)?9{J%$GMF?>XT#uZhw$W z9c*dG#?0*pEFWFsm`J{j!@nnZx#rJE0BVwUOLIxP7^R-BP82 za;~}^g~$OZnsx58+#Vq!c6;b6(1UqU2*Kf`>sZ(D;&4}^MrT@`J}13-;MF{Y>d6{r zARE{>iDv97$tj?!jJyFjU|#51Ha?nKZ-;HFjoavm}25=kE^*;uO@ z_#~6OF0D2yEONBSp9g=AHHFdlsV)^~^=ks8bVMVRU>G3;zU@`H$O=6<-W)6GyQ|b$ z`#VUSO=13E7E)@bnk#5I)3`%<13LrKrU@(PXPi=(dF1M*q*6Mi>?UZka>n5XorhPH z)ELln%3+*1Jzm3dHYnfd2|*;IW;gk&V!}be;5c@OR(d?;=Bk22MNd8n)adO)hX-3A zxLInE1MJ8)j=4$JVhj@x`c<&>4|Zh1n+$!#_ecDAkTgC<>ArIrlKUhupeXl28V<;Y zBJ_j@@T6lx4#(gbl4nNm5fuZcX~>hRUI2Q7-N^$joVuL(EmFO zo7V$Yb=3vQJLbD8EaYfMK9yfA16l`;ot(7S235ZTuNf@4IunLEeM3^wD_}@Bz*LwL z;bT_vBT3kWT1E)cR9Bkwg9se{%>tQ1(rt4g;pD8>ww#IIOIo=a$Yuw+Kx}lDdo}`E z)0IG>qx8(U0Di_^F=wqpcxXUH$pw2xE6lSD7~+|}ntN?ciA=<=q?E<|N==a6{-D0G z13FTAbS+8~zu#_*#gfA|q^DlJR5A;Y6>Y)iYzItH0^b4{0f8E(p*LCUzsT1HHGo2# zm`Z0z0?1BJ>FQVSz;P#j!Rr^Q5|=aj_Qn9+2XCnzv#rvbgT)XFBNy5IbdchCrm6G* z(@?$LKw5`}X11e=sEwFDxS~{Qk|phP!pZ;>q0|rRPaHD%$XTbK=qo)?(cwvWOYII< zQ!?9he#?sx7#y4Y(-kce~XxHl}2($?&iTA)y$iJJz04v~1knFu63m zgOHsDqC(iKt$@nit;%Qt!CeQ2Bwo2;szd8Fro*ruBOw}+E-f~`KMwd=WPMCsm+$xL zDBGKG7LjMPC9KLBBsno80b$rT^%H&Uwi?R7*v0y4SE*bU@ku(Y72p~FaM4$%!I;oV zK^it1oDnYb6&2ZNKz3AnDYysvknSUF^90|Hn<@LsX6*f(YF9799xZE+)0`?G`%weH zBfU-hlo-n=)3dL(0gzlgEg6EzyjvYY?{|yRzt{g2I<5gT!^g{}It_NR!Hrce+r-oBBQ?{{g`E%E}3Y#^A_{}&SJ?yQ;;r=<;KjRLb#eV{+f_tUOU zt&;SGKr1?sJM^s-o)_>Pz+BqxE-yBNC)Cy63BW` zrhXFe3==voT^&jOeuRYU{uTw9CT?%LTP{b-xvuD9vBPEdh#@X}+%q&BnP_4=fa74n zKbd+FV3=OD?K)+2*woCc)x^|s1E1zgMJ!9`##<{NFXlQjQ#U3L%QhJ=zJ?AeoZaFl z(MM0@bP|(izXaA29AjCPK|G`sA&mnU{ zYK4_2uw`sPzg>PjSG>Dcf^tVLUw##s>4^*34>EP3cm@}5O z`V&uz67I~dFW5(ZS%Bf6GTQ4y#dqgfgiYK|^26>*aG-&k`lQ}Il|Wi+eE|!?%9xUo zZZ&_Ko&K)H=e{1-Xs(v{yyxcX0fyvKA3T`pY`?g(SnY3H_=C|h?iy*N__zclxka(H zS|(zri^E5G#6*7_7|8vkuPmd5PsPd;vrvfNQ^Q>6x-dywH6WfHgF9fk(DiS^LkL`FJ@?bJiO zTQ#;1Q2!)O`Sa#NXglV!arcS$vi$0@KHgDMqMg2eZhabyL@hS;G9NI{)dx0?B1@eJ zDZ19S48_&cW=MOq_>;q|B(@cOT-ZZ@$KS|(y_QZf-n4st!N)pg`j7uA+#4RIrza&g zZEcRq`XF=4+l*f(si2z4`WhC2;rpxX=aDb1H0^k?xxU*X zP<13EY1^Ap+wbw1pAB%(_CAhOWE8m$wo!ZRK`;1UzMQ49ijq%HBl<}?ym)W)x_XBo zRrE9bWqa`sU&zD#uO+fW**W5xXB(<|KiJ9W?F^_?F~0orkU8II~NQQ~oy5#2ecezeixp2Yk5$-2@pP)LU zgb7EbG@RlF(jik%+J7Eu>iNLubt{!-WV-CsI@keh`Z>9Lr;$%0asNc6cG?L1eUYx1y7Low z9@REp?NLszH(=`GpCANzP&i@D!RX(9*v_ zZx~o9n!?Rrz5%Z}Q~Tobiyw=T%bQ0WsD4`~!dJfSuj(IpGZjVGaH&fTc4k^#02x$P zZ@tHMdqrjJ?&*Tc$GnI6*+~DZA#gt@?90>OG4=*uDl(+d3TZ>d5({fv6pJfA?t!C- z+b!)2wQdAiou~qc>tH8=%TE4Fd1^MVg|2D13djj`Pbpl7@L&mkg*R?=Mh3&^)tIFK z?f}|SmUAXERw5WH>{%|#OpkG7$SfigzM^UdEa|9gsU*IDI65Qw%Y6HZ;0;9V*AJXc zU`1IM`Us*kpj2nRwe(O6L4|@#qowd15v%t1xXrvBl3&#l9r2w%Ud6~4!9+eNJ8p`W zLtA%4qFfXc*tK3Iphwye{TM#j0NZgZSfd^l7m|sPBC_lQ#LNc+71OxrgS!dyo8k=} z%z@;nE4WHIe(dKH60FIHqgGq&%{(3JKIKdRex)06CQyUQQI&q*Zc%91^Y9U!-raE$ zpwu=y=rz0cCBQhFxHbom@dMBK)iqt_&RW+$m;%VwC*aP8XA8}|Xm_mS$iR`h!HvwEy4fAmck_gvn}~S~V;f9GqW~;K+~nq1*otVwLtZBJf{eUO>}E4jF9(xv6&=qet7ZGOo%}7xt0OoAtBy@ zMNEZD#B&3GYM>U$$aQ}@%Zg{n*wgQNj{F{yvfgZE0U88D09tUC1I~`9c%x`Tm%fVU zY?LizjdLOg>mFRc-m^6J7GD4%7x~Ul?`*!yfEum1R?#`Bz_9+LBEYzD=_pQ-EsEWz zn3SUkEi)a2pd8rYk5}#A6VsNzD{QE%H-)AuQ5b@PxU+t^34S_?qaYzY=1 z(5N=47r36DdL-IjUTmvkHIppHCGpNdC#nA*yAwrcUT1J$?t)5`d>EY31B31E_z72| zXBIt~n(gkm1;2*n+b-;0&X=->!Ok3a|BAXgT1Y%+%|z1;@1!Co5ZZ8G7QdjL}<1%^+Rtg8kaJhBKI0$E@|i zPV#Qj4P*l@Xcq|c+g6N6<_>5C*|T+h(p{MCJ=t)4s5nd;)^6j)-X=~WI61Z{(85~n zOH6a!K=gWXk2gwz$GG=)A37ZBHMN{xiM#tKt`t)yof4m_k(U=bv5ej0105F1A#X~d zB2oiiepuK;NRybY3|l(Fhsil;t-PNO^uC6O!YNE(4*R)RTCo7=7bFK`z$3YBA|m=v ze5KnFXO_~qkcVP0g;3`J4P|@b;{&L{Re>xSwop@JoJe0%ijcss=QE+jD|DV;vQO|vNYObzX0|S0i?wGbwAnPP^X?2}KvJBOgwxUYc@GisR(zPIK!PK} zNw*@=q8)3mAYLB%RRCVga2RLqe58=8p_9J}OX8cYA>o%iydvgd1Xl1<3_MgQ&;@BrPrS|b=+SA! zSGUgO6Z3D3;KDc=DZJxehfcVe*trWhTBV_&=B;!lj>57tw%W>qR5rJGA;vk?y@By7 zHGTp%#{Al^jei(gg-Yn2$X|6+D-4GVN!{;2y*GxpSqBNoqSO585Ok>z4M4`Q_XlnfhBgJKjcP%s52MT-uVAR`yb$hg7i z2m3AaZY#PuW^brI=Xjh(s~GV+TX!o8$WFj+oNYOi#0E~R)?EMj{~ z!tLgN^Dpy*hEtt9VX6<2on_Ojd%j}TBx=;YG-~N8p`Iq+550&wI3DP zRou3WC-38%9EQ9h(VW{WiOg;(V{Au2nZ8FC|Dl;hpb<}Lb*Vf)zIWS1K*THb=8nQ0z}4WfRVMl}qlMe>2?=SPZzX?m;j8S8 z(1(50+Wa9%VcOi*3N%`nv7zgwDW*dYeU^8nFnAYqu?jP`Y&+oSy*5j1nCSg7FvQiE ztK$bkZVOcWwj+)S)clsZ(RGqVtz2^kXdY3dbI3E-!cv#-c*?0h1$WQulp1EcFG>2@_SWsPH2EpTfe%*XC8VQ10*V=hWG9ivv^eM{JJrmGBiVHg5! z#`pmISp83nur?9I22YEBuN=yth_8v0S4g<^CtC0Oi{oG7TK8g5jgKrO6KdK#QvF98 z?dNU4$mF)$km;azoBTak*6p!CmpWg-yQ`R3HZkbxF^nk&t~-1kd&Wrmy}YHhd~0EzNp3{c(kpS2bi9+ovIFvc3?+0_9*>cb0I|TTs zivn=*{AG%ob86)78_fTHQ2@h>`P>`$%^Y>5!{B3b;88oy(vz8aaGcJOMnz$K={}*u zjTmF-R*o-+0FQAd7xAF*=+rJXp_I~D3Hv0p=dp%dk0-6ARZ3rP<>>Dt$UIc?25hWg z8l+!`vfsiVHBmB?^27iZlqaN%>+9{lG#N6jfqMa zayVo!iYf-1X>qZI#yaYuD`%d74pVy}Ghp>YG>ZafXLBAI{(hqmR2va4=|b){IE^&C z#Q=0lxDjo^)6ByL^ch#e7Q)Mc&K8L{so ziWqEmW>Q%mMFIc#jba2%)@ripJ?e!BmyS2W!&h6Gzw9{d!?ig}TFkEOc$XfF4={HC z2q>l58sN#n;PYLb0hTDhL&R$pbdn{$IRsm74U(MEbnllX+ZarE+@{_d+ zUe{uXCz|(URneP>#-$dB=1wW3N{rM+xF)3vKj+xYFBaKPYIOakr6upE62Q(eg&fvP zmJD@7pN}>tg>ql&{f_hmDN-3ZT>9no7OWY-q`t4cZ8Wr}gX-|Uh=)8#ad2PkSFHmqP6;b30N&)+o_nHI7)!mi2Hq;xB-+$=6Sj{wP>2>u|jqGe70ksS58kg;!g_Hbuy7I9ugmC; z3XiZ@u{s{GE1?uRg;XW-EoO-VNtb>hgrRRAt3 z`sR0Ljp!2P7=!hdb9I7}gbdS!5ZXI_#-&_k9<4rQ;z3TCk|QxzY_CG|$ zZqk`2T=gzil3@KKs4@Wa{RmZn#jEn!UA2j`!;;JpO1C;9Aq{m>+v`dO$G7>K<}iW> zO=AY>=d9#rdVm$`ZI79>-P(qhB|=hG^Dtg^tv>Q_3MZ0(3X&EA4pxd*;ipo)GzN+X zxmPJ~sTU@q#tZ$5VhErRrPwB1z#P=XR?#dl2yGtuMdt( zl~q%fIu?w0RF?!=9nVTKf(xw>CsJ$sDz?7vZ@VX_!E@W>yhCs~h(gjAljcj_VWdm^ z;&vS0K-seFYmR`5JH4gylROLv@cdpvRNAG_c=Dm1>$oJ;Na}%kDlM%iwq{fo=cJWQ z<#lfly$nZZpaG-_BY@LRR&#|gjZB3|c^OZcJu=1T`q)^R(EN;IS-l3q7y>i^Gi3I^ zIOhxgz*=-IE5`&PvC@!05;THz3bO_e@aR<2cA==K^ug$cpSn>{2C2a%Jsd{3QjhLq zQX9@SjiklODRoYjeG|5SeTq3+U%WmT4~M+&f%9>}<4WI6Jijv?Bf)5~neW)$Uvs*{ z)Zj+m5JpyRbxY6uL*^9hPJ}5kLAw?R=6cnZ!rsu6c(G-eRK;`qh7HM=g-U~w>Jvz4 zT71%fgt^*I{u}nv4r7{#R?!0pLJGp!J|r%qLx%S%pv^`~XfGj#p;&w8K$VmO0pOOK zDe0TWjWhue=t!sJlm4vBb`4jRWGpT@!%s75f4|z0Uf=KCPSn~w3L7cbxLgBD)fT3S z13N;3gwZ`?N2Bc8ga_@$CYG*FsqH-2)m7BsCRLW#oT*?%G4|Xv$O*IU9RJVeZrm`x zL{9*AA4GS4GbhA*WFw(9Baed>&h|_(ZzTf{NABLtBpiM=6$ZeMBmeMDlJaH~a9VG7 zhGlHo;;E>~vyPe=NZ{B8Juy-4{kyMBV(R%Bc*_NV{hN4q``+8{jZY?H-f7=qHH+Gs z5?7LF`)73n*SjxWoH`j90`xynWgzHj{1^N4gj?-@sCB11?sI`Q;155V33KK1*VKv8r7nE?bq*&V7WS#9la#%;A z3j7f6_4Sh7$;!7RXkg|cewf< z7^WnJ7~CCU#Tmd*YNb;DKe*=5D|x_fc;TT#nV24Z@7UZ`gffn=mzW2R`hb?a>uko^ zwJ~gm9=C@__R)znmvxxL%&br+Xa_f`+nwdGvKmv!gw+G6tq8uiTSabfTjr$rk%Eh?Tb`P{+0sIZ?VOjrtsn(_nT&p@Avj9_50(6X&xv6J z_EK)5t80`HLW4VKAA@OQkPkGCRVCkEu(J5jw|T}4`d^Qx$ zp6s4&(lc*RmDM%%TKmJhhvv?9W(6J5&7iyn0c+>;W$PcrqB?vp>zT6=G6UeQU4rKf zn2p7t6ff_x_9QK{Ad{i<8Rwu^L^bgGWeMk{eO8bF+Qk6x#vAi;tL4k%DwWd8#VF}+ zRAc|SC^=aGa4fo=nKzhkm~3BzonCXE=w*;%NITJHJI9Yv%&fPgqI^LtJCxmps^u`E zW{uBaiQt5zv*9EFGTup$eigH_>g#n@WXoFXqYc5EoeAi1p5^o0w8?QynJMq_C{dlevc4rRm#!)I)vo<}geURNXye?97Lo|H0R#?63 z4Lwo+UMa%Bl#y~hrDM9A1-UKQ_A!4ySGV2f*cXBDTio4GjeQ&AzrZ=+kspfD>OJ%E~ZmNlCA^rr?nBkYORw?|*1s6@{ zL<4Fe-HTrDt1$6+kzT0}X4zyL8-f!2AYFgQiU~0Bdjw zP;;4yEye`ZqMyAZu3%g?*XJvi@0!Jc2@I{{k=n+px6TTN2v`MF3t^J4;P{rD(To^4 z$6__bl&J}*#}H3MJCnl42tzLx*3J(lyXHaKrBgx@&{iX0_xqYH5rs5d{ky> ztkvaP_hpuThp`R=z4fW|yf{+}OhHA;eer9485U6>RI0^~mx!YeN6}k}JyfUoJBe>Z~miOPnJI9Va$x6%cC!n2sA?LF8}zcHyI6Kk1yEtGm10)u0+ zyl4vNoEj33T~5tSqOSPz+{C7sjdzC$$H({OZ!Lo~Z5iVRgsUyaI&Ked0W+2Gm_uaS z9i**k3D%~1`4^k+BB#RF=5>>Cmm{s^i0y){BtP6oBy;KZQyn*UoGts0{H0&Se_1x`PW#vBc}hxrDeu1JaizEOvp43Mu595Q zJvIXAQ+lw-qKXsxX~4?L0h;Q3(3OOwi9U0oG|+=+;T`2k?9Ub0B}bc4&MhM>Ut&<5 z%zs9^(>5nrPtE{yP|mV2-e#j+UC_=IYOk-=*4hWE=!4Hy6X5Z9O=tJ{)MQlNx_=on zivLu$l}vHXBOU+Ah^-cwlv|JYa`Z}Mh?>>RPlKlCXmH^9!yv+Et~>FL}Hw=w3x3$1b{PwVp* z$YO1rhnI~I9RrfiJZlY03i)>_W+XTQo&fg>8J>osMG{L{VyIdi zjaoay0BwQDg_6ToD=!$$OPICkVn1T!*Vh3lbF^oMMrJP_>46Um7*0v#{qi=oM6iyG z0VDnnbu_=CIkG~=Gr{tBr=j$WZTC)*R!%6r#0tF`rz^kWk1O3)-ID|7**if#+Di=g zjE!VCeJw7aT_q0b=5#FcR+E(pX-pQ5vo|>m2^8>EvbvV=OzJc? znH{cj+BPxKhomO8_#IWtif`SsQSB= zE`t;ZENjpH7}|mLy`KAuwBIlp2&QK#`AKb#euE28QNIDcD@}!S3BEBag+&O9+r{yt zoD%XE)Ahv|huK}bXrTd0xywz-lOQ~Cftnaf6hs2lyEgh0e~fWLAeAe;4UR(@{%IZu zb>Ii4RpWtZQAIG|8QaY4%|cDO9Fu%`V%@LGv)ZNm1ia=08p62r zZDkE)w_^RL_vcN)MajkIw}jk@1b=Vo?|$4utT2wywm+?mF;s8b{3c;qha~+GB+wn@ z-=!EUbp$pyfX`QUW}^E2+$IbHj&T{wMmV_jBvFw-Zd8M6O}AUB{3j~k6g=*sj*vSH zlGP-UJCYfiJ6jDP1k54k5i*XS4Ulp)FVc>uL(+X=lMk5O1vLrX`v6G!>>eFG#{zqX zXpA=aR@6ks-9_uM2s{?=uLf#N*6wQ;tSP_s5oV z41Lv58A6=8Fw=Do)X+qLirHLVgE+2RgO@-Z;Clh9(W-F{>A)Yz%p8J6A*>fG?F7#~ zdJ;5q$e?E*(Guw+i4F{O#LF4>Z&2O4!@*12U`_Pri!~{so1o7ak1J618c+nYUNaGb z*YC7ujwB2Q<05q_jTlhkl4XFGizqwjBb$2N%PrqRhSrq&RIUF`W@WeWBh<1M(uIKF z>g{%JAu}u-Zg~&;r*T7sNX>HVC_Z3rdI92YBN#UDx3jao+sPjl4eUHob za55mE+g<7Yqr#XnX;by^;qYREFDbqWb5EY_Gd5%l4+fD~mNg`uCe-WO6}i^LH_vWP zB^>*nS${eusXgQv-|!<*uw?sAMSmE*?0qyD{H_n=ift8yVbsi6n46%Lj030@`d=08 zG@msnVw^S!^3vkr;BThV{4aU$VRWt&H&SL)cWWx+S40anEJjhNF<&jBOL`qN1xYsy z0By+>sqnb^O3PiL7u;m88d4akG{yxed^t@ek4%j*(mkJMK44?E$8lPj`-WyG=kyai zbq8u+(SnqFB(qZF?3FJ&C1&3(+_Em6W2UE+X&2jSKp0jBuCzl$UyZ$$H_cRhK`-Gg*ww8&{&W9fSLlw1z@ zw9wJQz__Fv=FJh#a>}#|utb4>uoihPwCCbYSp+&HZJ(!=`|kA=>D;^QU;5N@q3w}{ zyLs1W<=2uE6(hqsXZSfgX11WBDZ>H zYW+KgrS6sHm}ur5;%a89BooM&z^T(^4A3(8FQ&+ZRDr`ftS>i>!1R!I*kchMF35U~ zc)gk<2UYymyxx~`o^0IaZ%f%-p&gxk126_^0h`IfjQ)pq`-hk!A=FB^k=^Y9%Qv5} z3Ggi#o)|&eN?=R~*Z`*oww)#{t`26LEq785|SL(X@aNnu=3L7m^^#?YM+R39> zGP;RLi+EX_U*08;=`Jg{D17X(A+C|+s!UtjJ9TV5n^5ReivE+-#Qv8uWW#qqrp=5Q zAXG?%<5jkQLn&l6rDg&MXrw?z-;N_o_Ou4)LfPxD{+BP=ug*er|A-nFpMhvVS?0}W z7`syy-$2);<2c}6Gr1eX+j79%n@tOq(wm`JMQ~3*kJe6K4bzP{Gb+AH!c5Dg6p2b%hUdh3fNiwcNEX*aGd{L`;M#$AqGZYwc-a_G4Gm+Mz?i~~OfmG96lQSoS zkW-C8f*za7ojVQT&mYGL%xk9iPhik^*pte~Kuy9&51;fgH8(LqF-7*6mj5$fC*4}(2<*PnIRF8A@ z*3RwADX2|$h9Ti3ybJI^1%8Sy?BxKCgytAR<=%fMgxM-Lt1E_r&~Sxfe`fZk9d(qJx#$ zfhh5e!*1qI*hF!cn^lwnC`}hj7<`b^diGR|7;QEI?u}?rr7}aGTXAW^$J-oKvn~1n zP6B*`3zOQ_e1VOFogbohEd$E0N?vDFasAlxVULK<63*SJ+huyGYRtUDJi~Uqy2b7;d!Sk8=cPo=b$UfIi)FNB0JklQGdQAOl_Odq1J0u-3gsKQN1Qt{j^6`~O-Ak2+ivs7X!2VT zx>hTd0Y)z=H<{;o!~`>z`oNAi6wz(Sd!EXJN*Wjt?_0KxcD|6~4=`+lv@G?CENJ2j^Ez?vW>wPNOM7 z+}F~G1?0|wHBg;nFC&A272Bd~hqxg)K%@PyM~FN^)Y*RS;rkRN7rUr~11Py-^k2c{ z@|w_}HB3uIgS5*GWG0RTNm0)6u-t#fMH9YEW4DY zn>0<0J4zPehWIJtQ6gT8(G9f^<9}|Ew0o=lSO)92`J|>ZF1v+6eM~u(*?KSwcF37K zN#`Zx3-Jy{l{V8q9D6pbffD^;{Ze1~(BR{QT-~4t6&3UgUZ>1RoZ+|EChX0KvF>eI zkJWKxl)pF}#7N8Kbf*Ux_w@DAaf;7?1v*!IEsyMZ5``QsGMzqNhbTB;{e>;yev$go z=vRA%?z|OC>R8KxJ=$2eFW35r98zLxvBc|IH;@Hv?B9$%8jaG5wu-1SeAHxY-e z(6&df!XaT#r6g`(j=VAr)yy5p41XOusW=*>sQI5gfsr(}YiD7Wl#rkB`>6u2-KX+x zO3lS=1dQvZTlQt|73sOn*eYfaLT#^$ITC!Lk@f|=%AO%x#Um}e>|BnS0pX8iK9@HP z&prK2S&!f}_|^u?jxR`57pdc6AICld11)DoM?t`CY@>5{CsJBO=zYgV&OTUhtYHi) z9veT#zSYA}-41-7#!(+4+d&oepW+)P>bA71X=OS1gRSg+S13m9i)RAmmcYe*fTT`8owcdacq$p&1%3`aaQfGl4#yjgYlih+%1llDi2WasDoIfUz*|YLd5EGB4*M zx#6t%5$1qYCOv4_$Q?z`xoY^e7j9A41oCO2HjT{i3^3g4g0Yj0E7{MjXu#U-n=$r2 zYhl$8f~norvzQweHUQ;h?i8+f(W3)FQ2LIpw9FVf<$KRSm2)o@JyMmUc016Ylkp3k zt^{ieP#QC1Db+f%VDIGruHtYDgLv*c$qB>L7kuAX&3 z;8U^lT-4OAv#%>#r>|wGWh(azA#46pouDb+h7tX%UGH*gmhjl>KZR6TR~x$Be}tH- zsc`K%)LkbAwN8K|@9BPU%L|`_ZF2>o-L&DM9=|oa#%0pu=C9K(3hIKI`|AFRWA!Eq zyz!A~Q<(v@yt;eO_*%Q$nj5ORTq`gF-K@9v@K51XspU3!=a%Kr0Gw@2RWiVN)(Z>S z{}x0LCD_J3RhmbLa+{uBLmerWf~v_MWLsZ0M>Hk03E(z|-rwYH+NeBMuBZMp-0#uS zMyOkt$7k>us;OP*;|TSr8qB5 z^Ck_61*0D;8?yw$KBSOr1&&mCp_)kCHQij|mp^Co5Oovz>J0~VV(nABe%)*!t2#RT z56^X7Op1!rBths+zzNC&%r;z%T(#BIG}@k0u}r&y{(kHPI&lobrFW}(ZY_{v@9#EtZk?_4t7s-vSkf0@>a+GdI- zQm)?8f)r8si_5julfO8&L}$$(@Q~nnRt&<2DW)d8r$tGQ!`RJMpl=WWe=PN}@HLTKcO9l9HvTP z$9XQmO<)(4td9mB?)PC=Dh(7Ue8caQI-f#kFDa=M8uy!xI}C>N=ep2#jC+g&ke^#H zfkJvPTD0fGMpwII+i6hyllY6V!*@@eZi5c75FjIdo9ofNIrer3PO*36w(i%IsB`C6 z#zGp+ewZkS&?|=p!or7KBceT{ZHS6v>W#K|*6gMu+wU|wcWV?^#^=gO1-=pUxa!AM z+4XZJBwX+%)L- z^`10nm9>Y_RfHb97*-u!YBS2TJLwgI8nwBx{`8jjanD}`mJAL7GP0Gl@}O> zvG@!z@1)%-XCcIbSmV%I1W>OCeJQI@XY?tUPYBVh@kgp#Qc|_UgDsE*Z-HO0aA~HVQX%w7H7Hu^ubq9raQHzy9_y4XD5vNLNGMID7q1_R1 z_{a9J4N1PX?7CO^Y#3a%e>jo@2=k3k9TGF8$#`>MzkLV-9&j^~m~AxlVT>QDM{0Ac zWckb>D?h1q*QW}WOLx(C;RDV%q=s75q3OI5#>9ZZHs?iT!{UzY)U!!-;7SjRw^XW; zx_b|j7UG-)Tt9|w8=jRyLc0v9OOnn5reXu61NDjKjaYf*VXSYR@x4tURH>;H_(Z>_ zgBWXZ?L9Mr{}lbHCcY1(qsgs}>Nb1vJ=eLnArph@sKtTrsr*%Slda!%#&u=Nfu?_o^j}jXmf*vO4CC2%=pK6;E z;H+yzlSeh^_b{+CeG{#EM5rA@sH62aB7@NPHld1@?E{R|tOz|@R`ZfYC>KB}G~!jH zVj~3CCA}mWSiGA*C~tQ4p$=Wm5pJ;1h)Awj?$&h+R^RASe4H81Vcvf6Ue%ZtR;M$y zUe-soC>Yjcry&MA@q=J4q10uBTlUfiXQT`rG@7%%AbKKJGG#X)VV`io0>2!EPg2Dz z(0-6j*o8E|$`Kn8a2{HFfNb48w+c*w0RaeFx${868 z+np#q5UFWK8iIhBq>2*=A1lH=KW)uxW$as`zwu`H-GE@@laYxasIj+K7{Qk_T!`YQ ztmpg>>C}Cg%^zfhbv_D7jjkI1gkSfko5x=vgGl@{x}poVDd@VhhZDBaUceVV`jT8_ zbHd-^Y`_R)X1bLh$V#kThe4loZVsvOU6Ap@Nb{Ye5Bx?K9Jexb^%KldI5s!IRDA#c(kybe_%WwE^YQ$kq!30Dy=i<5_y}eh z;`C*!h*?78mW6Msvns342x&&NxsEIDVz20}Lz+xSH|Hq!2y z9$<>L*i2(A)UG%^r{7gw1n6y5uFlfwu74|WHGE+1=XDg<;F>u!EVZYf_52#}^&&KS zvv7g&{fx{%pcn~Ok4-nuYFfi=f{Bntp%9BpTB!VnUIJvi4k;Z!o*h1&BFh+@+g62H z9w*vh=8U&;E;<3n`4wm{3HNbkuTii291+mEN?d-Gn$`BO`+i``X)TkO(S6D>?IpUx z-NPi_pnjd8=wa$<1n{a3yk)I zU^Q=k$E@Qf#Dly0q6W~amE3QGHd%3lp`)h)37d`!fh3vHLKnKZN@zB`Ci-%>E%J#m z^MA+$0UUVCFLO7d*c6$n!KDX-)KT0u6<#QqM1=CYcsVsr!VXnyA>Fg|tlT==wDh3k zu17e9OEtMvpEGWlJ_xoF6BI!sNGC z9BPL0>(&tF*ZS}T2tr6skALcH-DpVx3vto$eg|;xqo2KCcP70@&f@he#LrQ#bDBB5 z@AqAXe&;J^)rDcg-B*$vmv8d-iQBbp%pJ-bzx#q53aUm1_pvtN=uKW?D(oO&%10$D z9h7%&1wj4sWa|fXs|Cs0ex1+WuOErCHFk|A{uhCjkiP&5P}az!6UdK&u!8hOnAi0V zEUGppRECW)fqQTyco=OG&_22K(>Q}{SMXBn>+Lg8g!!)W6A#A6eH4Gk%&z}C-uCw_ z4|7eLa9fTIR%m}#j14kgFa*iBbW2Ff7#KEVQZ0@mObw`7tZ73F#F!o_^-b4L%jxDL zMzH)~Ijy@@@XSeA8RTw0?C{moZa<4xpt|bk?C>g`A%OG4jA6hHz$35EjF%`m*&j4` zrU_*XR3=8xG3?MDBCdEIjC6{1M`W^8ADA*nnuEF<#;xy5kJs%e4T+OdL%Xc41I6IK zv;MJ@9>z87ypfbB<0FUQBQlxe3#b1zD!iLoLx&#+aKR+{yGMimpXwos)})_<6!U+d z!eryejQP0TGTjBIbL@p`rG83V#Zh^g2N2&Y+3LK3gwu?6o%-)l^q=sMH(@mwja`N) zm)R&a9!FUzUA6ri756XnbQf3K!q2%Wl|ojzj*^;Oe9&R;tG8gzr*&byyW81l$#M4T zSG^&=!DhVT{8sPCpW0{{YEd!r-@^%};_*nPxH|Y$ylN_K{?-kvWYifJGI@N41Aa8dv`Iy_#T!myA_0}Js7~smoGm^qfbH^ zE*4<_%DRVna&2*sIiaa2t^N~KfkC=*kbX$B6ASPgDl7_kx)$=($BJ_|Gmubx>bpn< zQzW>hvT=JXf&nmwTmp#I2c!6#mg!2t*kht#Z=Ap1 zaJr+B*|4%AQ>F*vq`sxy~uGl2y4p; zcH_;G1mvv{rXqXy_xPtqGJPYeXsW6igU#8BJWo3~O<rzTTjlHc zrin07E zCS31J&(DY5)?zYNP0}do+sbtMM+xdVZv2|q2uob_S^(Juyp|32&CYk5`{viMIy%m~ z?5RPLtDsXICYvS4ycJ-`^=N}(jYVvVz7(sOe@e2R8IiV%5Q?(#w%eC^9CKeuEVAVR zq_~+0QUV<_yt-6R>;cG3pk3B5GIZ6w4c*rF<{0(5{XEM$sqihYsbjFR@0iGfl=_wT zmNgtfAe#rHF`gj-T?4P=NP{YlrbS-i{$QjnQj=cmp^f({QBbvwX83Ee+^}o1e}Vc! z>5w>xzp`Crf}-Yde~zPSM|yHbw(+o=NT6|!G~cH$u+4K?@p5dozCOY$?sm1z z3fa7L9~Iglr|bW0Rcfq7PPO}4LaeKgP}t(upln!69(&^M(1FQ7^WO#IYU3vjDad#{ zcj=w~yg`nqup=ZvD6cP-u^KJPI%VGu9XCD+6WiucQMURwv^*iBHBDU%tzl2J+vfF4~=ya z=!}*=RyQOqjP#6l8bC34n(a-iP28C)asm;yaZyr;*9X{tH9-TRKVTD-z3yPD5RbSZ zu&dwvd4>iCvC#qfT{jeMQyoVeD6k+d0#u4@!(g~LHtP}M2c@(nNVD#k&?I08z` z6XvLNGVHWhA5ae?jK4s8F$f^lF$1_gL)F~tQ2A0gP=s8Y@*@>qPccK0Mk9&&6amBU zq09ne?G7pM+R>&%m(D^R!=Fg1z679RpXR>$Me)3rAQ9K#VNXvv+S1$(Ozk3A%XMAZ0NIWtat~}0_4^@L+ArNd2%KbK-$t-aZx!P2I3}kS3x1v|xXuDKUs}iu?L*f~VBO^F*1N~qfi{eR_aWGeFNu8W zH;K%m^auTSH06@5BQABF=V-Uf;@fwQ(C|^i6PF2=oK2FE3Mth%(l%;r=#k%6Te|h{ z&Vb;e?)AQ7L0x^5Z+`=Ts)#<%)mq1!LGH-o6dIJTQw7-#O;9URZ7_28r--6Gl&C_` z5z_O!@L_Kw+)Y$?(FKTO5zxW^>a}R(!uQW$XVU<-ZL;kiwtT0K)Uj;=tZY!lWn#uN zY@cyDlS%Sgte;B1ieGJEkPm+w6SnnjxS-aP7w3S-n>J}EXhQ}MF;}Zy1~3o4Hfo@u z`yFL6PJtUlB=~>JNH$ZS*cNbV;5O-0tvU4>M*k#43PuNgmabB7r`Qm)1 z{2J`=H0pcj@gm7H{A=%tmk%(2C0g%TL^7L@7356~Sew!mN-z*~R-vN%@95J<@V2Fa zc&idKOslZct51A@^EG9cB6<3t{a?Y}#12&SYcp}G1P`$r@Stua*d!w zf8CT<(M|qDHHLq^g-|1V9$;$;e*aB8rgeZgW@fizPzJN0nh|eZ+7()VpT|&*7Exy( z1Mlqjf~zU`k|OCzg&4kl7sV%PQhVrJTZ?jZ;d&%(!YWTdH>*vLJ=SP@In)a@gJP6X zZ?pG)fA`8|A{2+$Hhekfwq&S+=j%8-u%Mdx`QJvoX_C%po~?&WwmD_QwG~sxXI~XN zxSX-;FP9=jT=YRDqBIlnOQ$saE_ui>kVP=mjz`B1ft@QfC`!&bVldpH3@FWO(9!?O zC_DxqZYOI!mLu4`}=-Ikat%cUyxP$D?N3`Nx4?f|Z!ZM3!{LydD8LU&P1|1W| ztS}zNWW{64XU59FK2kjgf?F=w!MBrY{S?g(SIRM7L|Si+yxQQr=zhO_Ji7Rs!U^$J zl{-trI3qyX2Uje9{qR(A-wDOJ7lAa3p}%a0sizz0@=}zWsND=)IPIStU3>IVn`}dILM_3 zUle+LT+8o@NsBoHphaFU3k<{sAFd0U@k0drqUw#xbqvGxJ53YOCRCLR1wQQ!vP2ll z`C1ywE3gJ`#_TiK8%FlO2;aag)tSSW2Rb;4wH4~D$U(wQ)k+NM5?InU<~z$tWi!3g$3!p1*mmKpc#enAqoDfSUoN#9OJN<qiHU_-;*phb@Fql5`Fw`4 zp>lh|21YToRm0|}gT{?HEC6LJj4boC1mUD{k^0y@v@afMo^SV6Df?BA7&uNc0^;D` zF`K6_It0T#V6IR|%?+Zdq1E50yJTnaM^NPjWQENkR%J zgoWUE)xvb;Bf9-#EDlPtUfP8(@;X-PJ@S8LsV@!jX7qI{&M0X7E>&kbY8eCn`fj@! zgJseQH<_%%%psT5IDv55ZN4*Sc2Kb6h6i4S*BSxFt0Sw@3SfM6%}(hqw$XK!JmQ64 z7V=QF*Vqde2oVch0eGg|58{*e%cl`mtyX)dFnCFPKN2${$iVv#cm2MOCP`nIMCr=@ zLBKkbmhUmc+6RN2O{F8qvP@sC%OvsE!N(K>>UjS=3QziK?>`b$E1(oN?}WmlCAR=! z80|JQPXZ)Pdgl7_X9oi?3F?i{-1VwMFH@wt7cI1R^tNqgFK#ZbD58zcBANral<>oT zYF$jyo(Z5!g1h(|^Dbc~>6(m6kjPe?X4lbF`pbV#m^Ko%E@QC+SgOrcvd%q2tFb@6 zzr-oNGn{8#VV4XZcn9*-BJ%<0{br@DC|CXH38}h(v2WRWPg&iH%#G!(O9SJ8 z-t>4<9HHJw4`VVy=_h>y5`w@y(GUeJC-Bm62OM1?GaHW=LkI&l! z?4z8&?JoQkbO1kXfeB&p_t`JX^%s#73X||>XGob^g^j~nkl+%=7bwG20-c26iED!o z5sf!RUk0tc{+O!8KZw;^7DBO%#=Rk`xy_I6G=&$R3_XVBA|OkGR~jGoW#}>+_db z-?~_4$4j(qse|qN9k>FhE!eno_H4l6G9Am*QWO}tCGzUOeU0b}w)76-KRuhi3qS8FiU~`A6J+7tMD`plSma^?v~) zkGPhL900l6aA5yJ>xk$*zB=V}%lS7b8CES}WAxAH+M8y#)p3kmqL)4v>; z8FiOPwjyxrfQj#R_JLM-%)zLC?I>0ODj3Z=1)ua1MLdG^61$BEMT?i!uPp& zwOg)e9*3a;XF{&ap7xac~a-s3~1+_(RxF$vRc=JO_b`u1jhpCZ>l}t$%>PL;fTWb4yrgz03 z0h{_~0PMk|l+|jP43Y9xUl#Mau23Q_ui+=d^LvCUxqBsB;Rxxj0BzE_#OiuQq~jl&36~*bbduVlMQW`D(}uen5fCGj?S!9IknuNxRT?b0JTss%Enp?%aR9+bM&o2>{#(gqT z6suTSQMM#b`Jh?zL0A&^$oG>s9IIbkw5p}tMH%9c=7?Qt2T>D(Z%S?*&pTu`5(o%w zjn7rS3;onq4^`{Q?iFl^sm8+86bOQ8x#x6A>>Q6y%~YJ`^<{Y;xytMrmXW#evh`Zk zCF)b@3?6h=HnWxS*)nVYm2`SaJQxLd@Fq{Wukm2sq-VnuM1piCLzSL(2IAIO6%@u! z${|JSs}FY`orWfU9t)KAXa6w~(+4KVQPOAF2M|Y~17-&$Zih^P>BGOK`{(S~>MDz{ z3G|A}vrtCdfjzug6i1;x;Vf>B?T~+8yFDiec9c|jK^q>>3jMCD6V?SDid;#gk*HBN zTGvMHGpu}A7dCCLn6vbyj;_xh-7CT*6A zO4NUug(2)g8}*|1xV%G7GHVeg^D$%JZrM&GjEZUMEkSWwKH^ATMWj97I?=g2c{Q}2 z@Ye}+f#UR$>9~V*lL9nG|t^6LenV^nw3@KX= zYIDu`@?(Q5x;z+`+~KDQhuKDhl@-0P(n>ed+Z|4ji{K-H~Hg6SF*f+yhPXX#lRjNsO54~wK0zKWv;8!EG9{LzB^rbD+{{eL2U z*ArHu2veM)8olpKR`Tle$>J~#+gQ=KRnsMvf7C#yX(MG^h^ptB(Ac~YcT7Nq zPd_W2*KZFn{-?)moEZo;C$-NI@f5HYNYUAOg3ZU81PVKRSk1lgYXYK`&sol%Z6XCD zNd#2^&Q~;%b37<9H=7>N5|cN&0#~cFfRmLD(@7%hyM-~D%>SkIEY_Jv=Y+Q3hgfXV z1xwI&cg4M1;r~{U*ZoPxcn5BmVYkZ#GZX>@><3tYwYl3H$Y&kDU}DT)yO}!J;rc0N z#vBP95uGYTHyOBWc#19RN(XURjb~O<^HKiw&|7drEsb#z7e?XVsE#9dTAAZV_GCsX zXJU$^c~_CdNDpA98&Ik%%NPZ-o!!43mzi1#Vyfx0m0C#Bbsk=j5-7Ts`_RCne`-EF zx2swv?s|+7HT_M{i#*t(^V-33kJR{n-h1tK%gzDw&oEz>02??A#xA2?rjXky@T%FY zmVy*7$Emc+Q$9;=6pVh9gjUD`+T$kh_aN} z<1O_F#_>ju!qPL)Mcn{m9zlCX{%@vuHqP;;29CGn>+n~75&$x{^EeO_6x_@r*%ox3 z6T&rw1kKW^Wo2RvXfeeVU9rLS`+BwKxnu_x0(a!~Z`urZ`1vwcgLrp(Qiyd4XjA*0 z-AMz4r(l4n9hx=s*G2=7dv0TC4VK4WL~U!+OVEc?C%pT&mp#_@nm)?P#V6Ht^iib`$ov`YYtC^Du(;mRA ztR23oF?&BE)MbX$R?!ucX2_SP&>Ol!IQuz>RPfhUn#U!618a7s%fLVNs}dw9?{I$2$VDh-z%69fI`n+hFzxdBeOL6A=@o z-+{C*TYWfuC}?P{uxY5@%$H)cj3q^U&J_*a0ZlKR5(HtbOi!<8>+(E*K}PCrmD!U& zF8y@M>O;C5l!A0Sey7t7=^zh72S~nAyftZ+%bfk`1(nj*hH68mOQJwf$-JeW=90tD zB7jNK@T~0}#Yt3?$T9fnuFgcd5hEj5Rqql9z{kmgXD4aUnJ1==hJcXGu3N;U7=8~8 z*0J%pP>nDXUwp(#N>q=Rz|FxhlW*xBN^aPc4aC{h@{%ldab~Xe^1Unb4WFc#G?wXT zn6W3mpwtU>2anSd=)#N!3j-AsIZWa*6ji*{a65xspDBArmmfg?*l+}$YC@}J@|94B z5&n{*rz4(a7iqsUU`jBu#JshyF~_iGe83$3bB5I_=nBY4=W4R15fEEq_ z?|+vY2jOCL_N!*wwliVwI+ddk_$z8EeeDtW*MBe_@nITmJ@GERgpczrD1a#dFisJ0 zPgOahcX*f~C*<;qpO9+I&4Otc7qjM8qhn%TXJO2D{BR5>$Xv2e1}k?-GOVt`ZSSKI z1`Ff=>b^W%nB69x0y4*@-%G0Fu{Qb&Q3Go|2Ld6|{`?)&)U5Bdx1$n4zX?sfon6!V#`MW63&0Jv}$@g`cvQ1}R+0Mv%z~kS<}YHAUtQz#rLf z_kdk@nU23n#wGXLV-Xhdsy?1s?g&FEiL-gz*5(kJTpq^XhiSvchkz*cVsea4$TsoY z=yhAjAiPH%KLFm>tOgss z9s~y=1D`c#s(EeGBa~SOR+u%6c|ZT4Ef>a5Zjmjyh?#L!_v#bZFQs`7}Lbs$(1 ztzDA%K^du9NUebqik)puuY<=@pOLxp#+d&B<1CcJek|cl^IVKso|D{qw+?~S5LYLJ zkamyh3Ek<43sr|o+}MQ7c<#CE@AZ98R54o;dCsEYuO|G z&lz?M+KL_64f78_o}bAqb+zr4pl|}g*w0bZ1R8>Sq8x-TR;^t6*nRzr(eiGh5dw6y z-G!#FHW?w>_7?#gO_Frr0740g1fCnfXPq6N{Gz=gEg5!mC0*hhnJXT+8~qUkl@z~Q zy*@84emFtxIb(wtpLqFgET~EQqDA{Fe;3&Ec&(2eQ(c3?8PJxXYiU{X0euVR!L-}o zmr7Uo#Zkeu^pF-%^_NG6c7k!=x1+dXzMRYGuK1TmUx%z79h zjdxCCLlBUGSCHcd6H8aY?GPP~kqy285edjb8@d)oIrl)IU!A37A!Q)8UTn~{GIfF7 zQal%|5|tcGbdc0*j0y4af;qtl%xYO{V4GSO)2VA^t;MoTXR4!V^^ zVqXL;eV&eXu#_%WBhx^WUz_onM;iM51o$p?wCG)@OZ+~vV`Y$g$WR?lbJ1x#$Cvp6 z$fHg#3gpIbf!ZXHmslbzc`bq{FKaGji5?D#^>^AlV4kU2FS3P7c_kyaA!37@wM0E9s%KbSAk5))^)}v`<7^iw+NhhdVIzYj>f`nj0$%Xt=nq^A;q9 z-}L_7n{h}qSv^bXF_Bvs8xy zl=b1DZa%0g;PyKEowMtC{nj+T6CVPMavyOCR6MvSt1kxcm-xiNUL{2!!CtMcLd;qY zc`Y;|he?f(XN4~d)pgm*dyq{X(l?>W*APl9R+}d38`T6;;RneK_is=l(v5KMoySKm z){dCOdqmM5eqmRsSpNo=qbz!4ePh*B8D!Q!GjXas)K$+{de)px$2annkhXF-&jMTM zdX#Aii}8DR$irwr%7SA@NFgkPA5=*L%cG;SL!)VaXA)Yjawa(6-A43c(H>Isk*a>b zftMt`1p8ZR6uX^*m02UO9fw28+lNC##TXrc`7;V_ib4p(S5~Ka730BG!8C9|{fUurru-D~@Rm?wCGtxI`X` z=7?uj41p_S6eH(zY7M_U7|}t2)Mnk+oTeR|PeS^)UEk@TiK(XopUl@b z#O^oDfpb6>!ln9i{u17b+es^htVc2sot40LzY-_K9VPrBZF`R=HTd2O)E*^?Nv<`8 z=8i;HJX*4Kg_@95LkNdJ(bIphC7IJ-Y^q|`KAaG|MH~d_To1nrwX&x+QPcZ7& z77aNmxTK*caX+3WZmn9op*!Qd_bgZNT%Eu3B?}Y{|9PbJo#eIC@Z?5W__vvXT*xC< z@ZwFKb8zUiYKG$>E}32wH|^T86)R5!m__Wlj2zRvd9!y1h@J#efuUXx0B3PLthnE6 zEwTqBPs3?|F$XDCo3CnY9jC*O!&%qsF_#=Yl~IHBz@Qian(h<}4a$QU-dw2N3>m-h$u_K1@!$Tf~LIvfxn|G_-Jm?el;!{hA|!k)Kc6RlJut6 zyrY9GURm%GF~M#z&aJFMx!r7^K3?ULuE5 zD^8Y!mpC1vYV~RCmuZ_9Ap2=@0do*ByeK~*3&)waS1VJ7(1rT)UB9NDrBaI;VECDj9_9zf+opdo! zc4&MPutw*U%-3yH_F%Sldb>yirxMYMdA|?z=@4W&E|$rP;mO=coLQ123v`sz=^t;# z>?a=Ayx*r{x;G*}yTq0eWK`@{B~4$Te@LC(rIi}kCN=&{eyz5nupR%AnvSnsW2uq_ z6}h@ax}g41ZF0D0M;p5}-mG-+@mO|70$5W5Gb3WXkB3`v-|^SAsXwX%O={DF8rCkCe5Ojr2uj!Xm5@wf1}XmQJ#c zd>aA&SfJ!`#{}%Nr>f*Zc`>l{=cDDnky|Cog9H}PgU&VAlBQ0Iy$yzbd5!t%e>-diE3v_752}z&ChR?$%zUCTte9Bid_;)(;dzR=ytRiHH{Rm)J20T&Lw|*+s+5rgxw|rt({Gtg7II=MC zx67VI=2<5DPToO8iC%!&KCXE3u)sxq&IAXj#EGBR-Qa<72z;Snovlh78ZWJ3a|K8| z;n&8Td7d@dX}sAXjI0~KT?gyrLzXAR9PdM3t3gbdf|lsY>id_&YR+?Af1OqeOtgwH zoXdwnVE*BLP2aX1{vbLRecDGlj`LW;sMQjyPzGSS)yqH_xx#@9Y%Z~CX_#l<%!>!@ zvN;#o8j$7MmF%&BLG!ly<%_H>5mZ|XJum89j=Sk?v4B~zzHd#4OJ^9*ld@waBqvm+ zzh-mlRkcR%KfUE^pHAW1afX44(e|1_<~#5RUm0qkH|$!4Z^1rN0~va*)I?(A3%HhB z^KwzA#cK6S9sJ`^$Pi`EKs~n!<7um62IkC7HyNhsbJ3HH_Ay_zSa&r^`RR@b!(QN< z`_1Bm4$0Z!)2gP`AiIcT`H81WJ9Mm^i^XMNHj`^Cz(4!HS5KVc5ah_@!B=t5>tcGi z@5s-X6BK>{Ew;)5u)cl9R|qHdU-Z0@;-p6X;@NU!hw>m_F)rA*;dZPWe=kVP3x^%_ppDCR z1JIJxsPtDowkLf*b?vw4D7V)=ZlI)PJ$WzyD^St}x-B>NbY(%RL^))AMdXQt(k5|s^v;<{R293yt*J`Pv*=z5i^((Lpx&q) zA9~y^9e6AMisCs7u&fz1bH$!B5S+VAb(UV1e5qS(q>ACNVwz(#s$;61jC*D=6-^!a zXHk?OJEaPDY-6F5w%xqUbaP4jcWO#%3|JrA3&>_q0WLbY@jIx73JUJd>GIki{L=hF z6~D6YHQ{*YezNk~Uu`f31B#p>B{&3|@$Du;SfVo3tCNE}XG6eLr;GRV= ziEcDk+t)i`NFjdWWY_H>xTunk%%vPXR%S!&4iQz;Q z2sViB@tM>3n5@kjhRaZvRhbC$62&iG+`_#3Tb7Z=d2&Fc)x(wX6&nP`U}@7la8x5Z zUgq$PL=qX9bT??yUFc+i#Y{6qh>onWU|QS*K+PwOGz{x2z>glqL-^vz@vopd@ToDw zq=WSTs1S2Z4Nm1AP?cdJcaL;-(26~~a{=rO#DTmiTKq$(p4AzHzQ(v2F-vZa-I`3h z?h|l59QVs1;0GXnr$sOJS?LH3k%d@HNRQ%MaQHXjhGxw%t3GdSdYSoG&V?*u8N-Qm+3+!3Dq;HKYq)vd9jd0|E$>I76dsg?*u?_%Vp;|mEBiLO2lP%e;zjt z`YM;@06##$zn7^DypOWF;+P=w6o0~#F8v2@4+9C zs|)lBpM#)+KIeXiCojXmEq2D+gD)%iyl=SsMx0W~sP$D52zyBCQew>icn}0}`1@v~ zNinj&?w5o)YTm>9XqP*eEB{!R&%wv8Ra#RsksKD;|ArG4>l4XCc%xrZ$Xp^(rvC=K z3=Vh^#R#WsX_{7mfMG5K-h(*wl4zD%R-b|U2dgUp%nb6PD*%mt|4t0z(x~REl{@Oe zlQmZF>63Cm%-{>dOT55*bmwT!d?h|Bf0DhVY^Zv34<5b>vM_8Ou{K2}q|qL{5L+h6 z?W|UJtCgE8JNpR>FrOHMM#U`+ER?oY!aLh30jT3NqTR5r6-dyb1((LO2YpR5HGt`` z{D&Les(^B5J};T)zxR-cV?G-QN(4}$iX^XjPE#!UQ{p~#qHAeqXP*yT+BFR!PJ3}r zf+qpD%{N@#m(JhWb=W&V-%h&YOD+ffPCb^XXF(iFFZ(oP=n~gk=)RSgB^T?0$uw~V zQM4vL(NhDojh0oJ0~m9$v%Gg#*vJfGw_qYzae z@s|fMF0I-1pqFnh!Rixiz%n352VgRyK8@OW{Tr=s_WukRiV z8H|o`>@dzbc0>UoNQ%5GX`tovaI~?#SlV={6v*^`g05+HcZ=xNeB!>fQR&`WO}f&| z4XBbvf5)7bKWKmQYjdeV*TIloAOF;+TX+Kg2ncYyceXjt(K1) z*o08b*j2esctybJF^~LN;#e?zoFV2*Jz_&U=ls=}V8PmLz4_+Gi|*H3>WtN>ZOv7tF}4llIT~JAHO1NdeBMez<|lv~$fE7NfvvS&XTD z<`S`=3b;JcLB8q&WCrZ3uJr)(fch4XQ`kG3CR+qQ5)`3^svGiRCGrY0B0MyAwyC^! zVAT9OOv4!Oss&v`fBTeOZMuwuBI;NS;b+fjOw?NsvMtW-+f? z<@d_bVBk5tiPFpq9LsD(`b2tb(yYi^bWJfd=w_8kyqDbeROK!-Hs{ic0L5J=gR|H; zVSIs;;1JYUE%@v*Mqu*Rrdj5ms&G@=oTKyR4$=x7N6FB87!8|ck3Z#1ok?d!u__VY zP9;_y4PpP5r5QH+;On@`*=;bPh~yZZSHnEpVkEc%v$kqC1XWh#K;*JZ1Vo(n3^sDg zeSs2xr&sI0&=x1N zo7{VpU(Es)ObG#PVx3PL{lrHR>CF|n(D|4U(0C}MQZu|Lz3e|IQXfJi9_!+JAERP z=1R+XtlLdh=gEm39n{9}fft0Lisn2O1R*4a8g}D9#?e=>_HWn=Q42P;@5!7a97OWe_vV&zaj50XUkzVqaF(ud0(10WosB8A^%6R+*AE3!7TU= zx$z?h#%u%=SMq;;r!hRs(u>LuD9v zfPqNF4Ds=pPP;6#js|SCGaUXUh{jz4ZZI}g^g#bYnV~IvI#q=|KUJ#nVAhAQ6U~N2nWtQ~ zFhdTZAKL&Jjt$PuxqRRSz@)Q?Sw0;H5(pJ;rwG{t)9R$rnt%K&L;Q)s11|&vP&bq=!qxvhb?`$@p`zrLafpUS7~Sn| zU*$w`@)Gn$0O$G*3o2(^@FtwLQM0Nb($tgOmX;zx16%X@ld{PpAmKK!5K*tr#Z870 z@7|H8l7uMPWv$&dIWA?e{IcaH z6U6WY7*L`%bhRN2)h6tAsG0+TT|^KW@wLzf#`MzzqZZ!e`dFCqKq>2E<#iIjLU?#J z3#89neF!_b(KS)q!D@Mc(#ey2DB0aw<)){Kadoh)S2R*SseLhLw+IT+EN^bh_TGzK_4eFmbZGhQ+4=LalD+lnR*!Y*c9Pc&8BD91|+^K*BAp2kdk$?c8yh~Ck&S4WQfoK8F zSyc^OXShXys4jG94@w0>gB6E17PudCU6ME3wQJuiUVQJ#FqRLrY(lT9>)+4qST zk3s}aKnX}BLKv`{=i;d;Vrr)i6ZZNZO9x{22T%&Z@CZs{d)+7{<7;L03c?;>mwiU? zVA36^Z~!dd)AZfz+Jx%dKzyt<=W^R4tXCZXAGdB&$S`0x_7FOAaJf?ki~Ct48toF;CmE`RNMHjGSZ@dN^}_kHq4uke;Dx*1=Qk% zA4x|q(oBIMnQg%!sq6U22|DJHyPX|I!=5DA-dxoTK|9GZf2{xEc|g(GCiY@+R_bxU zHt!inb(oY(H>u#Sd;GD-YS5RS$_TN_}hKvmsqtWrxkb-{(t zsYsTdd1yD=bd+uA{WF#9WBFcdwLqFQ!)Z87L9D5?Jnd3H)dLyO<_*6M*Bv(Wzo2{KzF#6ENqQ)ksp(G`ImUla11O&SOdmUd9Au`I4Ou4u& zP~uLObD%3zU|a>!D|nb8xkHVI&B|{Jirxo)Et*tQi-%C3L6u!|tGKj72*B(&SeS?GgQ2mwoui@P8zqb)#ftKo06bjXgX zV*kVp(8sOF_N;V>K}cH&J?@S0AfO(4A+9kVmB$T&+I3UjbJ}aWJ}Q>0T%#Rvgvbnt zN`(CRB}#e9u6@R#kz**aWT0X^Wm*8t&Zr<2KSN_0)3VKZZDZ4;eFa-q5CQZqQ<^?B zA6;yFqSAugG9Ja6E%Wwd$KQ{gQ#c||@>VG8-S!LljqN~ORg0>K`C#&u0Z~2QFDR(0 zlNdCs;DT40SD?!1(#&mtAs%8{s7)C?9^0>Jw4LBzFH9$xFIA7Z7aT!GM+(0jo2 z0M?bN^0Q%s{2_*MXWqOJD;yLpy*`B%teo|P1(MAFmCC!oz_CQlKsViM7d_%D;5RhKa{j>7UVBzA6r7+m5S8)JH)?)e8^DXgzNVmbZo<^k3%6Zzse(iWRpa!ZJ=1->UKtrffh z94jLw(x0k41)&Hz^}erQ1K4!$=2=@P;w|yT6vCv{fp-7 z@4|A=n5qa|y$Grdz4C{|Sb>f=^_B_tB*Z9~_Ve8sP!DrXBro29Xc+6siiUUdgV=Y` z3zRwtC}rcg=pqiVbOxKfUHZj7{w`I#w~N!HuhP zT6CNZgSm(UiW;ChzUhe0C#6+EHQ8GWL#($Zc2Xg0rn17H42t=F1F2PX=Yu+e53lDd zCDO^U!u(tnvql?|giVUOWARs3s@}O)_y!)&+-<=~)%j%9{2^4!cuSMy6s~1>{4xEb zXa+`~yjC5IUt=TLp}c$3DkHFER5;uDRHQA)A=gC+Y>9#h3LY9$UsHevgHDt{4GNk| zaj;R>M|~TmL(NA(l86=8m{eE2u^+Bl?t1~#W8gN@YxA&0*c1wn@+jg)eSZ0Rfaz}2 zeH%b+k*PIfnsrkwq>vvdF`0O5f20r(^7;#~8&N%@1!tyJpcM5k{0PQMKtPfF;${@Hg40!azk>Di1dO`C@ZBCI`*AxR$lObQy*_c{Y40R-J;bNH z-r(h~;1OZhDguvcu6E6_6*g@C)bD9hU$JO>qbi3E5j!~&zl5+V{`9V7s_O=5&Tec( zOb0oRyu7rcfFS%^y20ugCMTOJ(qe2k60NuF!gFqmC~&&^Gl`x9kRbeUO>B4XZiH6Fb`MB#$_Kyuc+v7DpH~_2z$B4gEco73 z2u-=MM2?h!#33tba$Cek%P%Xz7@F-$+YNzW!piW){8&J&yu=+KLvn0pHdtoH%GX7N zh^<5VW!qavzADnIiXG%_=xXgu$nL`u;aRG0dY4j-6S^XpztB!D{`!UoBZyGqKjl>$ zp21*7BVr10I~q)9lt2e${Y_bK^wdieAGD^o9C)g zDQyz)ygL0`Fw_W?l@Y(}#dcsx?-x>DB1{69Znpr3O#Q4^4Ca_F^vQ3fRho1)VgO+WMWfVM$K6HxEUVbo&Wow~6U&;D*RwQAMe zx_EOY(xroXB)J&FYVv0>SU&axq{$ri)s+s}=?O%VdY;J|azd6rvnDxEg!N|tvwq{- zW5}UNyxFsl#Fl+${gKR6%;OolU~0iNjVfN=aAuOaZ0={a^CK_t8ZAzp9Q1sFC$J`s z37tP4*UhN+_lqdzDEdw`9)9~_fKIQ9OQb1PLc$ynkP}Hn?QPzVXe~7wwOv~sC6$-+ zHa-`r%76t%%OYqf!v8C0!3nIzk%wZ7<10QWxW(14BBH!Z_2S?MPx?awsi?qh0zelo ztY+@itq+I)O(h{ymWb5RSxDKrp3_F2)7+O!r6ETM-T>qBiRmvZW!n^R(_v2&b}UlP6qZ!(3JFH6?GYsw?S-hE(Mjs4Lp0D$}7)}m}vz_l%7ya zWk->!LlgatI7S50O`Whu?Pf7Dd{Qhg6#ku+(1{N78Jc~zpW>jgwtmaMR_g+y4boJC zDwRXlJTM511fonYPT34P7(1_{n~FA+b;U1Q63eiSQa-N=#E%!egH}pY;#Gu84@W}; z3r#H&+=)XO6lK5&mC)!T6|?RyQkoY?uc>;@z^0tpQFc7 z3NQ7g8ZyEys_Z7fw!{#L%!TNej$EX5m3)7{T|t(g)>)kn1W#C2t-50OZlA$sHiTeM z%3bcR5eCM(cEBU}?3mj>2bB{x`b!Q2w_cG0A{$IOwg;wYsqDf4Nf=%|J~R;ZLBJke z@SFan-bOLZ&iTQYrsvxq!dquLb@Tud)uGWV)%`ih4Pa?fiEed-i^xsAKLsF50xI^_ z6T4T2cRWSuI$jH#5k=|l<9-c9<}*BJqz$_J4i;CgGz%`8nDN#d(zf%%PQ(NyLH^Q` zY}Pi`mOo;;K_x*UPvgW!x)hSxqN3j>OdXybz*7Y;0;<$j{C*~Z)pv1r4*88MyFa8G z*#?}2J>vYXnCx0Kxi$|5J+h;48R!8b;wxX#)ZcCAzXmrxM-=v&!f#N=zpyX&p4# zxNourb$^z)O8=(w7N;zD*izY;*cdkw>&Y?>{BO+y{EAReC=@!$+KQiv8no%M1g_0a zC4qsMe3AL*JjLt{yWnP`nS%CaUr$>fdA@nWlAm&>APF?JX) zO=a^gJGr=BmF0r&@khh1d$F7<=;s%}WLtpX#VP;6M^e1ykcZZgT0Qm9tQ28j1=~-Z za|bQ04?6`(Zx+%lH3n9G=lT7+=ZKbGnm85whLZr)dE;Y)Q;6>O5R=Oso2IG9L8tNoyxKgb8LGeTrbad$Zbc7HW$bx_d z-2JHeI)q%cY%0xSlAh0We|-7~&=StZ-vVF|(%5y8Cb9+A=960*%78yH$0CDw67;*9 zt}vawtE-FR2(Q2&;vK2QFcfhzuqT8*Gk^pwEHDJeQTHOdJKJY1e#*5K3i4QM&Ezm*wYA>p85V}Xaz!xq zwf7*s+bN~+iiRl#ORqo|O^n1-Gpk?7D#HTE6hZ2I*wiQL4v#ME04l&y{@W_=P7EbV zEtUr+3ko?=+y1OC_$R3!Ld|?DsIVGR+R7sG1z?Za?46Gx} z!0i8q$P^0i5fAbZ^iaJ^FCM@e_p?DN3_jDr@!S+v&wF}%Y{ zKp>;cb3?V9ri&1>iKe9ygJn-=nT(ctVuEntZ;@ndDqq3e6h#38Ef?m4n_XYS#cG4u z3In0$H$=n!tESlts4^yoc^7A!Urn;U6F;L`!n|;-)1fOT#Q9!uv+kR zLdVz&;7&d^z7#RsM*Gf%KcJ8Fw=}5Pn1UocEyQrIW;|u^Z9`gdzTQX@$ccwziph|3 zw1OY=XXR)SM;!8ERjWjIgKBsNGQp1ttN8r;zTYv|J>_h^(LPZXcOLW4n{R;(;mue${N518S9?Z4KpiN`(C+bazQ>)_g&mW55 z%y$?@gLr6#$4NND5qm! zI0Q9&3KSuNd=J1VDPk{rNl;w3KjIRIAg{h6l~$Fuz_u3^mlR zqB?X$=~b3udlavQAOem$ITS=Tw*6a8ECS?EWm<0Qw8qv%4(y#C+IBb+iMAuO8W`35LX zF0KPc5f%Q`R%_=UDMzC1b?fzFI@>U^N^X zcz|tN)5qP3Pi%>J2yuG8+eLE!UTRS?LfW#+nWQ5EqeW!%i*keUdPi^fKVOYuVqS0> z0Bk&cH%}-WKnNDPRie(bFX*d++yW{q%LD(c{WtUS{ibHteY4mC<#k?Z+8z=#Z&VP2 zy(73b&&n3`aTx#tz_M2Lfb=8*)^P~NrkPXNF_h+u+I38<11TynD3X|p>o~U8=*QAV zd@EczyE(rye<*#3o%{r(JELiBOz9g6($b&b6s8#-$sKN>D`+S zXCg@^U#~o;R5XbnI7Ks9fE(7o;{HEzN21TDTI59%=9rK0}@)&o-{&@le--xrc8B@7%1aMue>b?Ee$lir3-65bpwj z9r=VsM95{a?Qqhbg_@(P8P9f~NPAJ_?z3acDvU;ZRwg#XooFz>8H#)y#ceUM!JFrX zOVoSYAax)eI54Ap>+?D$2-P`gLnEu9Udf?%yxSl^eMX9<+g);&>B!9aJ(hu1$;MQ{ zCO9wxVMYY|kgm1#Ff`eZOhrIzA_9p->2Ch&*wL);L5&U2K>Eb3gI=c70CCZkOYy3m zlK{!bn!%EOroQQ+rGo0D+`uC0Wb3{2e<1rUzy?-+hP76|0=|rPq(kQBCli#3J!dJg zctS3N2ErdeRrrE=R6wMJzhnx<4HqoT`NR zH;e*W(dngNz)8k(cj$?8PLGW@R&~ikOSG4<9Yj*=`%TW?w_Oc}2-wmgEQVf$s&4oW z*8tI@#Obp)t>5Tbd$_34FY`S#2a8gga_L83NDKV)0vfg)uv8lUTRP7OK<@r$Tq*za zF9LDRQKf`b0+#Q%sC~^?dM?R!-WcdNiyV+&Pi@zkMk~RtFH3(;k-Lfyh!hUBPA1JF z9=sFq6j0AAZ?JmZ9TqJ_t5x?kX#(vCSNwMxC}sg~Dd(T|1lQ-B#>|2EfXV)QF*?}MX0XFzDL6W$V}d(Kfrof=x8h{_NbkHf zh0uKO(@vtCMO1N2qofg+ULi2F(oR5Hlf3atI0kp8`lnTD(*^4Dd}oJp<55DiG4%0E z;w1{jN0rDW{p~}3)IouCZ=T26rg?k?0)%3Rpk;2o56tYcX{9HT498G~5BeWX+LiJT ztBN&+O~Drjbnerm(OTKjGp}kA&M*@|`HcO2zjcgctUZx25T<3~ah~=!-lyYsnqJvX62e(M9Xp*%5jP|yo#+~jO&tA^i*gBfW zyBZY7HMdh8Y?n2d`*I~x3fifL3(O`N`)SJQZn8b%8C;o-O4V>`46G3++^6!2*_+dn3asG0w{2}7Byu*G+&X+(=v=(n>w+F)KI z_=XfLxH2F-X8}oV^(I5J8?^s?yxE&5E7%b&@%)QuT%pSEzm#jj@2QhrcNXl2wwrAQq>wlJ;c|*av#NlWr>9X zGL3_XnE#8s!!Y8-U|PHl86>Q>@POe-)Ar8QDI5O<*H_e!q3s8l4qmZrUl!uogLnbE z<^F302+W81Rom+8mRI{csviotNcGeeK)}OminH|Z%KIM!GlUv`!o}QC091vS2u)mgtu6acMSBKfYOzV!0B)T3;0t3&EKO#lnr`F3H}E*b9m%xhyqQXs3NJWkYg^jKMZd>^HKb`AO=w|F;>MhD~A z`Fwx`Gc(2mX;)88*Dc*S%TU>Jwma3W%694PzU^>=I?iN`Vb7G@3T@3aXls^zG5|g2 zSSf1CPTnbb>-&HB{|>B+3x$_0uu?!j)fzzj{9MT4vYi!jYeU~8@e#zDFzk+k9P@9w zGOfNTD4X+g0KrQ`##-ptgYLK@Tmdxkgb>xi-`ME8VHQOuc&itGma7&vKK}hL8Pl$H zj`E1Dyc<_3HeFD41u)}aHH1}E)DL+ndD%3CLi0k7gt;X!z{ds_BgiEv3d6yP1y%o1 zp^kf-gdUGrZAZ_p8rn@f{n1deIr`tdW!|#`>@hnj``!tgM*!pieujF|Kasr zpg#Kif>M8#@mFeE2j?aw{Z5qR4zCXmDuSlzz&BGhr1}WB8f1HyJbfVfC>q^uXrSr^Rh93YCK z-J2^^2?f?>i^ZAtYpg_U>gb|tT3qA z;PZq8FTqCw}Y;K35J77`h6_}V36mF5pKWc1Dj9C(mX2KiZ>|50earBqY)dXF- z_tLi8K&>AJ;UBKhf55P2HgEfvz~LlXASf+q8!yXz&)XGil)+~}+dX}szYy%lb|IkY zjR;|Wm&nyxcG~c@l317gS3y~$(sPVTInh^(l(A4d`kX=MoO7_3Mqi#dLNu5$06xq{ zeVVA6)dkN@sQmPSzUd;pW52j+9@_AzDBHfEySGi3V?_bgHYRrXJX5bS4n z-Q1@@^*XZ0@RRLM%9z)>N7s9D0&0Rfsg`wj`UHb{u3(8KAs;gET%Qe3a-2ndDUI57 zA$qYf7mIL5HdUQO8^0m!1NhBGj^C8<6uOauv%Rg>r6GWMbAEjL28^KDkDktcq%WTv z&mHHS&MIT^k8)Xj2a;~Dtsz(wF$XUU zQ?IHsvu!|54;Rwqs~}U3Dxa#?K2tauR<2qB7YEd%=_)Z0oh82!ovZ20RI9!4rnRF2 zX$}TTPd*%f8Nk5rZ{)|B_Ogy0$ctX@7q?HJA0JkvBp>l7d=O{S_E@xawV4bTZ-6_s z7T5}td8fJ-w%A&^i6UBn%8e;JSlh)`kUEyAJ@n}8TS_E;Pt&k0;j)!#wHZw(iJaey z67P4{D|4MvxuyR?HDEb`kq;tmNJlv-xj;LSMtq9dPOXz~W8d~}wG4a^tmre0%rd<8 z&|9cj{BqvqB1I5U1}~A-n|Zd=tEau4XGrW~`M*k7yJ-OkFt8RXlJ1m~=4l6dH+$!n zX4h1PH)Y}+3Oo$+KSpn*?Mni~P~*{vecZ+$uM|BKx+i8W zIUf9JGq!GTMHlKYZW-QRq<~1e#eW_639ww|uy1$B;rdJQ&uNjzx;ti0UlO9zN42*H z*HsoI3}wum-nO=3xT!t3q@h`MC?hhAYOFgZt_<9P%D-O&4!S6q0%PZlNF?VIF<7UP&TwZt|CK7{1iJ$e}pdTX*Wz%_3PeC9aRmm#Y-!d zKzwVT_*TQ88pkVJ21G(cVLE1!b)KhbZQee&{t9d4>egrhbH%D;i z@aA!ODpOz?MiquA2tPCf66fG>_$k!x*N4EH)pOG~HBLXab+37OIKD)4aJ6``OhR!5zIF* zXu~^Fh$^a%Cxxw?uxj+ll%!q`qEzKX+oGj~VlUHdvGE2|oKL-+91%Pk)_bP{y<35v zMK9DQ+)3*O+tU)+)z9P+B1}zHlf>j@RDH2nd;N!7zn*qKzHAu-*xg=R!l-rX{)6|v z8T?wC7r&{P+z|SAzio#oK%3iQ22j-4+G8certJE=e}<;FBaC;#+91rT`as=&yw}uL z$y$F`y##LhHx?&JouX(EizuA>u`|^%{W)^)IjPM&_fi0l zc?<1?)I4SHl3_iftAUJ?toq8~1W|#z)Y<6A!bTF%Yv@?%;5{4#%rx#o#VsW({&JTM zXH4NTZ>*$@x8D0>IQHke;xDtnf_xgX;nW}e{jYSV)C=G`kKgzclbGd9&(r8z3-Rmp z#j^P`kE_J!n@cn(IC zn!!QmV*gh}ccx+FGhO|w^7wlo+BmIMKp=U1zuA9u!owmU_+Ckrz-$Jk*0 zcWW}OzOaZZg)?QazhqF;&RBawn4^ORI=0z9x*nf}FVhm9;~InI>eTXX{F=Pta@Ve5 zrq&;i7h52!KJacwr__>_(ReF@jbnqOT}VTkKy+npU;FBL=Gz?}TW)0eyq8W!WFsrf z?}E%r?j5{n`b{+6C4R~V13p&2imqOgW%GhoF#j1-JeLN&)@)dG3qBHj9N{R*L3BLb zb5mQ!7M2H{cQdl~9}u~6ZWRW!QdofdK2}I~ZhUDw74h;=67!JV)?w`izrBz}ObfFJ zq;OfoWS9dp-76)bm%YI4F) zntVo|kb~jJRILca-a{zye-e^1s%QxSnl2^)9>*X3kT0DOEMvb{URg?jNP_ACc#xD( zu#oyqKWNW$%3~?*6SZ7+UxN2a4*odUjUaT#U`}0$f*^lhNLAY~Q&c@i4W(WZ&T4rA zVEIFB=?;@!*r%B%wc*SNIGTQVmvPj~ef42W8U)Fy?rueY?CNd+bUVd3S@l(njDBgV zZ!TW&>~JDPc(`jwz86Z2dAt_(@m+~0YKMsCy$5gybtDlQ*S$vt)@(yua3@CYPN?*| zh!X_)0il{=g+ll7I13D7O>snN1qI$!MY^D&I`OU{$tNh2`VLR6P{vz^?kOkQa}`wd z`7rids7MscwzGGNZwA-Du(v3efn|IrY zKqU5=B#LCnv93i|;>+tpScQ+lOJehyP)X!r@C~Bndy{{aKfV&ePWVm|4fGMvQ(Rk$ z{u)lU8&|Iv<5+ab`Lw%>nCPUR};aUmi0sV&plcHVHIG&roqUw?!Biq_<0c5wGk(Xz8+PFK`M#)Ul{J3j!&1 zmC$5S$lFoPIH&TJR`8+Rit@!p<}aRanG~C~5GJcPO*dfWstZ1>NNgD)+lA;R>U=m8 zJaT+P*Tw35!jd&gQ+%py6K|dJp=qhu8T@}tPj&v-bQO6RL=WV zn_A_zlidN}a#qEdxOb@}&I)TxWnjq>8TX-mcxEq|Ho?AWm4OaG-#8(TpmLRuhT}O( zIjuyC4lGVb0Q*M-JMhL|4`=OF5E1Jc*Wxh}@@T$IJLAvH`J3N31TTHQ%iy3ZtKUKj zI>eQN8Ce?>*K9x*luU>oP3=L+W>~!e>bCj4JaD3c{0YDAA-x8Rx;a|`r)Oj=;iE<1 z0t6_Yj*`1)KK~=ISZfRYkXF z8mKu?4ANjnsHi{!MyCM)u~wI7hG0p5KUB<-f;gxDvn!C4InWH<72Sv#> z608MM!4OeRe1U)_4;Cs9IXTK7hlu7Jjh|F*mZV|nNt)~*DwFcKAUkc|*@C+_?Hve+?{CZ}JdV5mR_X9PRAK{;I?$C+QSh&mNz)51Y z)?}RbUBC+Zx=UH_;h@CWwBmiXkUy(_+u4~~SJ|7&k=$A;M~~`??`5W`_O7^*z&(>bN>psz?eJG zr?gTGpwzv*Z${K^3tUT03asni{fD(miQsQuO0gzVfehP9S#&fS+GP?td*Y*^8=x&C z#IAmMi0iYuv*9o67}8o-JLd))cy!KeLZ0f303stNU(hnf_G*r>aB${s!AmIlNEXR^~wY1MK%PJ`4bwGNA`QdYd_{OT2Lrv#(~|FqH@{wg$f@h%jJ^`N?I75NRnG| zuBKz?4kCTUgQy1`Z80_Q^b5LB)*6H-5Yf!?lcDQQLg9(b`qr(O+!n!RH-)#k+ccCX zp|-43Oi*~_lk38<8e1i*k$svoZ}-?k!MD-^nB$F_IzJ{nrao|-V|FfT80p* zCf4w{=V9P1FPz^Her!!80(seQLVJ=QamdPD1`&uZ>JtVO{r^PXQ*V%X7?j3+BZUXw5Fq1_%Jw=JZmSq+~@2r0x)Q zFl(?orO-TY|2-_(LIcisz8`N=prfjHK1&nH7X_?)d<{7+9}N;SRYk7_Ua%X&+)=rB?7}PuB|O zjIfK*rs!exonJLo;DUGF_bY@Qb00E>Vgd!Ur z_vJOM#4TDTJ%I&oBvM9Vy^JJ-<6`y@|ik>?t%P%|gBTnkyNg#0#GRP_6 z9JUYy$c2gaj75tJw)bHLcmwT3IHqo3OFBk5a6=#OYZ+tS==o5OAL}hTxtQK>U>dxY zx^zT#mDc@nFGSdb6D2vj;3N@fit&3_)6#OZLVH{mI0%m`_#y%How;5HI*+SOEt-1- z>Xu%E?*^jli3`58^K^4BiOJT3u^4^TX z3*~;OjF>RKg3;oYF@nhLS%bqmgH6*lY$Q2SE$02_Ib32sK}ogAp$KoJFD4jekTp;$ z*s*{(>=saFE5j-__&t%<(Tx?qPVK9n!_fwqu!;e;OEo zpoj$}QkkB@HuzBa=rm_P6$Zy0`(u2A*xXP%vF?}EqtR#tesREvwh;0+jY6>|!k!r@ z<^gmunE=kYNiq^_Eyn#5Y-~>4PKo`1Tr3r=_#eta@+C*M{D#9$@CSoqa05KBW;iKe zVGIIpNLS1Xm@A7wEOO=1bm(pu_M!;yqRxj_i9M5A8#}4@Jo5NUy-8p)9Z3ZQ*`iKb z&I+b^7Hh0IiHo@qub%oQQq)ZvI&gR5-VrT8Bsqsx5E@iWXpmRyeQ?!6clq~9(KU}i zRaqP8dIM_}P;RveQR)Dc+lBbV+JBAn8cZBK1?aMz@rM+bXQ0B~J%vW9siURuVa@Gs z4eu_tNQoxi&=NvtSE`wl8U&x-EC$RPQH{WUo6-?SzJKyg?GPE)E|?8dK1?+x0w=r= z4y5&H^mrrXbFB8MsyVygkJYhyi59Mivfa-+8h~*JvWb}4lko6O!6NEW{^H3Kp@b#r z^*7#g;prR%%}1+Bfe43$v54pYtp#P#HZaq5Rivm9^0vcaoffUPqRpdDBlSfg2-n{IdCGhme_j+NRVcYA;Hu7JHe{gX$0V)45p=VaNla6kVd1IL%EekfYnjy4zC6EYi`xN2TDDn z&35y{Vj^7EV+mvgW?r$*YUAixIjn&mk4fx$96-%yfG7{eGue3G;Z6f;{ow9c%2~ar z_ptAX!E{*XR6QP_-Va_FpO8jw9=v=?w7t{2R>O5W3~{}Su|6=b^E{#@VW~C|t@=QD z6u1Xks`~;C=PN(PD$FxCEKHXO3fPa<>WxJtp>JBSO%br{)@$~4+9;&3qy|hni!WAE zPdZd`*@YLN_PoF0(%Vh~qWW%Oo5mfBQKbr6)?A~X3)3)9>wcsDoQ=C~W}HjjVJy2Fi;4=R5HC#N z8IFM-!w9hsF0>Z;x7?|GItw@%D)^d0QWaa~urf6vb^0~ql!%|x}c9S1l{%c#R$Uhfr5)|eXVn=oepc=T_OXC)Z<01dPsiX*uRqr z51wpXt+8W_@dA#L73xz51O&dV`Qv0}Q12{l!H;~C=1F6frXEHTk@C)$fiYspj(tbF zhfZD>1}f`tn=GvG;@l-;6KHy98I|P4Oz^BIz zltwH|Z`CnEj0lA?yTrAfVmXTv5N$U1098P$zn){vZDjJgeV3HAKd_|*$9iCL*#-8I z9`u)E!H*);6J)yp_SIc7D8X>nn+gHHLkhYU{T#tC3iDS-18{1r;BQOlRoM~+OJ z5i8Jx692C-ZWOT6DMWnOHEFbou3=_ciKkoCtW$55!h{V;)bgsET%;qPyEw+lt4DKC zEBEVntgqtu*3M^zhqZ8zTNnb>q3L7 zz~l&E57Ax?hy{rT*Jzvo)GixJnTT+#znGH;*s6IQ93$hABz!4npkIkj=6|J$g+xC^ z3VzQ@?dGO11@pQiT;GtwKG7Qz!5FY%Q9yrJtzVU_zi)w?&r&Q1DC?s~>3l-`$H561 zj7Y-082HgvkqWQF`<$dCpybeU12qo_u1HoOvaUG2%w;}{n zADtbRwtY3msm>-LozU%uOMn}JAW+-;#P3BIL7pCGQ8e4hK1p_lGG*4lC`uW*k|Vb- z(#W^pN)(Erb3KE`kH{lRLDRHK_-ri)b=U)}x+Mt9+QzHFarBo0+T?47$0+bL4TQ+* z?1}MJ93a9x3NXm%?M`vi5jK;ccUrnr4V^m5&rpUnCqNRZ<;HgPR3>S*w>)XapK?iJ zEyX0ygg2RlEyUgENj9a=h3=m%_x?t>Kg_9W>W4fxeF?vKtd$eR$D)P!@jeS4q?CB}4UD^}<%vz04FJxXxerr;u41 za&?o!FHPJ8h5>zssF&cYN_XfuGZwTE1q7)gzY5FBo9sD5?%vSgHuCP&W42m1)D73j zvlMr15aeeQbyqAY_Qf#?8L2wFpdHfXjF{vj4TRlm*<_DL^W)AHQ9RQT%eC<$E(GEy zWHX~C@xLICDmvN7kCi>o7**5Zwy|4DhJ^ACxo7g1YM)OxKaL0KwyM2y~J`cSIO!u6oPKc^NT8lfEk0RqK^ZeNk`Dg z+WSh_3%as=?uT*u(|nOwQhKw2cEG<Re5l5U+PMusJ&V?zsL^1#*Ym z@ZTZB)CP5GrjS+m>aYh6N17rMpQ{v(X7_=dypJNdwXPMXQ?FYba3GH|x0|d6B_n9r=y&>7 zI;3B3>(IwghII@$qrm;1k)4mdxYpq`J3q~b)HQ)>>YCzGzvsg%N|mW5S(?yjwt#ro zO-*AD6+~13(nkqui2X!I7o9LQB1^%j)txmw2;+JMLa=%IsTo~fzEi^oTCnWx0crDP z^Fbb;t~+7F_g{*hK|!Wi)Lo`kRdrWHMO$w1=w3ubhhGJEf73bV0h(--EOQR(B<+d& zmdAxPAnoK-u%*%#g*3(vB>V(2n~TVA!6SyO1eH{n){U5s$^O*nk9ZQ>;y8Rbp)``rLcD46-zLs`fN<#S+47fy5S+7_HQjspw9B5Z4F=C zRqU9$`Ps0Pios2E!GT}p_UUe5IY~ZNa8CGF6DIZ<@BV8maE~BE>61J&VlGVM`-e&4 z`ZbZ2K_b^}*BAYO8IJ6Qv^OXz`gCZ*1t<|1S2nD&8UyB~RITK}-9T-32CP2YmS?Y7 zH+BDt`aNtHun}~Qh_hIyt@^Ud60mUd@@cY7aYJAv##%V38MQIenRW|K>Z9jk5U0nb zbuDOLaGpt`JF`3FV+Vd78#p#P;++CI&-i$ElOiiR#9jOGaxyC0!W}%4`GlCAQwd@`2s!v@?#Xe6{H?+vva zM8?s^VOMtbbXHY7+WS-o>7oQGU}oXNAg#9NC9O`fZJ8amua#^#$Bl6U_meG_;MjY= zh(6riL}$LSIxX7q7kLj##Q62H9RDwK19y_rp3X)~@pugvDj1Q;tAMK^R!vS(IwY8G6mfi}0YssO?nebvF#p`vk9G>L8 z;>Or6TwF`Y1*oa{YU|g1WT=D!U$^fLQi+qHqm-4trp?PqNZEZ+EibVdLih=XGd>lT zt#u>LvpY}#gVauR?UK1B#}aO01f>euSbuxlmz)P_oiP?*fYS4h;DB z@#E1Qa5o~hj1YWl@JtWlhT%en*(%UWSMVyF5&|y}IS3x~ObErY%c1}plGLyecMUKQ z%Kk)3UUltyf~;bW0KNf|O4QZ%>w6(bi92p!w?BZ57}Lizp7GQBCMIOk?x8khdsip- z?1I3xAa5EFFOE;}0x*F-fHej5%J^rQ+FtcIm9TTLEEtIqi5v>lD64^`MYJL)OSJsM1-_y4G3Q=kB2!mpa3!jsl)}g3d(NjYHB1i*tJPI!=*BRm5M>k zkM*eAw9}6-#arSfCP70RV&Zx!8>1rOlrYkygO+4NhGRegPUSA-C@P>Uv?AonuL>d0 zTLV=|tPM(R=+@td>>h)#xE9@0W?7_<3W6Q3$@YG}M}VCJjT2hI+lwo?juaeDgQ_oo z+G`(6`EF^UR>5N9^g#v&>d_^#o4NQ@-=;VAQ zc7bGxu2#&f3QslV@+lUb7p#(?&d!Q|ia~zGveBXEQFJ?TK473@OZnv{!@^yl82w(Z zBg)?T$;YSwP=_jdaQnx_nV73X$RxHlBtyw~*Gm}HI9>ggx@?n~bb)IxQ;RA|P4ncb*sIuYbR}!; z&YMsA+P+_@-tE&#_b`TTd5-a06GY@Jwo_)#4UWtYMmW+LOei|Tn`S=P6~>^tM03vf zm(EpdJ#h0pzmF@eh|8?z&_UPvq}8AMgPzI)7%GOYhwv$}k40H4LClysRB4Tku+BRR zr#AfT4e`|dg2T#E2oS?LRIzQ=Xb8fB;|bHIJY_{uT_K2Ra7^kYp9Yg(1qyUqFQzZ+ z?Ow-Zm@7@k&+GqEUroCcs9RdGFfu*Z{e0U~Ztgy*je75%LCZNZ26Il0MFWGJ7$!EY zDvW6P5;N=tFaPGO@?kdK){iTLQvo{;mL>&{%pbK5VFLnuED)JMmHo;_`$N3XzPi

-KaAdNcd;}`!^zc;R%F5z<0 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 E0GX>LFaQ7m literal 0 HcmV?d00001 diff --git a/docs/javascripts/ai-assist-widget.js b/docs/javascripts/ai-assist-widget.js index a1c1f016..d4192f24 100644 --- a/docs/javascripts/ai-assist-widget.js +++ b/docs/javascripts/ai-assist-widget.js @@ -129,21 +129,24 @@ } function buildWidget() { - const launcher = document.createElement('button'); - launcher.id = 'rfai-launcher'; - launcher.className = 'rfai-launcher'; - launcher.type = 'button'; - launcher.setAttribute('aria-haspopup', 'dialog'); - launcher.setAttribute('aria-controls', 'rfai-dialog'); - launcher.setAttribute('aria-expanded', 'false'); - launcher.innerHTML = ` - - Ask AI`; + let launcher = document.querySelector('#rfai-launcher'); + if (!launcher) { + launcher = document.createElement('button'); + launcher.id = 'rfai-launcher'; + launcher.className = 'rfai-launcher'; + launcher.type = 'button'; + launcher.setAttribute('aria-haspopup', 'dialog'); + launcher.setAttribute('aria-controls', 'rfai-dialog'); + launcher.setAttribute('aria-expanded', 'false'); + launcher.innerHTML = ` + + Ask AI`; + } const dialog = document.createElement('section'); dialog.id = 'rfai-dialog'; @@ -178,7 +181,7 @@ `; - document.body.appendChild(launcher); + if (!launcher.isConnected) document.body.appendChild(launcher); document.body.appendChild(dialog); return { launcher, diff --git a/docs/javascripts/landing.js b/docs/javascripts/landing.js index 55f8af33..a76e9048 100644 --- a/docs/javascripts/landing.js +++ b/docs/javascripts/landing.js @@ -1,356 +1,145 @@ document.addEventListener('DOMContentLoaded', () => { 'use strict'; - // ── Scroll-triggered fade-in ────────────────── - // Cards have `opacity: 0; transform: translateY(...)` in CSS so they - // can fade-in when they scroll into view. The catch: anything already - // in the viewport at first paint *also* runs that animation, which - // reads as the page "stretching itself in" on hard reload. Snap those - // to visible instantly; only animate elements that scroll into view - // *after* the initial paint. - const animatedSel = '.feature-card, .why-card, .why-card-small, .usecase-item, .hero-stat'; - const animatedEls = document.querySelectorAll(animatedSel); - if (animatedEls.length > 0) { - const inViewportNow = (el) => { - const r = el.getBoundingClientRect(); - return r.bottom > 0 && r.top < (window.innerHeight || document.documentElement.clientHeight); - }; + const nav = document.querySelector('.nav'); - const snap = (el) => { - // Disable transition for one frame so adding .visible doesn't animate. - const prev = el.style.transition; - el.style.transition = 'none'; - el.classList.add('visible'); - // Force the browser to commit the no-transition paint, then restore so - // future state changes (hover etc.) still animate. - // eslint-disable-next-line no-unused-expressions - el.offsetHeight; - requestAnimationFrame(() => { el.style.transition = prev; }); - }; + const sections = Array.from(document.querySelectorAll('section[id]')); + const sectionLinks = Array.from(document.querySelectorAll('.nav-links a[href*="#"]')); + const updateNav = () => { + if (nav) nav.classList.toggle('nav-scrolled', window.scrollY > 32); + const current = sections.findLast + ? sections.findLast((section) => section.offsetTop <= window.scrollY + 180) + : sections.slice().reverse().find((section) => section.offsetTop <= window.scrollY + 180); + sectionLinks.forEach((link) => { + link.classList.toggle('active', Boolean(current && link.hash === '#' + current.id)); + }); + }; + window.addEventListener('scroll', updateNav, { passive: true }); + updateNav(); - const observer = new IntersectionObserver((entries) => { + const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const reveals = document.querySelectorAll('.reveal'); + if (reduceMotion || !('IntersectionObserver' in window)) { + reveals.forEach((el) => el.classList.add('is-visible')); + } else { + const revealObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; - const parent = entry.target.parentElement; - if (parent) { - const siblings = parent.querySelectorAll(animatedSel); - const index = Array.prototype.indexOf.call(siblings, entry.target); - if (index > 0) { - entry.target.style.transitionDelay = (index * 0.12) + 's'; - } - } - entry.target.classList.add('visible'); - observer.unobserve(entry.target); + entry.target.classList.add('is-visible'); + revealObserver.unobserve(entry.target); }); - }, { threshold: 0.15 }); - - animatedEls.forEach((el) => { - if (inViewportNow(el)) { - snap(el); - } else { - observer.observe(el); - } - }); + }, { threshold: 0.14, rootMargin: '0px 0px -6% 0px' }); + reveals.forEach((el) => revealObserver.observe(el)); } - // ── Mobile nav toggle ───────────────────────── - const navToggle = document.querySelector('.nav-toggle'); - const navLinks = document.querySelector('.nav-links'); - if (navToggle && navLinks) { - navToggle.addEventListener('click', () => { - const isOpen = navLinks.classList.toggle('open'); - navToggle.classList.toggle('open'); - navToggle.setAttribute('aria-expanded', String(isOpen)); - }); - navLinks.querySelectorAll('a').forEach((link) => { - link.addEventListener('click', () => { - navLinks.classList.remove('open'); - navToggle.classList.remove('open'); - navToggle.setAttribute('aria-expanded', 'false'); + document.querySelectorAll('[data-code-tabs]').forEach((tabs) => { + const buttons = tabs.querySelectorAll('[data-code-tab]'); + const panels = tabs.querySelectorAll('[data-code-panel]'); + buttons.forEach((button) => { + button.addEventListener('click', () => { + const selected = button.dataset.codeTab; + buttons.forEach((item) => item.setAttribute('aria-selected', String(item === button))); + panels.forEach((panel) => { panel.hidden = panel.dataset.codePanel !== selected; }); }); }); - } - - // ── Nav shadow + active link ────────────────── - const nav = document.querySelector('.nav'); - const sections = document.querySelectorAll('section[id]'); - const navAnchors = document.querySelectorAll('.nav-links a[href^="#"]'); - function updateNav() { - if (nav) { - nav.classList.toggle('nav-scrolled', window.scrollY > 50); - } - let currentId = ''; - const scrollY = window.scrollY + 120; - sections.forEach((section) => { - const top = section.offsetTop; - const height = section.offsetHeight; - if (scrollY >= top && scrollY < top + height) { - currentId = section.getAttribute('id'); - } - }); - navAnchors.forEach((a) => { - a.classList.toggle('active', a.getAttribute('href') === '#' + currentId); - }); - } - window.addEventListener('scroll', updateNav, { passive: true }); - updateNav(); - - // ── Scroll-to-top ───────────────────────────── - const scrollTopBtn = document.querySelector('.scroll-top'); - if (scrollTopBtn) { - window.addEventListener('scroll', () => { - scrollTopBtn.classList.toggle('visible', window.scrollY > 600); - }, { passive: true }); - scrollTopBtn.addEventListener('click', () => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }); - } - - // ── Terminal typing animation ───────────────── - // Output generated by: python3 -c 'json.dumps(line)' on actual ./rayforce pipe - const TERMINAL_FRAMES = [ - { type: 'cmd', text: '$ ./rayforce' }, - { type: 'wait', ms: 300 }, - { type: 'cmd', text: '\u2023 (set t (table [Symbol Side Qty]', prompt: true }, - { type: 'cmd', text: ' (list [AAPL GOOG MSFT AAPL GOOG]' }, - { type: 'cmd', text: ' [Buy Sell Buy Sell Buy]' }, - { type: 'cmd', text: ' [100 200 150 300 250])))' }, - { type: 'wait', ms: 400 }, - { type: 'out', lines: [ - '\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510', - '\u2502 Symbol \u2502 Side \u2502 Qty \u2502', - '\u2502 sym \u2502 sym \u2502 i64 \u2502', - '\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524', - '\u2502 AAPL \u2502 Buy \u2502 100 \u2502', - '\u2502 GOOG \u2502 Sell \u2502 200 \u2502', - '\u2502 MSFT \u2502 Buy \u2502 150 \u2502', - '\u2502 AAPL \u2502 Sell \u2502 300 \u2502', - '\u2502 GOOG \u2502 Buy \u2502 250 \u2502', - '\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524', - '\u2502 5 rows (5 shown) 3 columns (3 shown)\u2502', - '\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518', - '', - ]}, - { type: 'wait', ms: 1200 }, - { type: 'cmd', text: '\u2023 (select {from:t by: Symbol Qty: (sum Qty)})', prompt: true }, - { type: 'wait', ms: 400 }, - { type: 'out', lines: [ - '\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510', - '\u2502 Symbol \u2502 Qty \u2502', - '\u2502 sym \u2502 i64 \u2502', - '\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524', - '\u2502 AAPL \u2502 400 \u2502', - '\u2502 GOOG \u2502 450 \u2502', - '\u2502 MSFT \u2502 150 \u2502', - '\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524', - '\u2502 3 rows (3 shown) 2 columns (2 shown)\u2502', - '\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518', - '', - ]}, - { type: 'wait', ms: 1200 }, - { type: 'cmd', text: "\u2023 (pivot t 'Symbol 'Side 'Qty sum)", prompt: true }, - { type: 'wait', ms: 400 }, - { type: 'out', lines: [ - '\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510', - '\u2502 Symbol \u2502 Buy \u2502 Sell \u2502', - '\u2502 sym \u2502 i64 \u2502 i64 \u2502', - '\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524', - '\u2502 AAPL \u2502 100 \u2502 300 \u2502', - '\u2502 GOOG \u2502 250 \u2502 200 \u2502', - '\u2502 MSFT \u2502 150 \u2502 0 \u2502', - '\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524', - '\u2502 3 rows (3 shown) 3 columns (3 shown)\u2502', - '\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518', - '', - ]}, - { type: 'wait', ms: 4000 }, - { type: 'clear' }, - ]; - - class TerminalAnimation { - constructor(el, frames) { - this.el = el; - this.frames = frames; - this.running = false; - } - - async start() { - if (this.running) return; - this.running = true; - while (this.running) { - this.el.innerHTML = ''; - for (const frame of this.frames) { - if (!this.running) return; - switch (frame.type) { - case 'cmd': await this.typeCommand(frame.text, frame.prompt); break; - case 'out': await this.showOutput(frame.lines); break; - case 'wait': await this.wait(frame.ms); break; - case 'clear': - await this.wait(300); - this.el.style.opacity = '0'; - await this.wait(300); - this.el.innerHTML = ''; - this.el.style.opacity = '1'; - break; - } - } - } - } - - stop() { - this.running = false; - } - - async typeCommand(text, prompt) { - const line = document.createElement('div'); - line.className = 'term-line term-cmd'; - this.el.appendChild(line); - - const cursor = document.createElement('span'); - cursor.className = 'term-cursor'; - cursor.textContent = '\u2588'; - - for (let i = 0; i < text.length; i++) { - if (!this.running) return; - // Render prompt character (‣) in green - if (prompt && text[0] === '\u2023') { - line.innerHTML = '\u2023' + - this.escapeHtml(text.slice(1, i + 1)); - } else { - line.textContent = text.slice(0, i + 1); - } - line.appendChild(cursor); - this.scrollToBottom(); - await this.wait(25 + Math.random() * 35); - } + }); + + document.querySelectorAll('[data-live-demo]').forEach((demo) => { + const label = demo.querySelector('[data-demo-label]'); + const progress = demo.querySelector('[data-demo-progress]'); + const progressTrack = demo.querySelector('[data-demo-progress-track]'); + const result = demo.querySelector('[data-demo-result]'); + const replay = demo.querySelector('[data-demo-replay]'); + const stages = Array.from(demo.querySelectorAll('[data-demo-stage]')); + const frames = [ + { stage: 'load', label: 'Loading 100,000 rows', progress: 16, delay: 1050 }, + { stage: 'optimize', label: 'Rewriting the lazy DAG', progress: 40, delay: 1250 }, + { stage: 'filter', label: 'Filtering qty > 100 · 60,000 match', progress: 68, delay: 1200 }, + { stage: 'group', label: 'Grouping in parallel by symbol', progress: 90, delay: 1150 }, + { stage: 'result', label: 'Complete · 100,000 rows → 4 groups', progress: 100, delay: 3800 } + ]; + let frameIndex = 0; + let timer = 0; + let visible = true; + + const paint = (frame) => { + label.textContent = frame.label; + progress.style.width = frame.progress + '%'; + progressTrack.setAttribute('aria-valuenow', String(frame.progress)); + stages.forEach((stage, index) => { + const activeIndex = Math.min(frameIndex, stages.length - 1); + stage.classList.toggle('is-active', frame.stage !== 'result' && index === activeIndex); + stage.classList.toggle('is-complete', frame.stage === 'result' || index < activeIndex); + }); + result.classList.toggle('is-visible', frame.stage === 'result'); + }; - cursor.remove(); - await this.wait(120); - } + const advance = () => { + if (!visible) return; + paint(frames[frameIndex]); + timer = window.setTimeout(() => { + frameIndex = (frameIndex + 1) % frames.length; + advance(); + }, frames[frameIndex].delay); + }; - escapeHtml(s) { - return s.replace(/&/g, '&').replace(//g, '>'); - } + const restart = () => { + window.clearTimeout(timer); + frameIndex = 0; + result.classList.remove('is-visible'); + advance(); + }; - async showOutput(lines) { - for (const html of lines) { - if (!this.running) return; - const line = document.createElement('div'); - line.className = 'term-line term-out'; - line.innerHTML = html; - this.el.appendChild(line); - this.scrollToBottom(); - await this.wait(35); - } - } + replay.addEventListener('click', restart); - scrollToBottom() { - this.el.scrollTop = this.el.scrollHeight; + if (reduceMotion) { + frameIndex = frames.length - 1; + paint(frames[frameIndex]); + return; } - wait(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + if ('IntersectionObserver' in window) { + const demoObserver = new IntersectionObserver(([entry]) => { + visible = entry.isIntersecting; + window.clearTimeout(timer); + if (visible) advance(); + }, { threshold: 0.15 }); + visible = false; + demoObserver.observe(demo); + } else { + advance(); } - } - - // Start terminal when scrolled into view - const terminalOutput = document.getElementById('terminal-output'); - if (terminalOutput) { - const terminal = new TerminalAnimation(terminalOutput, TERMINAL_FRAMES); - const termObserver = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - terminal.start(); - termObserver.unobserve(entry.target); - } - }); - }, { threshold: 0.3 }); - termObserver.observe(terminalOutput); - } + }); }); -/* GitHub widget: fetch live stars/forks via ungh.cc (CDN-cached proxy - * without GitHub's 60/hour unauthenticated rate limit) with a 1-hour - * localStorage cache so repeat visits don't refetch. Falls back silently - * if every source is unreachable. */ -(function () { +(function loadGitHubStats() { const targets = document.querySelectorAll('[data-gh-stat]'); - if (targets.length === 0) return; - - const REPO = 'RayforceDB/rayforce'; - const CACHE_KEY = 'gh-stats:' + REPO; - const CACHE_TTL = 60 * 60 * 1000; /* 1 hour */ - - function fmt(n) { - if (typeof n !== 'number' || isNaN(n)) return '—'; - if (n >= 10000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; - if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; - return String(n); - } - - function animateCount(el, target, duration) { - if (typeof target !== 'number' || isNaN(target)) return; - const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; - if (reduce) { el.textContent = fmt(target); return; } - const start = performance.now(); - function tick(now) { - const t = Math.min(1, (now - start) / duration); - const eased = 1 - Math.pow(1 - t, 3); /* easeOutCubic */ - el.textContent = fmt(Math.round(target * eased)); - if (t < 1) requestAnimationFrame(tick); - else el.textContent = fmt(target); + if (!targets.length) return; + + const repo = 'RayforceDB/rayforce'; + const cacheKey = 'rayforce:github-stats'; + const format = (value) => { + if (typeof value !== 'number') return '—'; + return value >= 1000 ? (value / 1000).toFixed(value >= 10000 ? 0 : 1) + 'k' : String(value); + }; + const paint = (data) => targets.forEach((el) => { + el.textContent = format(el.dataset.ghStat === 'forks' ? data.forks : data.stars); + }); + + try { + const cached = JSON.parse(localStorage.getItem(cacheKey)); + if (cached && Date.now() - cached.savedAt < 3600000) { + paint(cached); + return; } - requestAnimationFrame(tick); - } - - function paint(stars, forks) { - targets.forEach(el => { - const which = el.getAttribute('data-gh-stat'); - const target = which === 'stars' ? stars - : which === 'forks' ? forks - : NaN; - animateCount(el, target, 900); - }); - } - - function readCache() { - try { - const raw = localStorage.getItem(CACHE_KEY); - if (!raw) return null; - const v = JSON.parse(raw); - if (!v || (Date.now() - v.t) > CACHE_TTL) return null; - return v; - } catch (e) { return null; } - } - - function writeCache(stars, forks) { - try { localStorage.setItem(CACHE_KEY, JSON.stringify({ t: Date.now(), stars, forks })); } catch (e) {} - } - - /* If we have a fresh cached value, paint it instantly without a network roundtrip. */ - const cached = readCache(); - if (cached) { paint(cached.stars, cached.forks); return; } - - /* Primary: ungh.cc (CDN-cached, no rate limit). Response shape: { repo: { stars, forks, ... } } */ - fetch('https://ungh.cc/repos/' + REPO) - .then(r => r.ok ? r.json() : Promise.reject(new Error('ungh ' + r.status))) - .then(d => { - const stars = d && d.repo && d.repo.stars; - const forks = d && d.repo && d.repo.forks; - if (typeof stars !== 'number' || typeof forks !== 'number') throw new Error('ungh shape'); - writeCache(stars, forks); - paint(stars, forks); - }) - .catch(() => { - /* Fallback: direct GitHub API. Often 403s on busy IPs but free when it works. */ - return fetch('https://api.github.com/repos/' + REPO, { headers: { Accept: 'application/vnd.github+json' } }) - .then(r => r.ok ? r.json() : Promise.reject(new Error('gh ' + r.status))) - .then(d => { - writeCache(d.stargazers_count, d.forks_count); - paint(d.stargazers_count, d.forks_count); - }); + } catch (_) {} + + fetch('https://ungh.cc/repos/' + repo) + .then((response) => response.ok ? response.json() : Promise.reject(new Error('GitHub stats unavailable'))) + .then((data) => { + const stats = { stars: data.repo.stars, forks: data.repo.forks, savedAt: Date.now() }; + paint(stats); + try { localStorage.setItem(cacheKey, JSON.stringify(stats)); } catch (_) {} }) - .catch(() => { - /* Leave the static "—" placeholders. Users still get a working link. */ - }); + .catch(() => {}); })(); diff --git a/docs/javascripts/main.js b/docs/javascripts/main.js index 75eb8f2a..091f3401 100644 --- a/docs/javascripts/main.js +++ b/docs/javascripts/main.js @@ -16,17 +16,32 @@ document.addEventListener('DOMContentLoaded', () => { const navToggle = document.querySelector('.nav-toggle'); const navLinks = document.querySelector('.nav-links'); if (navToggle && navLinks) { + const closeNavigation = () => { + navLinks.classList.remove('open'); + navToggle.classList.remove('open'); + navToggle.setAttribute('aria-expanded', 'false'); + document.body.classList.remove('nav-open'); + }; + navToggle.addEventListener('click', () => { + const drawer = document.querySelector('#__drawer'); + if (drawer && drawer.checked) { + drawer.checked = false; + drawer.dispatchEvent(new Event('change', { bubbles: true })); + return; + } const isOpen = navLinks.classList.toggle('open'); - navToggle.classList.toggle('open'); + navToggle.classList.toggle('open', isOpen); navToggle.setAttribute('aria-expanded', String(isOpen)); + document.body.classList.toggle('nav-open', isOpen); + }); + + navLinks.querySelectorAll('a, label').forEach((item) => item.addEventListener('click', closeNavigation)); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') closeNavigation(); }); - navLinks.querySelectorAll('a').forEach((link) => { - link.addEventListener('click', () => { - navLinks.classList.remove('open'); - navToggle.classList.remove('open'); - navToggle.setAttribute('aria-expanded', 'false'); - }); + window.addEventListener('resize', () => { + if (window.innerWidth > 1219) closeNavigation(); }); } @@ -38,8 +53,8 @@ document.addEventListener('DOMContentLoaded', () => { updateNav(); } - // ── Release banner: pill-nav offset + dismiss ─ - // The floating pill nav (landing/about) is position:fixed and would overlap + // ── Release banner: site-nav offset + dismiss ─ + // The fixed site nav (landing/about) would overlap // the in-flow banner. rfUpdateBannerOffset() publishes the still-visible // banner height as --rf-banner-h so the nav sits just below it and rises back // as it scrolls away. Dismissal is keyed to the release tag (set on the @@ -91,28 +106,13 @@ function rfUpdateBannerOffset() { return String(n); } - function animateCount(el, target, duration) { - if (typeof target !== 'number' || isNaN(target)) return; - const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; - if (reduce) { el.textContent = fmt(target); return; } - const start = performance.now(); - function tick(now) { - const t = Math.min(1, (now - start) / duration); - const eased = 1 - Math.pow(1 - t, 3); /* easeOutCubic */ - el.textContent = fmt(Math.round(target * eased)); - if (t < 1) requestAnimationFrame(tick); - else el.textContent = fmt(target); - } - requestAnimationFrame(tick); - } - function paint(stars, forks) { targets.forEach(el => { const which = el.getAttribute('data-gh-stat'); const target = which === 'stars' ? stars : which === 'forks' ? forks : NaN; - animateCount(el, target, 900); + if (typeof target === 'number' && !isNaN(target)) el.textContent = fmt(target); }); } diff --git a/examples/rfl/market_demo.rfl b/examples/rfl/market_demo.rfl new file mode 100644 index 00000000..d892a52e --- /dev/null +++ b/examples/rfl/market_demo.rfl @@ -0,0 +1,21 @@ +; Website demo: aggregate 100,000 deterministic market rows. +; Run with: ./rayforce -t 1 examples/rfl/market_demo.rfl + +(set n 100000) +(set trades + (table ['symbol 'side 'price 'qty] + (list + (take ['AAPL 'MSFT 'NVDA 'GOOG] n) + (take ['BUY 'SELL] n) + (+ 100 (% (til n) 700)) + (+ 1 (% (* (til n) 17) 250))))) + +(set leaders + (select {from: trades + where: (> qty 100) + by: symbol + trades: (count qty) + notional: (sum (* price qty))})) + +(show leaders) +(exit 0) diff --git a/mkdocs.yml b/mkdocs.yml index 5dc11ad2..a3d9835c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,8 +74,7 @@ markdown_extensions: - footnotes - md_in_html - tables - - toc: - permalink: true + - toc - pymdownx.details - pymdownx.superfences - pymdownx.tabbed: diff --git a/overrides/about.html b/overrides/about.html index dedf5177..cf8aadc0 100644 --- a/overrides/about.html +++ b/overrides/about.html @@ -75,22 +75,13 @@ {# Reuse the marketing footer from home.html so About matches the landing. #} {% block footer %}

{% endblock %} diff --git a/overrides/home.html b/overrides/home.html index 7477b533..661e6762 100644 --- a/overrides/home.html +++ b/overrides/home.html @@ -1,372 +1,309 @@ -{#- - Rayforce home — custom landing template. - - Ports the marketing landing (formerly website/index.html) into the - mkdocs build so the site can be served from a single source. This - template extends Material's main.html and overrides the layout blocks - the docs use (tabs, sidebar, content, footer) with the marketing - hero / why / features / architecture / demo / CTA sections. - - The pill nav at the top stays — it's injected via the regular header - override (overrides/partials/header.html), so landing and docs share - one nav. Landing-only styles and JS load via the styles/scripts - blocks below; everything else stays on the docs bundle. --#} {% extends "main.html" %} -{# Flag the document early (before renders) so landing-only CSS - can scope itself without FOUC. The class is set on so it's - available before body exists. CSS uses `html.landing-page` below. #} {% block extrahead %} - + {% endblock %} -{# Hide the Material tab bar on the landing page — the pill nav is enough. #} -{% block tabs %} - {{ super() }} - -{% endblock %} +{% block tabs %}{% endblock %} -{# Replace the entire docs container (sidebar + TOC + content) with the - marketing landing sections. Wrapped in body.landing-page class via JS - below so we can scope landing-only CSS without touching base.html. #} {% block container %} -
- - -
-
-
-
-
-
-
-
-
PURE C · ZERO DEPS · GRAPH + ANALYTICS
-

Columnar analytics and
graph traversal in one pipeline

-

Rayforce is a zero-dependency embeddable engine where columnar analytics and graph operations share a single operation DAG, pass through a multi-pass optimizer, and execute as fused morsel-driven bytecode. Pure C. No malloc.

-
- {# Primary CTA. Defaults to the releases page (works with no JS); - landing.js detects the OS and rewrites href + label to the - matching binary asset of the latest release. #} - - - Download - - - Docs - - - GitHub -
- +
+
+ + +
+
+

Open source · Pure C · Zero dependencies

+

Analytics and graphs.
One fast engine.

+

Rayforce fuses columnar analytics, graph traversal, and recursive queries into one embeddable execution pipeline—built for teams that measure latency in microseconds, not meetings.

+ - - + +
- {# Three display-sized stats — replaces the older 4-tile icon - grid. Reads as a single confident sentence rather than a - feature dashboard. Pass count matches the why-card list - below (type inference, constant folding, SIP, factorize, - predicate pushdown, filter reorder, fusion, DCE) — keep - these in sync if the optimizer chain changes. #} -
-
- 16K - Lines of C +
+
+ + market_demo.rfl + real engine run
-
- 0 - External Dependencies +
+
; 100,000 deterministic market rows
+(select {from: trades
+         where: (> qty 100)
+         by: symbol
+         trades: (count qty)
+         notional: (sum (* price qty))})
-
- 8 - Optimizer Passes -
-
-
-
- - -
-
-
- -

Analytics and graphs shouldn't need separate tools

-
- -
-
-
THE RAYFORCE WAY
-
    -
  • Single pipeline for analytics + graph
  • -
  • One optimizer understands both
  • -
  • Fused execution — no serialization overhead
  • -
  • Zero dependencies — embed in any C project
  • -
-
- -
-
TODAY'S APPROACH
-
    -
  • Separate tools for analytics and graphs
  • -
  • Two query languages, two data models
  • -
  • Serialize between tools on every step
  • -
  • Heavy Python dependencies for simple ops
  • -
+
+
+ Loading 100,000 rows + +
+
+
    +
  1. Load100K rows
  2. +
  3. Optimizerewrite DAG
  4. +
  5. Filter60K match
  6. +
  7. Group4 symbols
  8. +
- -
-
MULTI-PASS OPTIMIZER
-

Type inference, constant folding, sideways information passing, factorize, predicate pushdown, filter reorder, fusion, and DCE — all in one pass over the DAG before a single element is touched.

+
+
symboltradesnotional
+
NVDA15,0001,181,242,500
+
GOOG15,0001,191,143,000
+
AAPL15,0001,176,404,200
+
MSFT15,0001,185,774,600
- -
-
MORSEL EXECUTION
-

1024-element morsel-driven bytecode. Element-wise ops fuse into single-pass pipelines. Thread-local arenas keep allocation off the critical path.

+
+ Recorded with ./rayforce -t 1 + Run Rayforce live ↗
+
+
-
-
CSR GRAPH ENGINE
-

Double-indexed compressed sparse rows. Forward + reverse indices, mmap-friendly persistence, and algorithms from BFS to worst-case optimal joins.

-
+
- -
-
- -

Six pillars, one engine

-

Everything you need for columnar analytics, graph traversal, and an interactive query language in a single embeddable library.

- -
-
-
- -

Fused Execution

-
-

Morsel-driven bytecode over 1024-element chunks. Element-wise ops fused into single-pass pipelines that stay L1-resident.

-
+
+
+
+

One execution model

+

Stop moving data
between engines.

+

Relational operators and graph traversals belong in the same plan. Rayforce sees the whole workload, rewrites it together, and keeps the hot path close to the metal.

+ Explore the execution pipeline +
-
-
- -

Graph Engine

-
-

Double-indexed CSR storage. BFS, Dijkstra, A*, PageRank, Louvain, LFTJ — all in the same DAG.

-
+
+ +
+ 01 +

Compose

Tables + graphs + rules

+ Lazy DAG +
+
+ 02 +

Optimize

Rewrite the whole plan

+ Multi-pass +
+
+ 03 +

Fuse

Remove intermediates

+ Bytecode +
+
+ 04 +

Execute

Stream cache-sized morsels

+ Parallel +
+
+
-
-
- -

Rayfall Language

-
-

Lisp-like query REPL with rich builtins. Lambdas compile lazily to bytecode. select/update bridge to the DAG optimizer.

-
+
+
16Klines of focused C
+
0external dependencies
+
IPCclient/server transport built in
+
MITlicensed and embeddable
+
+
-
-
- -

Multi-Pass Optimizer

-
-

Type inference → constant folding → SIP → factorize → predicate pushdown → filter reorder → fusion → DCE.

-
+
+
+
+

Rayfall language

+

Ask complex questions.
Keep the syntax small.

+
+

Use Rayfall interactively, embed the C API, or connect through a growing set of client interfaces. Every surface reaches the same optimizer and execution core.

+
-
-
- -

Custom Allocator

-
-

Buddy allocator with slab cache. Thread-local arenas, lock-free allocation, COW ref counting. No malloc.

+
+
+ + + + One engine, three surfaces +
+
+
+
; Aggregate high-value flow, then traverse counterparties
+(set flow
+  (select {from: trades
+           where: (> Notional 1000000)
+           by: Counterparty
+           Volume: (sum Notional)}))
+
+(.graph.var-expand network flow 1 3)
+ +
- -
-
- -

Zero Dependencies

+
+
EXAMPLE RESULT5 rows · optimized
+
+
CounterpartyDepthVolume
+
LYNX_02184.20M
+
ALPHA_17261.08M
+
NODE_08244.71M
+
FORT_04327.19M
+
EDGE_12312.42M
-

Pure C, single public header. Builds with make. ~16K lines of engine code. Embeds into any C/C++ project.

- -
-
- -

Build, optimize, execute

-

Data flows from lazy DAG construction through a multi-pass optimizer into fused morsel-driven bytecode execution.

- -
- - - - - - - - - CSV / Tables - - CSR Graphs - - Rayfall - SOURCES - - - - - - - - RAYFORCE ENGINE - - scan - - filter - - group - - join - - optimizer - - fusion - - bytecode - EXECUTION - - morsel 1024 - - graph ops - - LFTJ - - - - - - - - DataFrames - - Paths - - Aggregates - RESULTS - +
+
+
+

Built as one system

+

Small footprint.
Serious machinery.

+

Everything you need to move from raw columns to connected answers—without assembling a second platform around it.

-

Lazy DAG nodes → multi-pass optimizer (type inference, SIP, factorize, predicate pushdown, fusion, DCE) → fused morsel-driven executor with register-slot bytecode. Graph algorithms run in the same pipeline — no serialization between analytics and traversal.

+
- -
-
- -

From zero to query in seconds

-

Start the REPL, build tables, aggregate, and pivot — all in one session.

+
+
+
+

In production now

+

Built for real work,
not synthetic demos.

+

Rayforce is already part of production systems at teams working with trading, market connectivity, investment analytics, and risk.

+

Production usage confirmed by the Rayforce project. Customer names link to their public company sites.

+
-
- -
-
-
-

Start building in
30 seconds

-
-
- - - -
-
$ make && ./rayforce
- (pivot t 'Symbol 'Side 'Qty sum)
+
+ + +
+
+

Rayforce Cloud Coming soon

+

The engine you can embed.
The platform you won’t have to operate.

+

We’re bringing Rayforce’s unified analytics and graph pipeline to a managed cloud experience—so teams can move from local prototype to production workload without rebuilding the data path.

+
+ Follow development + No launch date announced yet.
- + +
+
RAYFORCE / CLOUDPREVIEW
+
+

Deployment

eu-westReady
+ +
QUERY SERVICEPreview
ENGINEManaged
-
- - - +
+
+

Open at the core

+

Read every line.
Own every workload.

+

One codebase. One public header. No external runtime. Clone Rayforce, compile it, embed it, and keep control of your data path.

+ +
+
GitHub stars
+
forks
+
Linux · macOSsupported today
+
+
+
+
{% endblock %} -{# Replace Material's footer with the marketing footer so the landing - matches the design from website/index.html exactly. #} {% block footer %} {% endblock %} -{# Load landing-only JS (terminal demo, scroll fade-ins) after the - regular docs bundle so the GitHub widget counter still hooks up. #} {% block scripts %} {{ super() }} diff --git a/overrides/main.html b/overrides/main.html index c81061a7..00caf383 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -1,5 +1,15 @@ {% extends "base.html" %} +{% block site_meta %} + {{ super() }} + + + + + + +{% endblock %} + {# Rayforce docs — Material override entry point. @@ -13,20 +23,86 @@ announce slot (its .md-banner wrapper is flattened to display:contents in extra.css so only .rf-banner is styled). Static text works with no JS; main.js fills the live version ("Rayforce vX.Y.Z is out"), rewrites the Download link to - the OS-matched binary, offsets the floating pill nav below it, and handles + the OS-matched binary, offsets the fixed site nav below it, and handles dismissal — keyed to the release tag, so a NEW release re-shows the banner. #} {% block announce %}
- - - Rayforce — latest release +
+ Release + Rayforce — latest release Download - - + +
+ + {% endblock %} diff --git a/overrides/partials/header.html b/overrides/partials/header.html index 316d4a32..6dc07261 100644 --- a/overrides/partials/header.html +++ b/overrides/partials/header.html @@ -1,98 +1,73 @@ -{#- - Rayforce header — conditional. - - • Landing page (page.url == ''): renders the floating-pill marketing - nav (Features / Architecture / Docs / Live Demo / About + GitHub - widget) so the home page matches the marketing brand. - - • Docs pages: renders Material's standard `.md-header` (logo + page - title + palette toggle + search + repo button) so docs pages stay - focused on docs UX — drawer toggle on mobile, search at the top - right, no marketing chrome. - - The pill-nav HTML is a verbatim port of website/index.html's