Execution checklist
| ID | Task | Deliverables | Verification | State |
|---|---|---|---|---|
| T8 | Replace the Mermaid schema source with dbspec, a neutral schema language that is the source of truth of a schema. Priority: first. Cause: the Mermaid dialect cannot express physical schemas losslessly; field names select behavior; generated names are rewritten; dialect features drop silently; hidden markers restore triggers and .sql sources. | Acceptance: T8.0–T8.8 complete in Go/PHP/Rust/TypeScript with conformance and MySQL/PostgreSQL/SQLite evidence; work runs in worktree orm-dbspec-T8 and merges into main only after every item passes. | Evidence: every T8 item is complete and main holds the dbspec line (T8.8). | [o] |
| T8.0 | Record every schema syntax and semantic difference between MySQL, PostgreSQL and SQLite and decide neutral support per feature (requires T8). Cause: dbspec supports only features with one common definition, the same meaning on all three databases, and restoration by introspection; that rule needs complete facts. | Acceptance: docs/spec/dialects.md and its Korean pair cover types, length/precision, null, defaults, identity, generated columns, keys, indexes, FKs, CHECKs, comments, collation, namespaces and identifier rules, time types and time zones, enum, unsigned, triggers, DDL transactions and catalog sources; each row states the syntax and meaning per database, the neutral definition and rendering or the unsupported reason, and the introspection source; each decision is verified by tracked shared tests/* cases against the .runtime databases. | Evidence: make dialect-facts-check TEST_ENV=<main checkout>/.runtime/servers/env ran twice with no failure: 239 probes (MySQL 8.4.11: 98, PostgreSQL 17.11: 75, SQLite 3.53.4 through modernc v1.58.0: 66) in 1.705 s and 1.637 s, plus four SQLite file-name cases each time; every probe drops and verifies its own database, schema or file. docs/spec/dialects.md and its Korean pair record per feature the syntax and meaning on each database, probe IDs, official sources, the neutral rendering or unsupported reason, and the introspection source, and list the SQLite library of each client (Go, PHP, Node 3.53.4; Rust bundled 3.51.3, not probed). dbspec supports local datetime(p) with a UTC connection rule (instants unsupported; timestamptz columns are converted, not mapped) and executor update-time stamping in manifestHash; the audit context definition is left for T8.1 review. Defects found against the facts are T8.0.1–T8.0.11, T8.0.13 and T8.0.14. checklist-check, docs-rules-check, docs-check and docs-verify-idempotent pass. | [o] |
| T8.0.1 | Restore PostgreSQL column import per schema (requires T8.0). Cause: the column query joins pg_class by relname alone (internal/ormgen/import.go:589, clients/php/src/SchemaImport.php:271, clients/rust/orm-build/src/catalog/reader.rs:204, clients/typescript/src/tools/introspect.ts:135), but a table name is unique per schema only (postgres.namespace.relname_per_schema), so a relation with the same name in another schema repeats each imported column; the import itself was not executed because PostgreSQL was unavailable. | Acceptance: T8.4 joins by relnamespace, and a tracked case with a same-named table in a second schema imports one column set in every client. | Evidence: the dbspec introspection reads columns, constraints, indexes and triggers only where relnamespace is the current schema, so columns were never repeated, but a foreign key read its referenced table by relname alone. tests/dbspec/introspect.json gains postgres-other-schema: a same-named orders table with another column, a check, a unique key and an index in a second schema {schema}_b, and a local lines table whose foreign key references it; the four harnesses replace {schema} with the case's schema and drop the second schema. Before the fix Go, PHP, TypeScript and Rust failed it with "document differs", restoring foreign key fk_lines_order (order_id) references orders (id); each client now reads the referenced table only in the current schema and reports the key as unsupported, and make dbspec-introspect-check, dbspec-introspect-php-check (30 round trips, 8 unsupported cases), dbspec-introspect-ts-check (41 tests) and dbspec-introspect-rust-check pass with one column set of orders. docs/spec/dialects.md states the rule and the placeholder. | [o] |
| T8.0.2 | Report index features that import drops or rejects (requires T8.0). Cause: observed with ormgen import: a MySQL functional index and a SQLite expression index abort import with converting NULL to string is unsupported (internal/ormgen/import.go:194-196, internal/ormgen/migrate.go:285-287); a MySQL prefix index and a SQLite partial index are imported as full indexes (import.go:187-207, migrate.go:263-283); the PostgreSQL reader joins expression keys (indkey 0) to pg_attribute and reads neither indpred nor indoption (import.go:653-691; not executed). | Acceptance: T8.4 reports prefix, partial and expression indexes as unsupported objects with locations, with tracked cases on their databases in every client. | Evidence: the dbspec introspection replaces the import path. docs/spec/dialects.md "Keys, indexes and foreign keys" makes prefix, partial, expression and full-text indexes unsupported, and the PostgreSQL index query reads indpred, indexprs and indoption instead of joining expression keys to pg_attribute. tests/dbspec/introspect.json asserts them as [index, table, name] entries of exact unsupported lists: mysql-unsupported the prefix index ix_name_prefix and the functional index ix_name_expr, postgres-unsupported the expression index ix_lower and the partial index ix_partial, sqlite-unsupported the expression index ix_lower and the partial index ix_partial (MySQL has no partial index, mysql.index.partial_rejected); none aborts introspection, and the descending placed_at desc key of the ddl.json round trip is restored on each database. make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check and dbspec-introspect-rust-check pass these cases in Go, PHP, TypeScript and Rust. | [o] |
| T8.0.3 | Report generated columns instead of importing plain columns (requires T8.0). Cause: observed: MySQL b int AS (a+1) STORED is imported as a plain nullable int because GENERATION_EXPRESSION is not read (internal/ormgen/import.go:130-131); SQLite pragma_table_info omits a generated column, so import drops it (internal/ormgen/migrate.go:225). | Acceptance: T8.4 reports generated columns as unsupported with locations on all three databases in every client. | Evidence: the dbspec introspection reads MySQL GENERATION_EXPRESSION, PostgreSQL attgenerated and SQLite pragma_table_xinfo.hidden and reports a generated column as an unsupported column (docs/spec/dialects.md "Null, defaults, identity and generated columns"). tests/dbspec/introspect.json now asserts it on all three databases with a type that dbspec would otherwise read: MySQL next_id bigint AS (char_length(name) + 1) STORED and name_size bigint AS (char_length(name)) VIRTUAL in mysql-unsupported, PostgreSQL next_id bigint GENERATED ALWAYS AS (id + 1) STORED in postgres-unsupported, and SQLite s INTEGER … STORED beside the virtual g in sqlite-unsupported; each is listed as [column, orders, <name>] and left out of the document. make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check and dbspec-introspect-rust-check pass these cases in Go, PHP, TypeScript and Rust. | [o] |
| T8.0.4 | Reject names over the identifier limits instead of rendering them (requires T8.0). Cause: observed: a 65-character %% index name is rendered unbounded as a MySQL KEY and MySQL rejects the DDL (1059) (internal/ormgen/ddl.go:138, fulltext ddl.go:141); trigger and function names append suffixes to the table name without a bound (internal/ormgen/triggers.go:61-66, 101-102, 165-167), rendering a 77-character MySQL trigger name and a 70-character PostgreSQL trigger name, which PostgreSQL truncates to 63 bytes (postgres.ident.truncated_to_63). | Acceptance: T8.3 rejects a declared or generated name over 63 bytes before rendering, with tracked cases in every client. | Evidence: docs/spec/dbspec.md "Names" limits every declared name and every name the renderer writes (<table>$<column>, <table>$immutable_update, <table>$audit_insert and the others) to 63 bytes and makes a longer one a name.length error, so a set with such a name has no statements. tests/dbspec/cases.json asserts it with names of exactly 64 bytes: name-too-long (a 64-byte column name), generated-check-name-too-long (a 64-byte renderer CHECK name), generated-immutable-name-too-long (a 64-byte immutable trigger name) and generated-audit-name-too-long (a 64-byte audit trigger name), each with its line, column and rule. make dbspec-go-check, php clients/php/tests/dbspec_test.php, node --test of clients/typescript/tests/dbspec.mjs, dbspec-rules.mjs and dbspec-render.mjs (168 tests), make dbspec-rust-check and make dbspec-compare-check (8 runs agree on 116 cases) pass them in Go, PHP, TypeScript and Rust. | [o] |
| T8.0.5 | Record the same audit changes when a foreign key action changes rows (requires T8.0). Cause: MySQL fires no trigger for rows changed by a foreign key action (mysql.fk.cascade_skips_triggers) while PostgreSQL and SQLite do (postgres.fk.cascade_fires_triggers, sqlite.fk.cascade_fires_triggers), so mode=changes audit triggers (internal/ormgen/triggers.go:378-437) record cascaded changes on PostgreSQL and SQLite only. | Acceptance: T8.3 rejects an audit or immutable setting on a child table of a cascade or set_null foreign key, with a tracked case on all three databases in every client. | Evidence: docs/spec/dbspec.md "Settings" and "Audit" reject immutable and audit on a child of a cascade or set_null foreign key with a setting error at the setting keyword, so no statement of any dialect is rendered for such a table and the cascaded rows that MySQL triggers skip (mysql.fk.cascade_skips_triggers, while postgres.fk.cascade_fires_triggers and sqlite.fk.cascade_fires_triggers fire) never meet a generated trigger. The rejection precedes rendering, so it does not depend on the database. tests/dbspec/cases.json gains immutable-on-cascade-child (on delete cascade) and audit-on-set-null-child (on update set_null), each expecting one setting error at its line and column; they pass in make dbspec-go-check, php clients/php/tests/dbspec_test.php (81 invalid cases), node --test of clients/typescript/tests/dbspec.mjs, dbspec-rules.mjs and dbspec-render.mjs, make dbspec-rust-check and make dbspec-compare-check, where the four clients agree on them. | [o] |
| T8.0.6 | Make the immutable guard equal for statements that match no row (requires T8.0). Cause: the PostgreSQL guard is a FOR EACH STATEMENT trigger (internal/ormgen/triggers.go:110, clients/php/src/SchemaTriggers.php:101, clients/rust/orm-schema/src/triggers.rs:112, clients/typescript/src/engine/triggers.ts:81) that fails an UPDATE or DELETE matching no row (postgres.trigger.statement_fires_on_zero_rows), while the MySQL and SQLite row guards do not fire (mysql.trigger.zero_rows_not_fired, sqlite.trigger.zero_rows_not_fired); it also covers TRUNCATE on PostgreSQL only. | Acceptance: T8.3 renders row-level UPDATE and DELETE guards on all three databases, and a tracked zero-row case passes on all three in every client. | Evidence: docs/spec/dialects.md "Triggers" renders immutable and the audit delete rejection as BEFORE UPDATE and BEFORE DELETE FOR EACH ROW triggers on MySQL, PostgreSQL and SQLite, and no TRUNCATE trigger on any of them, as the statements of the immutable-and-audit vector of tests/dbspec/ddl.json show. That vector gains zero-row steps: UPDATE ledger SET amount = 7 WHERE id = 999 and DELETE FROM ledger WHERE id = 999 on the immutable table and DELETE FROM service WHERE id = 999 on the audited table must succeed, after the matching UPDATE and DELETE failed (MySQL 1644, PostgreSQL P0001, SQLite "table ledger is immutable"). make dbspec-ddl-check runs the 13 steps on MySQL 8.4, PostgreSQL 17 and SQLite and passes; the four clients render these statements byte for byte (make dbspec-go-check, php clients/php/tests/dbspec_render_test.php, clients/typescript/tests/dbspec-render.mjs, make dbspec-rust-check, and make dbspec-compare-check, where they agree). | [o] |
| T8.0.7 | Keep the automatic key valid after an explicit key on PostgreSQL (requires T8.0). Cause: GENERATED BY DEFAULT AS IDENTITY (internal/ormgen/ddl.go:539, clients/php/src/SchemaDdl.php:542, clients/rust/orm-schema/src/ddl.rs:230, clients/typescript/src/engine/ddl.ts:163) does not advance past an explicit key, so the next generated key fails with 23505 (postgres.identity.by_default_not_advanced), while MySQL and SQLite continue (mysql.auto_increment.explicit_value_advances, sqlite.autoincrement.explicit_value_advances). | Acceptance: clients reject an explicit automatic key and PostgreSQL apply steps that write explicit keys reset the identity sequence, with tracked cases on all three databases in every client. | Evidence: dbspec renders PostgreSQL GENERATED BY DEFAULT AS IDENTITY (docs/spec/dialects.md "Automatic key"), and every client already rejects an explicit automatic key before any statement runs: the request validators reject a primary key or identity column in on_duplicate ("on_duplicate cannot assign"), and the planners reject an identity column in an insert ("cannot set identity column") and a primary key or identity column in an update ("cannot update"), so no client writes a key past the PostgreSQL sequence. No test asserted the insert and update rules. New tracked cases run the four requests on the mysql, postgres and sqlite dialects and require IR_INVALID with the message: Go TestKeyColumnsAreNotWritten (engine/query_forms_test.go), PHP clients/php/tests/engine_test.php, TypeScript clients/typescript/tests/engine.mjs and Rust engine::tests::key_columns_are_not_written; all pass, and the PHP and TypeScript cases fail on all three dialects when the expected message is changed. No plan statement writes explicit keys on PostgreSQL (docs/spec/plans.md copies rows only in the SQLite rebuild), so there is no PostgreSQL apply step to reset; the SQLite rebuild resets its own counter, recorded as T8.5.1.1. docs/spec/dsl.md states the write rule. | [o] |
| T8.0.8 | Render a binary collation for MySQL text columns (requires T8.0). Cause: DDL ends each MySQL table with DEFAULT CHARSET=utf8mb4 (internal/ormgen/ddl.go:146, clients/php/src/SchemaDdl.php:134, clients/rust/orm-schema/src/ddl.rs:564, clients/typescript/src/engine/ddl.ts:360), which selects utf8mb4_0900_ai_ci (mysql.collation.default_accent_case_insensitive), so MySQL unique keys and comparisons treat a, A and á as equal while PostgreSQL C and SQLite BINARY do not. | Acceptance: T8.3 renders utf8mb4_0900_bin and PostgreSQL COLLATE "C", with a tracked unique-key case on all three databases in every client. | Evidence: docs/spec/dialects.md "Inside CREATE TABLE" renders MySQL varchar(n) and text with CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin and the table with COLLATE=utf8mb4_0900_bin, PostgreSQL text columns with COLLATE "C", and SQLite text with its BINARY default, as the statements of tests/dbspec/ddl.json show. The keys-indexes-foreign-keys vector gains a unique-key case on orders.code: a, A, á and a (trailing space) are inserted as four rows, a second a fails (MySQL 1062, PostgreSQL 23505, SQLite UNIQUE), code = 'a' matches one row, and A sorts before a and á; under the former utf8mb4_0900_ai_ci the A insert would fail (mysql.collation.default_accent_case_insensitive) and under a PAD SPACE collation a would (mysql.collation.utf8mb4_bin_pads). make dbspec-ddl-check runs the 15 steps on MySQL 8.4, PostgreSQL 17 and SQLite and passes; the four clients render these statements byte for byte (go test ./engine/dbspec -run TestRenderVectors, php clients/php/tests/dbspec_render_test.php, clients/typescript/tests/dbspec-render.mjs, make dbspec-rust-check, and make dbspec-compare-check, where they agree), and introspection reads the collations back (make dbspec-introspect-check round trips). | [o] |
| T8.0.9 | Render datetime(p) as a local date-time with the UTC connection rule (requires T8.0). Cause: datetime renders as MySQL DATETIME, PostgreSQL timestamp with time zone and SQLite text (internal/ormgen/ddl.go:487 and the client ports), which introspection cannot restore and which docs/spec/dialects.md now rejects; the SQLite =now default writes UTC (ddl.go:553, sqlite.datetime.now_is_utc) while the dialect rules state that SQLite date-times use the connection time zone. | Acceptance: T8.3 renders MySQL DATETIME(p), PostgreSQL timestamp(p) and SQLite text with its renderer CHECK; every client sets UTC on each connection; introspection reports timestamptz and MySQL TIMESTAMP as unsupported; tracked cases run on all three databases in every client. orm reports an existing timestamptz column as unsupported and does not convert it. | Evidence: tests/dbspec/ddl.json renders datetime(p) as MySQL DATETIME(p), PostgreSQL timestamp(p) and SQLite text in every client (T8.3, make dbspec-ddl-check); tests/dbspec/introspect.json reports a MySQL timestamp(6) column and a PostgreSQL timestamptz column as unsupported in every client (make dbspec-introspect-check and its PHP, TypeScript and Rust variants); T8.0.9.1 sets UTC on every Go, PHP, TypeScript and Rust connection and conformance reports 25 vectors × 4 languages identical on the three databases. | [o] |
| T8.0.9.1 | Read and write every Go, TypeScript and Rust connection in UTC (follows T8.0.9). Cause: the PHP client sets MySQL time_zone = '+00:00' and PostgreSQL TIME ZONE 'UTC' on every connection and accepts only UTC or +00:00 as timezone, but the Go, TypeScript and Rust clients accept any zone and use the server or process zone without the parameter, so the MySQL and PostgreSQL clock defaults write the session wall clock (KST on the test servers) and Go binds a PostgreSQL timestamp from a value with an offset whose offset PostgreSQL drops (postgres.timestamp.ignores_offset). | — | Evidence: the Go, TypeScript and Rust clients now follow the rule that PHP already kept: MySQL connections set time_zone = '+00:00', PostgreSQL connections TimeZone=UTC, values are read in UTC, a value with an offset is bound as its UTC wall clock (Go no longer hands PostgreSQL a time.Time whose offset timestamp drops), and timezone accepts only UTC or +00:00 with CONFIG "dsn timezone <value>: every connection reads and writes datetime values in UTC" otherwise; the named-zone code, the MySQL 1298 mapping and the chrono-tz dependency are removed. Red on the test servers (MySQL SYSTEM = KST, PostgreSQL Asia/Seoul): Go TestParseDSN and TestParseDSNRejectsInvalidInput failed on 5 and 3 cases, and TestConnectionsUseUTC read 2026-01-02 00:00:00 +0900 KST for the instant 2026-01-02 00:00+09:00 on SQLite, MySQL and PostgreSQL without the parameter and 2026-01-02 00:00:00 +0000 UTC on PostgreSQL with timezone=UTC; TypeScript clients/typescript/tests/dsn.mjs accepted timezone=%2B09:00 and clients/typescript/tests/model.mjs (connectionsUseUtc) read start_dt 2026-01-02 00:00:00.000000 and a MySQL created_ts nine hours ahead; Rust connections_use_utc and non_utc_time_zones_are_rejected failed. Green: those tests pass with the parameter absent, UTC and +00:00, the PHP connection time zone cases and clients/php/tests/dsn.php pass, go test ./clients/go/orm ./clients/go/model passes, the Rust zone, value_decode and sqlite_file_name tests pass, clippy and tsc pass, and the conformance runs on MySQL, PostgreSQL and SQLite report 25 vectors × 4 languages identical. docs/spec/config.md, interfaces.md, dsl.md, protocol.md, dialects.md, features.md, interface-implementation.md and usage.md state the rule. | [o] |
| T8.0.9.2 | Read SQLite DSN paths percent-decoded in every client (follows T8.0.9.1). Cause: the Rust parse_dsn reads url.path() without percent-decoding, so a sqlite:// path with a space or another escaped character opens a different file. | Acceptance: shared DSN cases with escaped paths open the same file in Go, PHP, TypeScript and Rust. | Evidence: docs/spec/config.md "Runtime connection" states the rule: a SQLite DSN path is percent-decoded as UTF-8 before the file opens (%25, %23 and %3F give %, # and ?), and a % without two hexadecimal digits, a decoded NUL byte or a decoded path that is not UTF-8 returns CONFIG. tests/dsn/sqlite-paths.json holds 11 shared cases (plain, space, escaped percent, hash and question mark, UTF-8, a space with a query, and four rejected paths); each client connects through its public connect to sqlite://<new directory>/<path> and requires exactly the named file (with its journal files) or the error and an empty directory: Go TestSQLitePathCases (clients/go/orm/sqlite_path_test.go), PHP clients/php/tests/dsn.php, TypeScript clients/typescript/tests/dsn.mjs and Rust sqlite_path_cases (clients/rust/orm/tests/sqlite_file_name.rs). Red: Go decoded the path and handed it to SQLite as a file: URI, which decoded it again and cut it at #, ? and NUL, so escaped-percent opened a b.sqlite, escaped-hash, escaped-question-mark and nul opened a, and invalid-utf8 failed with unable to open database file (14) instead of CONFIG; PHP opened the literal a%20b.sqlite for space; TypeScript threw URIError: URI malformed instead of CONFIG for invalid-escape; Rust failed 10 of 11 cases, opening every escaped path under its literal name and accepting the four invalid paths. Green: Go rejects NUL and non-UTF-8 paths and escapes the decoded path into the file: URI, PHP validates and rawurldecodes the path, TypeScript decodes it in one function that maps a decode failure and NUL to CONFIG, and Rust decodes it in sqlite_path; the four tests pass all 11 cases. go test ./clients/go/orm, cargo test -p orm --lib --test sqlite_file_name --test zone and cargo test -p orm-build --features cli --test tool_limits pass. Gates passed: client-unit-check, ts-check, rust-check, rust-fmt-check, go-fmt-check, interface-check, docs-rules-check and checklist-check. | [o] |
| T8.0.10 | Import SQLite primary key nullability as declared (requires T8.0). Cause: observed: ormgen import writes k TEXT PRIMARY KEY as NOT NULL (internal/ormgen/migrate.go:238), but SQLite accepts NULL in that column (sqlite.pk.nullable_non_integer). | Acceptance: T8.4 reads notnull without inferring it from pk, with a tracked SQLite case in every client. | Evidence: the dbspec introspection reads SQLite pragma_table_xinfo.notnull for every column and never infers it from pk (docs/spec/dialects.md "Types and columns", sqlite.pk.nullable_non_integer). tests/dbspec/introspect.json gains sqlite-primary-key-null: table kp declares "k" varchar(8) NULL as its primary key in renderer form and is reported as [table, kp, kp] (TypeScript reason "key: primary key column k is null", because a dbspec key column cannot be null), while table kq with "k" varchar(8) NOT NULL is restored as k varchar(8) with primary key (k). make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check and dbspec-introspect-rust-check pass the case in Go, PHP, TypeScript and Rust. | [o] |
| T8.0.11 | Report catalog types and actions that import maps to another definition (requires T8.0). Cause: PostgreSQL timestamp and timestamptz, time and timetz, char(n) and varchar(n), real and double precision, and json/jsonb and text are each imported as one type (internal/ormgen/import.go:845-877) although their stored values differ (postgres.char.padded, postgres.float.real_single_precision, postgres.json.text_jsonb_normalized); foreign key update actions are not read and RESTRICT, NO ACTION and SET DEFAULT are imported as the default (import.go:518-527; observed: MySQL ON UPDATE CASCADE is dropped). | Acceptance: T8.4 restores each supported definition exactly and reports the rest as unsupported, with tracked cases in every client. | Evidence: the dbspec introspection maps only the catalog forms of docs/spec/dialects.md "Types and columns" and reads both foreign key actions, accepting only RESTRICT, CASCADE and SET NULL. The round trips of make dbspec-introspect-check restore on delete set_null on update cascade and on delete cascade on update restrict on MySQL, PostgreSQL and SQLite, so a MySQL ON UPDATE CASCADE is kept. tests/dbspec/introspect.json now reports the other definitions as unsupported: PostgreSQL timestamptz (at), timetz (tz), char(4) (ch), real (r), json (j) and jsonb (jb) and the keys fk_refs_a (ON DELETE NO ACTION) and fk_refs_b (ON UPDATE SET DEFAULT); MySQL timestamp(6) (ts), char(4) (ch), float (fl), json (doc), the NO ACTION key fk_c and the SET DEFAULT key fk_refs_b; SQLite the keys fk_refs_a (NO ACTION) and fk_refs_b (SET DEFAULT), while the rest of each table (refs with its columns, key and index) is restored. make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check and dbspec-introspect-rust-check pass these cases in Go, PHP, TypeScript and Rust. | [o] |
| T8.0.12 | Complete the PostgreSQL probe evidence (requires T8.0). Cause: during T8.0 the PostgreSQL primary and replica of TEST_ENV stopped because their data directories were configured at a removed path; the servers have since been restarted at the current path by their operator, and BENCH_SQLITE_DSN now names the current path. | — | Evidence: with the restarted servers all 75 PostgreSQL probes pass twice, including postgres.index.varchar_640_four_byte_fits, postgres.render.time_of_day_check and the COLLATE "C" catalog step of postgres.collation.c_codepoint, which had not executed before; no PostgreSQL fact differed from the record. | [o] |
| T8.0.13 | Find the opener that created a SQLite file named with its DSN query (requires T8.0). Cause: the previous .runtime/servers held an empty file orm_bench.sqlite?_pragma=busy_timeout(5000)&timezone=%2B00:00 (dated 2026-09-26), so some program opened BENCH_SQLITE_DSN with its query in the file name. TestSQLiteFileNameWithQuery reproduces the mechanism: PHP PDO, node:sqlite and the sqlite3 shell create that literal file name, while modernc.org/sqlite parses the query. The current Go, PHP, TypeScript and Rust conformance runners and the Go, PHP and TypeScript import tools, run against a copy of the bench file with the same DSN form, created no such file, so the code path was not found in the current tree. | Acceptance: each client's DSN tests and each tool that opens a SQLite DSN assert that only the path names the file; a found opener is corrected with a tracked RED case. | Evidence: no current opener names a SQLite file with its DSN query, and each one now asserts it. Every case opens sqlite://<temporary directory>/named.sqlite?_pragma=busy_timeout(5000)&timezone=%2B00:00, the form of the observed file name, writes or connects, closes, and requires that the directory holds named.sqlite and no name other than named.sqlite and its -journal, -wal and -shm files. The client openers: Go TestSQLiteFileNameIsThePath (clients/go/orm/sqlite_file_name_test.go, orm.Connect and schema install), PHP clients/php/tests/dsn.php (Orm::connect), TypeScript clients/typescript/tests/dsn.mjs (Db.connect) and Rust clients/rust/orm/tests/sqlite_file_name.rs (Db::connect, which bench/install and the Rust tool database also use through the Go and Rust clients). The schema tools that open a DSN themselves: Go TestToolSQLiteFileNameIsThePath (internal/ormgen openToolDB), PHP clients/php/tests/schema_tool_test.php (SchemaImport::connect), TypeScript clients/typescript/tests/sqlite-auto-import.mjs (openToolDb) and Rust tool_sqlite_file_name_is_the_path in clients/rust/orm-build/tests/tool_limits.rs (tool_db::open). All pass; the new Go and Rust files are listed under the dsn_connection feature of contracts/features.json. The opener that created the file on 2026-09-26 is therefore not in the current tree. | [o] |
| T8.0.14 | Propagate a failed MySQL context reset in the Go client (requires T8.0). Cause: MySQL user variables survive COMMIT and ROLLBACK (mysql.context.user_variable_session_scope), and the Go client discards the error of SET @`orm.<key>` = NULL at transaction end (clients/go/orm/utils.go:152), so a failed reset leaves the audit operation id on a pooled connection without an error; PHP, Rust and TypeScript propagate it. | Acceptance: a tracked Go RED case with a failing reset reports the error, and the other clients keep equivalent cases. | Evidence: the Go client now returns a failed reset of SET @`orm.<key>` = NULL: clearLocals returns the mapped error, commit rolls back and returns it, and a rollback after a callback error returns CONFIG transaction failed (<callback error>) and rollback failed (<cleanup error>), the form the Rust client already used. Go TestTransactionReportsFailedLocalReset (clients/go/orm/context_reset_test.go) wraps the MySQL driver so that only the reset fails; before the fix it failed with "commit: want the reset error, got <nil>", and after it passes on MySQL. The equivalent cases found that PHP replaced the callback error with the reset error and TypeScript discarded the reset error on rollback: clients/php/tests/model_test.php (failed local reset/mysql, a PDO that rejects only the reset) failed with "rollback reports the callback and the failed reset: reset rejected by the test driver", and clients/typescript/tests/model.mjs (mysql/failedLocalReset, a transaction control that rejects only the reset) failed with "…: callback failed"; both now report the two errors in the CONFIG form and pass. Rust tx::tests::failed_local_reset_is_reported ends the transaction connection with MySQL KILL from another connection and asserts that finish and rollback report the failed reset. go test ./clients/go/orm -run 'Transaction|Local|Cancel', go test ./clients/go/model, the PHP and TypeScript model tests on SQLite, MySQL and PostgreSQL and the Rust tx::tests:: tests pass; docs/spec/interfaces.md states the rule. The remaining discarded transaction-end results of the Go client are T8.0.14.1. | [o] |
| T8.0.14.1 | Report every transaction-end error in the Go client (follows T8.0.14). Cause: after T8.0.14 the Go client still discards the result of SELECT RELEASE_LOCK(?) in releaseLocks (clients/go/orm/utils.go), of tx.Rollback() in abort and of finishSQLiteMode in rollback (clients/go/orm/tx.go), and of the rollback that a panicking or exiting callback triggers, while the PHP, TypeScript and Rust clients propagate a failed RELEASE_LOCK and rollback. | Acceptance: each of these errors reaches the caller or the panic, with a tracked Red case in Go and equivalent cases in the other clients. | Evidence: the Go client now reports every transaction-end failure. releaseLocks (clients/go/orm/utils.go) reads the RELEASE_LOCK result and returns a failed statement and a result other than 1 as CONFIG lock <key> was not held at transaction end; rollback (clients/go/orm/tx.go) runs finishSQLiteMode, releaseLocks, clearLocals and the native rollback and joins every failure; commit, begin and callback failures go through rollbackAfter, which returns CONFIG transaction failed (<cause>) and rollback failed (<transaction end error>) when the rollback fails too; a panicking callback and a callback that leaves through runtime.Goexit panic with that error when their rollback fails and keep the original panic value otherwise. beginSQLiteMode marks the mode before its first PRAGMA, so a begin that fails after PRAGMA read_uncommitted = 1 restores it. The transaction keeps its pool connection as a *sql.Conn, and a native rollback on a connection the driver closed is complete: TestWithContextCancelsInsideTransaction showed that both drivers close the connection when a statement in the transaction is cancelled (MySQL rollback returned "context canceled", PostgreSQL "conn closed"), so the rollback recognizes driver.ErrBadConn (clients/go/orm/pg maps pgconn.ErrConnClosed to it) and driver.Validator and the cancelled statement still returns CANCELED. Go TestTransactionReportsFailedLockRelease, TestTransactionReportsFailedRollback and TestTransactionReportsFailedSQLiteModeReset (clients/go/orm/transaction_end_test.go) use a test driver (clients/go/orm/failing_driver_test.go) that rejects a chosen statement or the rollback of the MySQL or SQLite driver; before the fix 9 of their 11 subtests failed, among them "commit: want the release error, got <nil>", "panic: want CONFIG … got <nil>" and "rollback: want CONFIG … got callback failed", and after it all 11 pass. The equivalent cases found the same gaps in the other clients. PHP (failed lock release/mysql, failed rollback/sqlite in clients/php/tests/model_test.php, a PDO that rejects a chosen statement or rollback) failed with "a lock released early is reported: no error" and "begin reports the cause and the failed transaction end: rollback rejected by the test driver"; Db::finish now runs every cleanup step, checks the RELEASE_LOCK result and reports the cleanup and rollback failures, and a failed begin restores the modes and reports a failed rollback with its cause. TypeScript (mysql/failedLockRelease, sqlite/failedRollback in clients/typescript/tests/model.mjs) failed 5 checks, among them "a lock released early is reported: no error" and "commit reports the failed mode reset: DRIVER: sqlite: cannot start a transaction within a transaction", because a failed begin PRAGMA left the SQLite transaction open; Db.finish and the SQLite and PostgreSQL drivers now run every step, roll a failed begin back and report the rollback failure instead of dropping it. Rust (tx::tests:: in clients/rust/orm/src/tx.rs) failed lock_not_held_at_transaction_end_is_reported, every_failed_cleanup_step_is_reported ("left: 1, right: 2") and panicking_callback_ends_its_transaction ("the panicked transaction released its lock: left: Some(0), right: Some(1)"), and the SQLite panic case failed with "the panic carries the transaction failure"; finish now runs every step and checks the RELEASE_LOCK result, and a panicking callback rolls back before the panic continues. sqlite_transaction_end_failures_are_reported denies ROLLBACK and PRAGMA statements with a SQLite authorizer. go test ./clients/go/..., the PHP model test (11 tests × 3 databases), the TypeScript model test on SQLite, MySQL and PostgreSQL and the 9 Rust tx::tests:: tests pass; docs/spec/interfaces.md states the rule. Found defects are T8.0.14.2, T8.0.14.3 and T8.0.14.4. | [o] |
| T8.0.14.2 | Close a dropped Rust transaction connection over TLS. Cause: CancellableConnection (clients/rust/orm/src/driver.rs) ends a transaction connection that was not completed with close_hard in a spawned task; with the MySQL test server over TLS that close started and had not finished 5 s later, and the session kept its named lock, so a failed begin, a failed commit cleanup or a dropped transaction future can leave the open transaction and its locks on the server. | Acceptance: a dropped Rust transaction future ends its server session and named lock over TLS, with a tracked Red case. | Evidence: sqlx's close_hard flushes before the shutdown, and the rustls stream of sqlx-core 0.9.0 reads until the server sends data when it has nothing to write, so the spawned close of a TLS connection never finished. CancellableConnection (clients/rust/orm/src/driver.rs) now drops the detached connection in place, which closes the socket at once for MySQL and PostgreSQL and closes the SQLite handle as SQLite's close_hard did; the spawned task and its discarded close result are gone. Rust tx::tests::dropped_transaction_closes_its_connection takes a named lock in a transaction, drops the transaction future and takes the lock from another connection; it asserts the MySQL connection uses TLS (Ssl_cipher is not empty). Before the fix it failed on MySQL in two runs (5.02 s and 5.03 s) with "mysql: the dropped transaction released its lock: left: Some(0), right: Some(1)" (GET_LOCK(?, 5) timed out); after it the MySQL GET_LOCK returns 1, the PostgreSQL pg_advisory_xact_lock under lock_timeout = '5s' succeeds and SQLite BEGIN IMMEDIATE under busy_timeout = 5000 succeeds with the inserted row rolled back. The 28 tests of cargo test -p orm --lib pass; docs/spec/interfaces.md states the rule. | [o] |
| T8.0.14.3 | Report every savepoint-end error. Cause: the Go savepoint (clients/go/orm/tx.go) discards the results of ROLLBACK TO SAVEPOINT and RELEASE SAVEPOINT after a panic, a runtime.Goexit and a callback error, and the PHP and TypeScript savepoints replace the callback error with a failed savepoint rollback. | Acceptance: every savepoint end error reaches the caller or the panic in every client, with tracked Red cases. | Evidence: after a callback error, a panic or a Go runtime.Goexit every client now runs both ROLLBACK TO SAVEPOINT and RELEASE SAVEPOINT and reports a failure of either as CONFIG transaction failed (<cause>) and rollback failed (<savepoint end error>) (the panic value for a panic), and returns a failed RELEASE SAVEPOINT after a successful callback: Go savepoint, rollbackSavepoint and endSavepoint (clients/go/orm/tx.go), PHP Db::savepoint and Db::endSavepoint, TypeScript Database.savepoint, and Rust run_savepoint and rollback_savepoint (clients/rust/orm/src/tx.rs), which serve transaction, transaction_send and transaction_once. The Rust savepoint also discarded RELEASE SAVEPOINT after a callback error and did not end on a panic; it now catches the panic, ends the savepoint and continues the panic. A Go savepoint of a transaction that already ended, through the cancelled transaction context or a connection that the driver closed, ended with it. Go TestSavepointReportsFailedEnd (clients/go/orm/transaction_end_test.go, the SQLite test driver rejects the chosen statement) failed 7 of its 8 subtests before the fix, among them "callback error: want CONFIG … got statement rejected by the test driver" and "panic: … got <nil>"; the nested case added to TestWithContextCancelsInsideTransaction failed on MySQL and PostgreSQL with "blocked read: driver: bad connection (code "")" because the failed ROLLBACK TO SAVEPOINT on the closed connection replaced CANCELED; a holder transaction that ends before it holds the row now fails that test instead of blocking it. PHP failed savepoint end/sqlite (clients/php/tests/model_test.php) and TypeScript sqlite/failedSavepointEnd (clients/typescript/tests/model.mjs) each failed 4 checks, among them "ROLLBACK TO SAVEPOINT reports the cause and the failed transaction end: statement rejected by the test driver". Rust tx::tests::savepoint_end_failures_are_reported (a SQLite authorizer denies the savepoint statements) failed with "callback error 4: sqlx: error returned from database: (code: 23) not authorized"; the two authorizer tests now hold one lock because they share the process-wide denial setting, which had made them fail together. After the fix go test -p 1 ./clients/go/..., the PHP model test (11 tests × 3 databases), make ts-check and the 29 tests of cargo test -p orm --lib pass; the MySQL and PostgreSQL runs used dedicated databases on the test servers while concurrent runs held the shared ones. docs/spec/interfaces.md states the rule. | [o] |
| T8.0.14.4 | Clear MySQL session state of a cancelled Go transaction. Cause: when the context of a Go transaction is cancelled, database/sql rolls the transaction back itself and keeps the go-sql-driver connection, which implements driver.SessionResetter and driver.Validator, so releaseLocks and clearLocals cannot run on it and its named locks and user variables can stay on the pooled connection; this reading of database/sql has no executed case yet. | Acceptance: a tracked Go case shows whether the cancelled transaction leaves session state, and a reproduced defect is fixed. | Evidence: the defect reproduced. Go TestCancelledTransactionClearsSessionState (clients/go/orm/cancelled_session_test.go) cancels the context of a transaction on a one-connection pool after the callback took a named lock and a local value (a read-only transaction on SQLite). Before the fix every subtest returned "want CANCELED alone, got CONFIG: transaction failed (CANCELED: context canceled) and rollback failed (CANCELED: …)", because releaseLocks, clearLocals and finishSQLiteMode ran with the cancelled context; on MySQL GET_LOCK(?, 5) from another connection returned 0 and the next transaction read the user variable "tester"; on SQLite the next transaction failed with "READ_ONLY: attempt to write a readonly database (8)" because query_only = 1 stayed on the pooled connection. rollback (clients/go/orm/tx.go) now closes a cancelled transaction's connection with closeSession instead of returning it to the pool, so the server ends the session with its locks, variables and modes, and the transaction returns CANCELED alone. After the fix the three subtests pass in two runs, go test -p 1 ./clients/go/... passes on dedicated MySQL and PostgreSQL databases of the test servers, and docs/spec/interfaces.md states the rule. | [o] |
| T8.1 | Specify dbspec in docs/spec/dbspec.md and its Korean pair (requires T8.0). | Acceptance: grammar; separation of definitions, settings and diagram; settings key table with schemaHash/manifestHash membership; neutral type and expression sets; naming correction table; cross-document imports; canonical order; limits; diagnostics; a keep/move/remove decision for every existing schema feature, including the PhysicalGraph records that carry dialect SQL text; shared vectors and per-client budgets. | Evidence: docs/spec/dbspec.md and its Korean pair define the header and document names, use against a declared document set without file paths, names, columns, the thirteen neutral types with their renderings, keys, indexes, foreign keys, the neutral check expressions, ten settings with their schemaHash or manifestHash membership, audit through an operation column and a history table (no session variable, transaction setting or context table; physical deletes rejected), diagrams, canonical form including literal forms, limits, sixteen diagnostic rules with locations, the naming changes and a keep/move/remove decision for every Mermaid feature including removal of the PhysicalGraph dialect-text records and the annotated Markdown path. docs/spec/dialects.md records the audit decision. tests/dbspec/cases.json holds 4 canonical, 2 normalization and 17 invalid shared cases with computed error locations. The Rust parse budget is 300 ms for the 2000-table case; other clients are fixed at their first measurement. make docs-check, make docs-rules-check and make checklist-check pass. | [o] |
| T8.1.1 | Settle the dbspec rules that the TypeScript, PHP and Rust parsers read differently (follows T8.1). Cause: the three implementations reported about 30 places where docs/spec/dbspec.md was silent or open, and they chose differently (for example the column unit, the order of table lines, comments before a closing brace and at the end, f64 and decimal literals, use cycles, setting repeats, diagram coordinate range, stopping errors and diagnostic order). | — | Evidence: docs/spec/dbspec.md and its Korean pair now fix each point (reserved words, tabs, line ends, header position, constraint order, unclosed blocks, modifier and action order, literal forms, codec storage types, aes_version and blind_index rules, audit table shape, use resolution, name scopes against table names, written orders, comment placement, error locations and ordering), and tests/dbspec/cases.json gains 1 canonical, 4 normalization and 16 invalid cases that lock them (5, 6 and 33 in total). make docs-rules-check and make checklist-check pass. The client items realign their parsers to these cases. | [o] |
| T8.1.2 | Settle the dbspec rules that the Go parser reported open (follows T8.1.1). Cause: docs/spec/dbspec.md did not say how repeated blind_index lines are ordered, which rules a failed key or index line hides, or how a line that fails on a tab keeps its kind; Go chose one reading, while PHP and TypeScript kept blind_index lines in written order and reported a missing primary key, the identity rule, a leading-index error and a key-target error after a failed key line. | — | Evidence: docs/spec/dbspec.md and its Korean pair sort blind_index lines by AES column and state that a failed line keeps its kind and name and hides the rules that depend on its columns; tests/dbspec/cases.json gains blind-index-sorted, failed-primary-key-line, failed-index-line-under-foreign-key, failed-target-key-line, tab-in-primary-key-line and leading-tab-column-line (5, 7 and 38 cases). Before the client changes PHP and TypeScript failed them; now make dbspec-go-check, make dbspec-php-check and make dbspec-ts-check pass all 50 cases, and interface-check passes for go, php and typescript after recording the new PHP and TypeScript symbols. | [o] |
| T8.1.3 | Settle where a header error points (follows T8.1.2). Cause: docs/spec/dbspec.md required the exact form dbspec 1 <name> without saying where its errors point; Go, PHP and TypeScript pointed at an extra word, Rust at the space before it, and TypeScript accepted double spaces in the header. | — | Evidence: docs/spec/dbspec.md and its Korean pair point a header error at the first character that departs from dbspec 1 <name>, <name> being a run of ASCII letters, digits and _, or one past the line end; tests/dbspec/cases.json gains header-extra-word, header-double-space, header-tab, header-version, header-missing-name, header-leading-space and header-double-space-before-version (5, 7 and 45 cases). Go, PHP and TypeScript failed header-extra-word and TypeScript also the double-space cases before the change; the four client checks (make dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check) and the four-language interface-check now pass. | [o] |
| T8.1.4 | Type the check predicates so that the three databases evaluate them alike (follows T8.1.3). Cause: docs/spec/dbspec.md allowed +, -, * and / and any literal against any column, while integer division (2.5 on MySQL, 2 on PostgreSQL and SQLite), integer overflow (REAL on SQLite) and the scaled SQLite decimal give different results, and no client checked operand types. | — | Evidence: docs/spec/dbspec.md and its Korean pair define the predicate forms (comparisons with a column, in, between, is null, a bool column, not, and, or, parentheses), remove arithmetic, functions, the null literal and bytes columns, make a literal that meets a column a valid default of the column type written in its canonical default form, list which column types meet, and order the diagnostics: reading first, then columns and types in source order. tests/dbspec/cases.json gains the canonical check-typed-predicates, the normalize check-literals-take-column-form and 26 invalid check-* cases, and every-line-kind now writes total >= 0.00 (6, 8 and 71 cases). Go, PHP, TypeScript and Rust failed the 20 first new cases before their changes and pass all cases now; their rule tests replace the cases that used arithmetic and add typed cases. make dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check and dbspec-compare-check (8 runs agree on 89 cases) pass; stress medians are Go 45 ms, PHP 205-211 ms, TypeScript 87-90 ms and Rust 30 ms; gofmt, rustfmt, clippy and the four-language interface-check pass after recording the symbols. | [o] |
| T8.1.5 | Bound the names that the renderer generates to 63 bytes (follows T8.1.4). Cause: docs/spec/dialects.md "Rendered statements" names renderer CHECKs <table>$<column> and triggers <table>$<event>, which can exceed the 63-byte identifier limit for valid table and column names, and the renderer must not shorten a name. | — | Evidence: docs/spec/dbspec.md and its Korean pair list the generated names in "Names" and make a name over 63 bytes a name.length error at the column name or the immutable or audit keyword. tests/dbspec/cases.json gains the canonical generated-names-at-63-bytes and three invalid cases (check, immutable and audit names of 64 bytes); Go, PHP, TypeScript and Rust failed the invalid ones before their change and pass all cases now (7, 8 and 74 cases). The five dbspec targets, gofmt, rustfmt, clippy and the four-language interface-check pass. Stress medians rose under a load average of 8.6; the Go stress measured 62-65 ms before the change and 53-63 ms after it in the same load, so the rule adds no measurable parse time. | [o] |
| T8.1.6 | Reserve and, or, not, in, between and is (follows T8.1.4). Cause: these words read as predicate keywords but are valid table and column names, so check ck (and > 0) on a column named and is read as a column by the PHP parser and rejected by the Go parser; the predicate grammar is ambiguous unless the words are reserved. | — | Evidence: docs/spec/dbspec.md and its Korean pair add and, or, not, in, between and is to the reserved words, and the four clients add them to their reserved lists. tests/dbspec/cases.json gains the invalid reserved-predicate-keywords (a table named or, a column named between, both name.format), which the Go parser returned as a valid document before the change (the other clients were changed with it and not run before); all cases pass now in the four clients. The PHP check reader and renderer drop the branch that read a keyword as a column when the table had one, which reserved words make unreachable. The five dbspec targets, rustfmt and the four-language interface-check pass. | [o] |
| T8.1.7 | Keep only the check predicate forms that introspection restores (follows T8.1.4). Cause: probes show that MySQL writes NOT into the comparisons it negates and rejects a bool column alone as a CHECK (3812), and that PostgreSQL writes BETWEEN as two comparisons, so a document with not, between or a column alone cannot be restored from the catalog; both catalogs also add parentheses that canonical form keeps as written. | — | Evidence: six dialect probes record the catalog behavior and pass on MySQL 8.4.11 and PostgreSQL 17.11: mysql.check.bool_column_rejected (3812), mysql.check.not_pushed_down, mysql.check.between_kept, mysql.check.string_escaped_twice, postgres.check.between_rewritten and postgres.check.not_kept; docs/spec/dialects.md cites them in the CHECK expression row. docs/spec/dbspec.md and its Korean pair keep comparisons, [not] in, is [not] null, and, or and parentheses, remove not, between and a column alone with the reason, state the reading positions, and make canonical form keep only the parentheses that an or inside an and needs. tests/dbspec/cases.json rewrites check-typed-predicates, adds the normalize check-redundant-parentheses and the invalid check-not-operator, check-between, check-bool-column-alone and check-literal-before-not (7, 9 and 79 cases), and tests/dbspec/ddl.json rewrites checks-in-dialect-form, whose statements alone changed and still apply on the three databases. Go, PHP, TypeScript and Rust failed the first four new cases before their change; check-literal-before-not was added after the TypeScript change showed that Go reported that literal instead of not, and Go was corrected. The parsers, typers, emitters and renderers drop the removed forms; the five dbspec targets, make dbspec-ddl-check, gofmt, rustfmt and the four-language interface-check pass. | [o] |
| T8.1.8 | Let audit stand without soft_delete (follows T8.1.7). Cause: audit is a schema setting that introspection reads back from its triggers, while soft_delete is a manifest setting that no database holds; requiring it made every introspected audited table an invalid document. The BEFORE DELETE trigger already makes a physical delete fail. | — | Evidence: the canonical vector audit-without-soft-delete failed in the four clients with the soft_delete diagnostic and passes after the rule is removed from Go settings.go, PHP Parser.php, TypeScript parse.ts and Rust validate.rs; the client rule tests drop that diagnostic; make dbspec-compare-check reports 8 runs agree on 115 cases; the interface check passes with the TypeScript symbols re-recorded. | [o] |
| T8.2 | Implement dbspec parse and canonical emit in every client (requires T8.1). | Acceptance: emit(parse(s)) == s for canonical input; located diagnostics; the 2000-table/60000-column/10000-FK case within budget; removal of the Mermaid source path and the T7.17.2.10.x document path; one CLI name across languages. | Evidence: T8.2.1 to T8.2.6 are complete: every client parses and emits dbspec with emit(parse(s)) == s on the shared cases and located diagnostics; the 2000-table stress document passes each client budget in make dbspec-<lang>-check; make dbspec-compare-check finds the four clients identical; the Mermaid source path and the T7.17.2.10.x document path are removed and the CLI has one name. | [o] |
| T8.2.1 | Implement dbspec parse, validation and canonical emit in the Rust client (requires T8.1). | Acceptance: every case of tests/dbspec/cases.json passes; the 2000-table/60000-column/10000-foreign-key case parses within 300 ms in release mode and emits identically twice; errors carry rule, line and column. | Evidence: orm-schema/src/dbspec implements parse, emit and Diagnostic with the signatures contracts/interfaces.json declares; the Red tests failed before the module existed. make dbspec-rust-check runs twice: tests/dbspec.rs passes all 50 shared cases (5 canonical, 7 normalize, 38 invalid), and tests/dbspec_rules.rs passes 52 rule cases, comment attachment and limits. The six T8.1.2 cases failed before the blind_index sort and failed-key rules were added. The release stress example parses five times with median 29-31 ms (min 28 ms, max 42 ms) and emit 8-9 ms on rustc 1.98.1, darwin arm64, under the 300 ms budget, and emits the document back unchanged twice. cargo fmt --check and cargo clippy -D warnings pass for orm-schema. contracts/symbols/rust.json now matches the native symbols: 715 added, 10 changed, and 37 removed from orm-gen db.rs and introspect.rs, which T7.17.1 deleted without updating the snapshot. go run ./tests/interfaces/check passes for all four languages. | [o] |
| T8.2.1.1 | Expose the parsed dbspec model in the Rust client as Go, PHP and TypeScript do (follows T8.2.1). Cause: the Rust model types and fields were pub(crate), so a tool outside the crate could read only the document name through emit and could not read or change tables, columns, keys or diagrams, while the Go model has exported fields, the PHP model public properties and the TypeScript client exports its model types. | — | Evidence: orm_schema::dbspec::model is a public module whose types and fields are public, with Value of the literal; clients/rust/orm-schema/tests/dbspec_model.rs did not build before (module model is private, field tables of struct Document is private) and now reads a parsed document, adds a column and emits the canonical text that parses again; make dbspec-rust-check runs it; contracts/symbols/rust.json records the new public symbols and interface-check passes. | [o] |
| T8.2.2 | Implement dbspec parse, validation and canonical emit in the Go client (requires T8.1). | Acceptance: every shared case passes; the 2000-table case is measured and its budget recorded. | Evidence: engine/dbspec implements Parse, Emit and Diagnostic with the signatures contracts/interfaces.json declares; the Red tests failed before the package existed. go test ./engine/dbspec passes 145 subtests: all 44 shared cases (5 canonical, 6 normalize, 33 invalid), 86 rule cases, 8 encoding and limit cases and 7 canonical forms. The stress document emits back unchanged twice with parse 48-53 ms and emit 6-10 ms on go1.27.0, darwin arm64, under the 100 ms budget of T8.2.4.1. interface-check passes for go. | [o] |
| T8.2.2.1 | Report the identity rule in Go when a table has no primary key line (follows T8.2.2). Cause: Go validated the identity rule only inside the branch for a table with a primary key, so table t { id i64 identity } reported only key, while docs/spec/dbspec.md requires the identity column to be the only primary key column and lets only a failed primary key line hide that rule; PHP, TypeScript and Rust report column at identity as well, and no shared case covered it, so the four-client comparison did not see the difference. | — | Evidence: tests/dbspec/cases.json gains the invalid case identity-without-primary-key (key 3:7 and column 4:10); Go TestSharedVectors failed on it with only key and passes after validate.go reports the identity rule for a table without a primary key line; the plan parse case target-diagnostic-located now lists both diagnostics; make dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check and dbspec-compare-check pass. | [o] |
| T8.2.3 | Implement dbspec parse, validation and canonical emit in the PHP client (requires T8.1). | Acceptance: every shared case passes; the 2000-table case is measured and its budget recorded. | Evidence: clients/php/src/Dbspec implements Orm\Dbspec\Dbspec::parse and ::emit and the diagnostic record as contracts/interfaces.json declares; the Red tests failed before the classes existed. make dbspec-php-check runs each test twice: all 44 shared cases (5 canonical, 6 normalize, 33 invalid) and 57 focused cases pass, and the stress document emits back unchanged twice with parse 218-308 ms, emit 24-32 ms and a 57 MiB peak within 128 MiB on PHP 8.5.10, darwin arm64, under the 400 ms budget of T8.2.4.1. interface-check passes for php. | [o] |
| T8.2.4 | Implement dbspec parse, validation and canonical emit in the TypeScript client (requires T8.1). | Acceptance: every shared case passes; the 2000-table case is measured and its budget recorded. | Evidence: clients/typescript/src/dbspec (index, model, parse, emit) implements parseDbspec, emitDbspec and DbspecDiagnostic exactly as contracts/interfaces.json declares; the Red vector test failed with ERR_MODULE_NOT_FOUND before the module existed. make dbspec-ts-check passes 89 tests: 44 shared cases (5 canonical, 6 normalize, 33 invalid) and 45 focused cases, and the stress document (2,490,985 bytes) emits back unchanged twice, parse 105-156 ms and emit 15-26 ms on Node 26.8.1, darwin arm64. The TypeScript parse budget is 160 ms. contracts/symbols/typescript.json records the 279 dbspec symbols and its hash is updated; interface-check reports no dbspec failure for TypeScript, while 164 unrecorded physical_* and introspect symbols remain from before and leave with T8.2.6. | [o] |
| T8.2.4.1 | Make the client budgets and the symbol snapshot true (follows T8.2.4). Cause: T8.2.4 cited a 160 ms TypeScript budget that no test asserted and that docs/spec/dbspec.md did not state, and it was committed while interface-check failed on 164 TypeScript, 107 PHP and 514 Go symbols that the snapshot did not record. | — | Evidence: docs/spec/dbspec.md and its Korean pair state the budgets (Rust 300 ms, Go 100 ms, TypeScript 250 ms, PHP 400 ms within 128 MiB), and the Go, TypeScript and PHP stress tests fail above them; the Go test failed with a 1 ms budget (parse 50 ms) and passes at 100 ms (52 ms), TypeScript passes at 121-132 ms and PHP at 222-234 ms with a 57 MiB peak. contracts/symbols/{go,php,typescript}.json record every native symbol, including the physical_* ones that T8.2.6 removes, and go run ./tests/interfaces/check -language passes for go, php and typescript. | [o] |
| T8.2.4.2 | Decide the parse budgets on the median of five parses (follows T8.2.4.1). Cause: the TypeScript stress test measured one parse; while other test binaries used a full core, one parse took 602 ms against the 250 ms budget, and 11 more runs took 111-131 ms, so one slowed parse decided the result. | — | Evidence: docs/spec/dbspec.md and its Korean pair state the median of five; the Go, TypeScript and PHP stress tests parse five times, print the shortest, median and longest parse and fail when the median exceeds the budget. Under the same load the medians were Go 45-47 ms (one loaded run 80 ms), TypeScript 94-107 ms and PHP 208-220 ms within a 59 MiB peak. | [o] |
| T8.2.5 | Compare the four clients on the shared dbspec cases in one tracked target (requires T8.2.1-T8.2.4). | Acceptance: the target runs each client twice and fails on any difference in emission or diagnostics. | Evidence: make dbspec-compare-check builds the TypeScript client and the release Rust example, then runs tests/dbspec/compare/go, php.php, typescript.mjs and orm-schema/examples/dbspec_compare.rs twice each; every runner prints, for the 57 shared cases and the stress document, the emission or the rule line column diagnostics, and check.mjs fails on the first line that differs from the first Go run. The comparison test (check.test.mjs) failed with ERR_MODULE_NOT_FOUND before compare.mjs existed and passes its 4 cases; with the TypeScript runner shifted by one column the target exited 1 at invalid/missing-header, and as committed all 8 runs agree on 58 cases in 1.5 s. The five dbspec targets are part of make check. | [o] |
| T8.2.6 | Remove the Mermaid schema source path, the PhysicalGraph dialect-text records and the annotated Markdown path, and give the CLI one name in every language (requires T8.2.5). | — | Evidence: T8.2.6.1 to T8.2.6.4 are complete: the dbspec document set is the only schema source for every generator, runtime, schema tool and installation; the CLI is orm-gen in Go, PHP and TypeScript, and Rust generates models through build.rs without a CLI. | [o] |
| T8.2.6.1 | Specify the manifest that generators read from a dbspec document and the schemaHash and manifestHash algorithms, with shared vectors from dbspec text to manifest and hashes (follows T8.2.6). Cause: the generators of every client read schema/schema.json, a manifest built from the Mermaid source with auto, raw and pk fields, and docs/spec/dbspec.md names the two hashes and their coverage but neither the manifest format nor the hash computation, so removing the Mermaid path would leave the generators without input. | — | Evidence: docs/spec/dbspec.md and its Korean pair add "Manifest and hashes": the declared document set is the only schema source and no other manifest file exists; the manifest text is the canonical emission without comments and diagrams, concatenated in document name order for a set; the schema text keeps only the immutable and audit settings; each hash is sha256: and the lower-case hex SHA-256 of its text; generated code embeds the manifest text and its hash, runtimes parse it once at process start, and the PHP generator writes PHP arrays so no request parses it (the PHP parser takes 208-220 ms for 2000 tables, about 0.1 ms a table). This replaces the 16-hex hash over Go-style JSON and the nine manifest structs of the four clients. tests/dbspec/cases.json gains three hashes cases (comments, a diagram and manifest settings removed; audit kept in the schema text; a two-document set in name order) whose texts and hashes come from the textual rule, and whose documents emit back unchanged in TypeScript; the five dbspec targets still pass. | [o] |
| T8.2.6.1.1 | Give the Korean manifest heading an ASCII anchor (follows T8.2.6.1). Cause: VitePress writes the id of ## Manifest와 hash with decomposed Hangul, so the links #manifest와-hash in docs/dbspec.ko.md and docs/dialects.ko.md reach no anchor and make docs-check fails. | — | Evidence: make docs-check failed with Missing anchor: …/ko/dbspec.html#manifest%EC%99%80-hash and passes with the heading ## Manifest와 hash {#manifest-and-hashes} and both links pointing to that id. | [o] |
| T8.2.6.1.1.1 | Point the Korean protocol link at the ASCII manifest anchor (follows T8.2.6.1.1). Cause: docs/protocol.ko.md links dbspec.ko.md#manifest와-hash, which T8.2.6.1.1 replaced by the id manifest-and-hashes, so npm run docs:check fails with Missing anchor: …/ko/dbspec.html#manifest%EC%99%80-hash at the current commit. | Acceptance: the link uses #manifest-and-hashes and make docs-check passes. | Evidence: T8.2.6.1.3 corrected the same two links of docs/protocol.ko.md to #manifest-and-hashes; docs/protocol.ko.md holds no manifest와-hash and make docs-check passes its 573 internal links and assets. | [o] |
| T8.2.6.1.2 | Make schemaHash depend only on the tables (follows T8.2.6.1.1). Cause: the schema text concatenated the documents of a set with their headers and use lines, so moving a table to another document or renaming a document changed schemaHash while the database stayed the same, against its definition, and an introspected database could not be compared with a set by hash; migration plans record this hash. | — | Evidence: docs/spec/dbspec.md defines the schema text as one document schema with every table of the set in name order; the new hash vector same-tables-in-one-document puts the two tables of document-set-in-name-order in one document and must give the same schema text and schemaHash sha256:9d2367df…f140, which the previous definition did not; Go, PHP, TypeScript and Rust pass the four hash vectors, make dbspec-compare-check and the four introspection checks pass, and the Go round trip compares with ManifestOf(documents).SchemaText directly. | [o] |
| T8.2.6.1.3 | Point the Korean protocol page at the manifest anchor (follows T8.2.6.1.1). Cause: the client switch merge (T8.2.6.3) added a link dbspec.md#manifest와-hash to docs/protocol.ko.md, an anchor that no heading has, so make docs-check failed with "Missing anchor"; that merge did not run docs-check. | — | Evidence: make docs-check failed with Missing anchor: …/ko/dbspec.html#manifest%EC%99%80-hash from docs/protocol.ko.md and passes with both of its links pointing to #manifest-and-hashes: 48 static pages and 573 internal links and assets. | [o] |
| T8.2.6.2 | Compute the manifest text, the schema text and both hashes of a dbspec document set in the Go, PHP, TypeScript and Rust clients; every client passes the hashes cases of tests/dbspec/cases.json and make dbspec-compare-check compares the four results (requires T8.2.6.1). | — | Evidence: Go ManifestOf, PHP Dbspec::manifest, TypeScript dbspecManifest and Rust manifest emit each document with a view that drops comments and diagrams (manifest) or also every setting except immutable and audit (schema), concatenate the documents in name order and hash with SHA-256; a repeated document name returns one name.duplicate diagnostic at 1:10. contracts/interfaces.json declares the Dbspec.manifest rule and the DbspecManifest owner, and interface-check passes for the four languages. The new tests (Go manifest_test.go, PHP dbspec_manifest_test.php, TypeScript tests/dbspec.mjs, Rust tests/dbspec_manifest.rs) failed before the function existed and pass the 3 hashes cases in both input orders and the repeated name rule; each client target runs them. The compare runners print the hashes and both texts of every hashes case, and the 8 runs agree on 61 cases. One TypeScript stress run measured a 320 ms median while other processes compiled Rust; parse code was unchanged and the next runs measured 86-104 ms. | [o] |
| T8.2.6.2.1 | Reject an invalid document set in manifest and rendering alike (follows T8.2.6.2). Cause: ManifestOf reports a repeated document name but not a used document missing from the set, the Go renderer skips a missing used document silently, and the PHP and TypeScript renderers throw for both; docs/spec/dbspec.md states no rule for the set these functions take. | — | Evidence: docs/spec/dbspec.md and its Korean pair state the set rule in "Manifest and hashes" (each document once, every used document present; name.duplicate and use at 1:10 of the later or using document, documents and used names in name order, no manifest and no statements on a diagnostic), and docs/spec/dialects.md points the renderer to it. One set check (Go checkSet, PHP DocumentSet::check, TypeScript checkSet, Rust check_set) now serves the manifest and the renderer, which returns statements or diagnostics in every client (Go ([]string, []Diagnostic), PHP RenderResult, TypeScript DbspecRenderResult, Rust Result). tests/dbspec/cases.json gains three sets cases (repeated name, missing used document, complete set); before the change Go did not compile against the new signature, PHP and TypeScript threw instead of returning a diagnostic, TypeScript and Go gave no use diagnostic, and Rust rendered an invalid set; all pass now. The TypeScript DbspecDiagnostic moved to model.ts, so contracts/interfaces.json names that owner. The five dbspec targets, gofmt, rustfmt, clippy and the four-language interface-check pass. | [o] |
| T8.2.6.3 | Make every generator, runtime, schema tool and schema installation read the dbspec document set: generated code embeds the manifest text and manifestHash (PHP as arrays), runtimes build their model from it without a schema file, and the sources of schema/schema.json, schema/bench.mmd and the manifest fixtures of contracts/ and the client tests become dbspec documents (requires T8.2.6.2 and T8.3). | — | Evidence: T8.2.6.3.1-T8.2.6.3.7 are complete, and T8.2.6.4 removed the Mermaid sources and manifests that T8.2.6.3.2 kept for the old tests. | [o] |
| T8.2.6.3.1 | Specify the runtime model that a client builds from a dbspec document set and what generated code holds in each language: entity and field names, value types per dbspec type, codec stages, the default select set, the updated, soft_delete, aes_version, blind_index and audit operation behavior, and relations from navigation settings only; decide every field of the Mermaid manifest (requires T8.2.6.2.1). Cause: the runtimes read nine manifest structs whose fields (auto, lazy, styles, relations derived from _seq names, fulltext, unsigned, enum) have no dbspec counterpart or a different name. | — | Evidence: docs/spec/dbspec.md and its Korean pair add "Runtime model": entities per table in name and document order with names from entity or the table, fields with a per-language value type table for the 13 dbspec types (a new i16 path; date as YYYY-MM-DD text in TypeScript, which today returns a date-time text; time(p) as text everywhere), keys, the default select set from select explicit only, database defaults for omitted columns, codec value types from the stages (styled values for ordered_json, serialize, yaml, gz and base64, which Rust today returns unstyled for gz and base64; strings for aes, hex and ip), the runtime behavior of every setting including the audit operation id, joins through match methods, and a connection without a schema path that carries manifestHash. A last paragraph maps every Mermaid manifest field and removes fulltext conditions and point values. The navigation setting now states that generated code does not read it, which matches the four generators: none of them uses the manifest relation names. | [o] |
| T8.2.6.3.2 | Rewrite schema/bench.mmd, the Mermaid fixtures of the Go tests and contracts/fixtures, and the Rust manifest fixtures as dbspec documents, with every name-convention behavior declared as a setting and every foreign key indexed (requires T8.2.6.3.1). | — | Evidence: schema/bench.dbs rewrites schema/bench.mmd: the is_* columns are bool, updated_ts is the updated setting, aes_key_version the aes_version setting, the name-prefixed columns declare their codecs (aes hex, gz, ordered_json for both json_ and jsons_, base64, serialize, ip), the columns that the Mermaid builder left out of the default select set by type or style are listed in select explicit, unsigned becomes the check ck_author_counts, enum(open_done) a varchar(8) with ck_task_state, every relation line a named foreign key with a leading index (five indexes added), and the fulltext directive is dropped. contracts/fixtures gains decimal_schema.dbs, secret_config.dbs and zone.dbs from decimal_schema.mmd and the Rust aes_json and zone_schema manifests, and audit.dbs for the history-table audit; the Rust audit_* manifests describe the removed operation and change tables and are not rewritten. Every document parses and emits back unchanged, and the new TestSchemaDocumentsApply in make dbspec-ddl-check renders each of the 5 documents with the Go renderer and applies it to MySQL, PostgreSQL and SQLite: 15 probes pass. The .mmd files and the old manifests stay for the old tests until T8.2.6.4. | [o] |
| T8.2.6.3.3 | Build the runtime model and generated code from dbspec in the Go client and engine (requires T8.2.6.3.2). | — | Evidence: engine/runtimemodel builds the model; ormgen gen --document embeds the manifest; Install(manifestText) renders dbspec; db.Transaction(fn, orm.Operation(id)) writes the audit operation id. The four switches share the IR records (manifest_hash, no match) that the interface check requires to agree, so they are integrated as one merge whose tree passes interface-check, dbspec-go/php/ts/rust/compare-check, client-unit-check, ts-check, schema-check, go-model-check, rust-check, dbspec-ddl-check, dbspec-introspect-check, client-db-check, client-pooler-check and conformance-check on MySQL, PostgreSQL and SQLite. | [o] |
| T8.2.6.3.4 | Build the runtime model and generated code from dbspec in the PHP client (requires T8.2.6.3.2). | — | Evidence: RuntimeModel builds the model and the generator writes PHP arrays; orm-gen gen <files.dbs>; install(array $documents); transaction($fn, operation: $id) writes the audit operation id. The four switches share the IR records (manifest_hash, no match) that the interface check requires to agree, so they are integrated as one merge whose tree passes interface-check, dbspec-go/php/ts/rust/compare-check, client-unit-check, ts-check, schema-check, go-model-check, rust-check, dbspec-ddl-check, dbspec-introspect-check, client-db-check, client-pooler-check and conformance-check on MySQL, PostgreSQL and SQLite. | [o] |
| T8.2.6.3.5 | Build the runtime model and generated code from dbspec in the TypeScript client (requires T8.2.6.3.2). | — | Evidence: src/engine/model.ts builds the model; generated models export MANIFEST_TEXT and MANIFEST_HASH; install(texts) renders dbspec; transaction(cb, { operation }) writes the audit operation id; a column with only host stages holds its string value after create(). The four switches share the IR records (manifest_hash, no match) that the interface check requires to agree, so they are integrated as one merge whose tree passes interface-check, dbspec-go/php/ts/rust/compare-check, client-unit-check, ts-check, schema-check, go-model-check, rust-check, dbspec-ddl-check, dbspec-introspect-check, client-db-check, client-pooler-check and conformance-check on MySQL, PostgreSQL and SQLite. | [o] |
| T8.2.6.3.6 | Build the runtime model and generated code from dbspec in the Rust client (requires T8.2.6.3.2). | — | Evidence: orm-schema/src/dbspec/runtime.rs builds the model; Builder::new([dbspec paths]) embeds the manifest; install(&Schema) renders dbspec; transaction(f).operation(id) writes the audit operation id. The four switches share the IR records (manifest_hash, no match) that the interface check requires to agree, so they are integrated as one merge whose tree passes interface-check, dbspec-go/php/ts/rust/compare-check, client-unit-check, ts-check, schema-check, go-model-check, rust-check, dbspec-ddl-check, dbspec-introspect-check, client-db-check, client-pooler-check and conformance-check on MySQL, PostgreSQL and SQLite. | [o] |
| T8.2.6.3.7 | Record the conformance vectors of the dbspec bench schema on MySQL, PostgreSQL and SQLite with the four clients (requires T8.2.6.3.3-T8.2.6.3.6). | — | Evidence: scripts/bench-db.sh installs schema/bench.dbs through go run ./bench/install and seeds it, replacing the hand-written bench/sql/author*.sql; every conformance runner takes only --dsn URI; Go, Rust and TypeScript select the AES key version column once when the request selects it (engine TestAllColumnsSelectAESVersionOnce, TypeScript tests/engine.mjs, Rust all_columns_select_the_aes_version_once each failed with two selections first); tests/conformance/vectors.json, vectors.postgres.json and vectors.sqlite.json hold 25 vectors each recorded from four agreeing clients, with every result unchanged except get_query, whose result is its SQL; make conformance-check passes on the three databases. | [o] |
| T8.2.6.4 | Remove the Mermaid schema source path, the PhysicalGraph dialect-text records and the annotated Markdown path, and give the CLI one name in every language (requires T8.2.6.3). | — | Evidence: 246 files are removed: Go 84 (engine/schema, internal/ormgen, the clients/go/orm physical records, tests/schema/record), PHP 46, TypeScript 45, Rust 45, 17 fixtures of contracts/, schema/bench.mmd, schema/schema.json, tests/schema, scripts/schema and scripts/db-test.sh. They are the Mermaid parser, the manifest JSON and orm-schema-v1 SQL, every CLI command that read them (build, import, validate, ddl, diff, migrate, plan, apply, verify, recover, rollback), whose dbspec counterparts are the render, introspect, plan, apply and recover functions of each client, and the PhysicalGraph records. The CLI is orm-gen in Go (cmd/orm-gen with gen and errors; the model generator moved into package generator), PHP and TypeScript (gen only); the Rust orm-gen binary had only Mermaid commands and is removed, so Rust generates models in build.rs only. Generated files say Code generated by orm-gen. The interface check expands entity rules from schema/bench.dbs, scripts/decimal-db-setup.php renders contracts/fixtures/decimal_schema.dbs, and the PHP examples no longer pass the removed schema path. The targets schema-check, schema-cross-language-check, db-test and the physical-*-check targets are removed, and CI runs ts-model-check in place of schema-check. contracts/symbols drop 315 Go, 334 PHP (8 added for OrmGen), 824 TypeScript and 682 Rust symbols; contracts/features.json drops point_type and maps schema_definition to the dbspec tests. checklist-check, docs-rules-check, go-fmt-check, rust-fmt-check, interface-check, rust-check, dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, client-unit-check, ts-check, ts-model-check, go-model-check, conformance-check (25 vectors and 4 languages on MySQL, PostgreSQL and SQLite), docs-check (48 pages, 545 internal links) and go test of the generator, CLI, interface, conformance, dialect and engine packages pass. feature-check fails as at the previous commit, on the dsn_connection line of docs/features.md that differs from contracts/features.json and on the T15 coverage errors; its feature manifest errors fell from 156 to 69 and none was added. Two raw quotes of the checklist (<value>, <nil>) are now code spans, because VitePress stopped on them and docs-check failed. | [o] |
| T8.2.6.4.1 | Remove the stale libc entry of the Rust lock file (follows T8.2.6.4). Cause: the merge of T8.2.6.4 removed the graph tests that used libc and the libc dependency of orm-schema, but clients/rust/Cargo.lock still listed libc under orm-schema, so cargo check --locked in make rust-check stopped with "cannot update the lock file … because --locked was passed". | Acceptance: the lock file matches the manifests and cargo check --locked passes. | Evidence: with the committed lock file cargo check --locked --offline failed with that error; after the entry is removed it passes, and make rust-check passes. | [o] |
| T8.3 | Render dialect DDL from dbspec in every client (requires T8.2.6.2). Cause of the order: the runtime tests install their schemas through DDL, so the generators and runtimes switch to dbspec (T8.2.6.3) only after dbspec renders DDL. | Acceptance: neutral definitions and trigger-based settings render per the T8.0 table; unsupported definitions fail explicitly; the old audit/immutable directive path is replaced. | Evidence: every sub-item is complete: tests/dbspec/ddl.json renders the neutral definitions and the trigger-based settings per the T8.0 table in the four clients, unsupported definitions fail explicitly, and make dbspec-ddl-check applies the vectors and every schema document to MySQL, PostgreSQL and SQLite; the old audit and immutable directive path is removed with the Mermaid source (T8.2.6.4). | [o] |
| T8.3.1 | Specify the statements each dialect renders from a dbspec document set (statement order, type texts, renderer CHECK names and texts, collations, keys, indexes, foreign keys, and the triggers of immutable and audit) as shared vectors, and apply every vector on MySQL 8.4, PostgreSQL 17 and SQLite in the dialect probe environment (requires T8.2.6.2). | — | Evidence: docs/spec/dialects.md and its Korean pair add "Rendered statements": statement order by use and document order, quoted identifiers, one line per statement, the column type and default of each dialect, identity, literals in dialect form (SQLite decimals scaled by 10^s, MySQL backslashes doubled), keys, indexes, foreign keys (MySQL and PostgreSQL ALTER TABLE after all tables, SQLite inline), declared checks, renderer CHECKs named <table>$<column> with their exact text for every type and precision, and the immutable and audit triggers named <table>$<event> with PostgreSQL functions. tests/dbspec/ddl.json holds 5 document sets (every type and default, keys, indexes and foreign keys with a forward reference, checks in dialect form, immutable and audit, use across documents) with the statements of the three dialects and behavior steps. make dbspec-ddl-check applies them to MySQL 8.4.11, PostgreSQL 17.11 and SQLite 3.53.4 and runs the steps: 15 probes pass in 141 ms. The first run found that PostgreSQL stores the input hour 24 of a timestamp as the next midnight, which the record and the vector now state, and that a behavior query left the MySQL keyword change unquoted. | [o] |
| T8.3.2 | Render the T8.3.1 statements in the Go client (requires T8.3.1). | — | Evidence: engine/dbspec/render.go Render(documents, dialect) writes the statements of docs/spec/dialects.md "Rendered statements". It was written before its test, so no Red test preceded it: it produced the statements of tests/dbspec/ddl.json, which make dbspec-ddl-check applied to the three databases with their behavior steps (T8.3.1), and TestRenderVectors in make dbspec-go-check now compares its output with all 15 dialect renderings. The four-language interface-check passes after recording the Go symbols; contracts/interfaces.json declares Dbspec.render once the other clients render (T8.3.6). | [o] |
| T8.3.3 | Render the T8.3.1 statements in the PHP client (requires T8.3.1). | — | Evidence: clients/php/src/Dbspec/Renderer.php with Dbspec::render(array $documents, string $dialect): array writes the statements of docs/spec/dialects.md "Rendered statements"; it re-reads each canonical check text to render every literal for the type of its column. clients/php/tests/dbspec_render_test.php failed with "Call to undefined method Orm\Dbspec\Dbspec::render()" before the renderer existed and now matches all 5 cases of tests/dbspec/ddl.json byte for byte in the three dialects, also with the document list reversed, and rejects an unknown dialect; make dbspec-php-check runs it twice and passes, with stress medians of 237-287 ms under load. The four-language interface-check passes after recording the PHP symbols. | [o] |
| T8.3.4 | Render the T8.3.1 statements in the TypeScript client (requires T8.3.1). | — | Evidence: clients/typescript/src/dbspec/render.ts renderDbspec(documents, dialect) with DbspecDialect writes the statements of docs/spec/dialects.md "Rendered statements"; check.ts reads a canonical check text back into a tree so that each literal is rendered for the type of its column. clients/typescript/tests/dbspec-render.mjs failed with "does not provide an export named renderDbspec" before the renderer existed and now matches all 5 cases of tests/dbspec/ddl.json in the three dialects, also with the document list reversed, and rejects an unknown dialect; make dbspec-ts-check passes 156 tests with stress medians of 129-130 ms under load. The package root (src/index.ts) now exports renderDbspec and dbspecManifest, which T8.2.6.2 left reachable only from dist/dbspec. tsc --noEmit of clients/typescript passes, and the four-language interface-check passes after recording the TypeScript symbols. | [o] |
| T8.3.5 | Render the T8.3.1 statements in the Rust client (requires T8.3.1). | — | Evidence: clients/rust/orm-schema/src/dbspec/render.rs render(documents, dialect) with Dialect { MySql, Postgres, Sqlite } writes the statements of docs/spec/dialects.md "Rendered statements". tests/dbspec_render.rs did not compile before the renderer existed and failed on content against an empty stub; it now matches all 5 cases of tests/dbspec/ddl.json in the three dialects, also with the document list reversed, and make dbspec-rust-check runs it and passes with stress medians of 29-31 ms. One run while the files were being restaged exited 1; its log was overwritten before it was read, and four later runs, one with the Makefile of that moment, passed. fmt, clippy and the four-language interface-check pass after recording the Rust symbols. | [o] |
| T8.3.6 | Compare the rendered statements of the four clients in make dbspec-compare-check (requires T8.3.2-T8.3.5). | — | Evidence: contracts/interfaces.json declares the Dbspec.render rule (Go Render, PHP Dbspec::render, TypeScript renderDbspec, Rust render, each returning statements or the set diagnostics) and the four-language interface-check passes. The four compare runners read tests/dbspec/ddl.json and print the statements of every case in the three dialects; make dbspec-compare-check runs each client twice and the 8 runs agree on 108 cases, the 15 renderings among them. | [o] |
| T8.4 | Introspect MySQL, PostgreSQL and SQLite into dbspec in every client (requires T8.3). | Acceptance: query count independent of table count; CHECK normal-form and SQLite CREATE parsing; restoration of rendered settings by exact renderer equality without markers; unsupported objects reported with locations. | Evidence: T8.4.1–T8.4.6 with T8.4.2.1–T8.4.2.3 and T8.4.3.1: the four clients introspect MySQL, PostgreSQL and SQLite with 9, 7 and 3 catalog queries whatever the table count, restore the schema text of every ddl.json vector and schema document, report the unsupported cases of tests/dbspec/introspect.json, and agree on the 2000-table stress database within 5 seconds each. | [o] |
| T8.4.1 | Specify how each dialect is introspected into a dbspec document (catalog queries whose count does not depend on the table count, the reading of types, defaults, identity, keys, indexes, foreign keys and checks, renderer CHECKs and triggers recognized by equality with the renderer output, and the unsupported objects reported with their location), with round-trip vectors: every vector of tests/dbspec/ddl.json and every schema document is rendered, applied, introspected and compared by schema text on MySQL, PostgreSQL and SQLite, and objects that dbspec does not support are reported (requires T8.3.1). | — | Evidence: docs/spec/dialects.md "Introspection" and its Korean pair state the catalog queries, the type table per dialect, null, identity and default reading, keys, indexes and foreign keys, the renderer CHECK forms, the check decoding per dialect, trigger recognition, the unsupported kinds, their order and the removal of objects that refer to a left-out object; tests/dbspec/introspect.json holds one case per dialect with its documents, statements, expected document and unsupported list. | [o] |
| T8.4.2 | Introspect into dbspec in the Go client (requires T8.4.1). | — | Evidence: engine/dbspec/introspect*.go Introspect(ctx, querier, dialect, name) returns the document and the unsupported objects; the physical tests failed to build without it (undefined: dbspec.Introspect); make dbspec-introspect-check passes 30 round trips of the 5 ddl.json vectors and 5 schema documents on MySQL, PostgreSQL and SQLite with equal schema text and no unsupported object, asserts 8, 7 and 3 queries for every set, and passes the 3 cases of tests/dbspec/introspect.json, whose assertions fail when an expected entry is removed. | [o] |
| T8.4.2.1 | Report each unsupported object once and read SQLite table items exactly in every client (follows T8.4.2). Cause: a table rejected together with one of its objects made the Go introspector dereference a nil table; a multi-column MySQL foreign key with an unsupported action was reported once per column; an unrecognized SQLite table item was reported as kind check named by its text, and a table-level UNIQUE was reported again through its automatic index; a SQLite column with an inline CHECK or REFERENCES clause was read without it. The PHP, TypeScript and Rust ports copied this behavior. | — | Evidence: tests/dbspec/introspect.json gains mysql-dependents-once (an int unsigned primary key column with an index, and a two-column NO ACTION foreign key) and sqlite-table-items (an unnamed CHECK, a table-level UNIQUE, an inline CHECK and an inline REFERENCES); before the fix Go and PHP stopped on the nil table and TypeScript and Rust failed both cases, and after it make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check and dbspec-introspect-rust-check pass 5 unsupported cases and 30 round trips each; docs/spec/dialects.md states the rules. | [o] |
| T8.4.2.2 | Recognize MySQL renderer CHECKs after ALTER TABLE in every client (follows T8.4.2.1). Cause: the 2000-table comparison reported every uuid and time column of the MySQL stress database as unsupported: adding the foreign keys with ALTER TABLE made MySQL write CHECK_CLAUSE again with an _ascii introducer on the uuid pattern and no introducer on the time literals, which no longer equals the renderer form. | — | Evidence: probe mysql.check.alter_rewrites_introducers records that ALTER TABLE t ADD INDEX rewrites regexp_like(...,_utf8mb4\'...) to _ascii and drops the time literal introducers; the vector mysql-checks-after-alter failed in Go, PHP, TypeScript and Rust with "document differs" and passes once each client compares a renderer CHECK with its introducers removed; make dialect-facts-check and the four introspection checks pass. | [o] |
| T8.4.2.3 | Read MySQL checks without the slow catalog join in every client (follows T8.4.2.2). Cause: the 2000-table comparison took 4 minutes 23 seconds to introspect MySQL; the query that joins CHECK_CONSTRAINTS with TABLE_CONSTRAINTS took 63.7 seconds alone on the stress database, while each catalog view read alone took under 0.1 second. | — | Evidence: on the 2000-table MySQL stress database the joined check query took 63.7 seconds and the two queries that replace it 0.09 and 0.08 seconds; the four clients read CHECK_CONSTRAINTS and TABLE_CONSTRAINTS apart and join them by the database-unique check name, failing on a check without a clause; the four introspection checks pass and assert 9 MySQL queries for every set. | [o] |
| T8.4.2.4 | Recognize the MySQL time CHECK of a time(p) column after ALTER TABLE in every client (follows T8.4.2.3). Cause: the first migration plan that raised a column to time(3) reported it unsupported on MySQL: the rewritten CHECK_CLAUSE reads \'00:00:00.000\', which T8.4.2.2 did not remove. | — | Evidence: probe mysql.check.alter_writes_time_precision records that ALTER TABLE t ADD INDEX writes a zero fraction of three digits into the time literal of a time(3) column CHECK; the vector mysql-time-precision-after-alter failed in Go, PHP, TypeScript and Rust and passes once each client also removes the zero fraction of a time literal before comparing; the four introspection checks and make dialect-facts-check pass. | [o] |
| T8.4.3 | Introspect into dbspec in the PHP client (requires T8.4.1). | — | Evidence: clients/php/src/Dbspec Dbspec::introspect(PDO, dialect, name) returns an IntrospectResult with the document and the unsupported objects; clients/php/tests/dbspec_introspect_test.php failed without it (Call to undefined method Orm\Dbspec\Dbspec::introspect()); make dbspec-introspect-php-check passes 30 round trips of the 5 ddl.json vectors and 5 schema documents on MySQL, PostgreSQL and SQLite with equal schema text and no unsupported object, asserts 8, 7 and 3 queries for every set, passes the 3 cases of tests/dbspec/introspect.json, whose assertions fail when an expected entry is removed, and rejects an unknown dialect and a failing catalog query in the exception and silent error modes. | [o] |
| T8.4.3.1 | Introspect 2000 tables in PHP under the default memory limit (follows T8.4.3). Cause: the 2000-table comparison stopped the PHP runner with "Allowed memory size of 134217728 bytes exhausted": the catalog held a ColumnType object for each of 60000 columns and an array for each of about 90000 document lines, and parsing the document came on top. | — | Evidence: clients/php/tests/dbspec_introspect_stress_test.php renders the stress document for SQLite, applies it and introspects it under memory_limit=128M; with the previous catalog it stopped with "Allowed memory size of 134217728 bytes exhausted", and now it passes with peakBytes=118292480, the source schema text and no unsupported object, after the catalog shares one readonly ColumnType per type and keeps each line object as one string; make dbspec-introspect-php-check runs it. | [o] |
| T8.4.4 | Introspect into dbspec in the TypeScript client (requires T8.4.1). | — | Evidence: clients/typescript/src/dbspec/introspect*.ts introspectDbspec(connection, dialect, name) reads a mysql2, pg or node:sqlite connection and returns the document and the unsupported objects; the physical test failed to load without it (does not provide an export named 'introspectDbspec'); make dbspec-introspect-ts-check passes 30 round trips of the 5 ddl.json vectors and 5 schema documents on MySQL, PostgreSQL and SQLite with equal schema text and no unsupported object, asserts 8, 7 and 3 queries for every set, passes the 3 cases of tests/dbspec/introspect.json, whose assertions fail when an expected entry is removed, and rejects an unknown dialect and a failing catalog query. | [o] |
| T8.4.5 | Introspect into dbspec in the Rust client (requires T8.4.1). | — | Evidence: clients/rust/orm-schema/src/dbspec/introspect/*.rs reads the rows of the catalog queries (catalog_queries, read_catalog) and clients/rust/orm/src/dbspec.rs introspect(connection, dialect, name) runs them on a sqlx MySQL, PostgreSQL or SQLite connection and returns the document and the unsupported objects; the physical tests failed to build without it (unresolved import orm::dbspec); make dbspec-introspect-rust-check passes 30 round trips of the 5 ddl.json vectors and 5 schema documents on MySQL, PostgreSQL and SQLite with equal schema text and no unsupported object, asserts 8, 7 and 3 queries for every set, and passes the 3 cases of tests/dbspec/introspect.json, whose assertions fail when an expected entry is removed. | [o] |
| T8.4.6 | Compare the introspected documents of the four clients on the three databases (requires T8.4.2-T8.4.5). | — | Evidence: make dbspec-introspect-compare-check applies the 2000-table, 10000-foreign-key stress document to MySQL, PostgreSQL and SQLite and runs the runners of tests/dbspec/introspect; the four outputs (2490991 bytes each) are equal, keep the source schema text and report no unsupported object, and each introspection stays within 5 seconds: MySQL Go 2.52 s, PHP 2.84 s, TypeScript 1.36 s, Rust 1.67 s; PostgreSQL 0.72, 1.94, 0.92 and 1.00 s; SQLite 0.72, 1.96, 0.92 and 0.98 s. Its first run found T8.4.2.2, T8.4.2.3 and T8.4.3.1. | [o] |
| T8.5 | Diff schemas and chain migration plans in every client (requires T8.4). | Acceptance: dependency-ordered plans, explicit renames in the plan chain, explicit permission for destructive changes. | Evidence: T8.5.1 to T8.5.7 and their follow-ups are complete: docs/spec/plans.md specifies plan documents, chains, the diff, the statement order and the comparison; the four clients pass the shared plan, invalid, chain, parse and comparison cases and agree in make dbspec-compare-check; make dbspec-plan-check and its client variants apply every case to the three databases. | [o] |
| T8.5.1 | Specify plan documents, the plan chain, the diff, the statements of each dialect and the SQLite rebuild in docs/spec/plans.md and its Korean pair, with the shared cases of tests/dbspec/plans.json (requires T8.4). | — | Evidence: docs/spec/plans.md and docs/plans.ko.md state the plan document, its canonical form, the chain, the diff with its changes, widenings, refusals and forced recreations, the ten statement steps and the SQLite rebuild; tests/dbspec/plans.json holds 9 cases with their changes and the statements of MySQL, PostgreSQL and SQLite and 8 invalid cases with their diagnostics; docs-rules-check passes 23 document pairs. | [o] |
| T8.5.1.1 | Keep the SQLite automatic key counter across a rebuild (follows T8.5.1). Cause: the rebuild of docs/spec/plans.md copies the rows into "$rebuild" with INSERT … SELECT, drops the table and renames the copy, which leaves sqlite_sequence at the largest copied key; after rows with the largest keys were deleted, the next insert reuses a deleted key (observed with sqlite3: keys 1, 2, 3, delete 3, rebuild, insert gives 3), while docs/spec/dialects.md "Automatic key" states that keys are never reused. | Acceptance: the rebuild carries the counter of sqlite_sequence, with a tracked plan case that deletes the largest key before the rebuild in every client. | Evidence: docs/spec/plans.md "Statements" states the carry: after CREATE TABLE "$rebuild", a table with an identity column runs INSERT INTO sqlite_sequence (name, seq) SELECT '$rebuild', seq FROM sqlite_sequence WHERE name = '<table>' before the INSERT … SELECT, so the copy keeps the largest key ever given and SQLite updates that row instead of adding one (checked with sqlite3 3.51.0: keys 1, 2, 3, delete 3, rebuild, insert gives 4, and 4 also when every row was deleted). tests/dbspec/plans.json gains the statement in the 9 SQLite rebuilds of identity tables and the case rebuild-keeps-key-counter: table k gains a check, which rebuilds it on SQLite; before, rows 1, 2 and 3 are inserted and 3 is deleted; after, an insert must make SELECT MAX(id) FROM k 4 and no row has id 3. Red: Go TestPlanVectors, PHP clients/php/tests/dbspec_plan_test.php, TypeScript clients/typescript/tests/dbspec-plan.mjs and Rust plan_vectors failed on the SQLite statements (Go, TypeScript and Rust on 8 cases, PHP at its first, rename-table-and-column), and make dbspec-plan-php-check, make dbspec-plan-ts-check and make dbspec-plan-rust-check, which apply each client's own statements, failed on SQLite with SELECT MAX(id) FROM k: got "3"; want "4". Green: the four planners write the statement; the four vector tests pass, and make dbspec-plan-check (the vector statements) and the PHP, TypeScript and Rust plan checks pass rebuild-keeps-key-counter on MySQL, PostgreSQL and SQLite. Gates passed: dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, dbspec-plan-check, dbspec-plan-php-check, dbspec-plan-ts-check, dbspec-plan-rust-check, dbspec-apply-check, dbspec-apply-php-check, dbspec-apply-ts-check, dbspec-apply-rust-check, dbspec-apply-pairs-check, ts-check, rust-check, rust-fmt-check, go-fmt-check, interface-check, docs-rules-check and checklist-check. | [o] |
| T8.5.2 | Parse, chain, diff and write plans in the Go engine (requires T8.5.1). | — | Evidence: engine/dbspec/plan*.go ParsePlan, EmitPlan, Chain, Diff and PlanStatements; the plan tests did not build without them (undefined: ParsePlan); make dbspec-go-check passes the 9 cases (changes, statements of three dialects and canonical emission), the 8 invalid cases and the 4 chain cases of tests/dbspec/plans.json; make dbspec-plan-check passes 27 runs: each case applied to MySQL, PostgreSQL and SQLite reaches its target schema text with its data steps. | [o] |
| T8.5.2.1 | Keep the Go plan writer free of dropped results and unused state (follows T8.5.2). Cause: EmitPlan discarded the diagnostics of ManifestOf with _, and the renderer field tables, which Render and PlanStatements filled, was never read, so PlanStatements built a second renderer for nothing. | — | Evidence: ParsePlan keeps the target schema text it validated and EmitPlan writes it, so no result is discarded; the renderer has no tables field and PlanStatements uses one renderer; make dbspec-go-check passes the plan cases, invalid cases and chains, and make dbspec-plan-check passes 27 runs. | [o] |
| T8.5.2.2 | Reject plan names over 63 bytes and cover plan parse errors with shared cases (follows T8.5.2.1). Cause: the Go ParsePlan checked the dbplan 1 <name> name only against reserved words, so a 64-byte name passed although docs/spec/plans.md requires the name rules, and tests/dbspec/plans.json had no case for the diagnostics of ParsePlan. | — | Evidence: tests/dbspec/plans.json gains 12 parse cases ([rule, line, column, message], the message only for the rule plan); TestPlanParseErrors failed on name-too-long with no diagnostic before the fix and passes after ParsePlan rejects names over 63 bytes; make dbspec-go-check runs it. | [o] |
| T8.5.3 | Parse, chain, diff and write plans in the PHP client (requires T8.5.1). | — | Evidence: clients/php/tests/dbspec_plan_test.php fails without the plan classes (Class "Orm\Dbspec\PlanStatements" not found) and passes cases=9 invalid=8 chains=4 in make dbspec-php-check; make dbspec-plan-php-check applies the 9 cases to MySQL, PostgreSQL and SQLite (runs=27) and reads each target back through PHP introspection. | [o] |
| T8.5.4 | Parse, chain, diff and write plans in the TypeScript client (requires T8.5.1). | — | Evidence: clients/typescript/src/dbspec/plan*.ts parsePlan, emitPlan, chainPlans, diffPlan and planStatements; the plan test failed before them because dist/dbspec/index.js did not export chainPlans; make dbspec-ts-check passes 190 tests, among them the 9 cases (changes, statements of three dialects and canonical emission), the 8 invalid cases and the 4 chain cases of tests/dbspec/plans.json and the unknown dialect TypeError; make dbspec-plan-ts-check passes 27 runs: each case applied to MySQL, PostgreSQL and SQLite reaches its target schema text with its data steps; contracts/symbols/typescript.json records the 107 new symbols and interface-check passes. | [o] |
| T8.5.5 | Parse, chain, diff and write plans in the Rust client (requires T8.5.1). | — | Evidence: orm_schema::dbspec parse_plan, emit_plan, chain, diff and plan_statements in clients/rust/orm-schema/src/dbspec/plan*.rs; the plan test did not build without them (unresolved imports); make dbspec-rust-check passes twice the 9 cases (changes, statements of three dialects and canonical emission), the 8 invalid cases and the 4 chain cases of tests/dbspec/plans.json; make dbspec-plan-rust-check passes 27 runs: each case applied to MySQL, PostgreSQL and SQLite through the Rust renderer, plan statements and orm::dbspec::introspect reaches its target schema text with its data steps. | [o] |
| T8.5.6 | Compare the plans of the four clients in make dbspec-compare-check (requires T8.5.2-T8.5.5). | — | Evidence: the PHP, TypeScript and Rust plan tests run the 12 parse cases of tests/dbspec/plans.json like Go TestPlanParseErrors ([rule, line, column, message], the message only for the rule plan); before the fix all three failed on name-too-long with no diagnostic, and now PHP, TypeScript and Rust reject plan names over 63 bytes with the Go message; make dbspec-php-check passes cases=9 invalid=8 chains=4 parse=12 twice, make dbspec-ts-check passes 203 of 203 tests, make dbspec-rust-check passes plan parse errors: 12 cases twice and make dbspec-go-check passes the 12 TestPlanParseErrors subtests; the Go, PHP, TypeScript and Rust compare runners take tests/dbspec/plans.json as a fourth argument and print 69 plan entries (emitted plan, changes and statements of three dialects for 9 cases, diagnostics of 8 invalid and 12 parse cases, order or diagnostics of 4 chains, with the message of every plan and chain diagnostic); make dbspec-compare-check passes 8 runs agree with 186 cases per run, check.test.mjs passes 5 tests including a different plan message, and a Go runner printing table and name swapped failed at plans/cases/create-from-empty/changes. | [o] |
| T8.5.7 | List the differences between two schemas in every client (follows T8.5.2-T8.5.5). Cause: the plan diff refuses type, identity, primary key and column order changes instead of listing them and needs an allow drop line for every drop, so a tool that compares a database with a model cannot show every difference. | Acceptance: a shared comparison API and vectors list every difference between two schema texts as [kind, table, name], including the changes a plan refuses, identically in the four clients. | Evidence: docs/spec/plans.md "Comparison" and its Korean pair specify the comparison, placed in the plan document because it shares the plan diff's kinds and widening: both sides are documents of schema texts, and a side that is not is a compare diagnostic naming it; tables and columns match by name; 21 kinds (create_table, drop_table, drop_column, add_column, alter_column, change_column_type, change_column_identity, reorder_columns, change_primary_key, drop_/add_ of unique, index, foreign_key, check, immutable and audit) come in table name order, kind order and name order, and the comparison is empty exactly when the schema texts are equal. tests/dbspec/plans.json has 11 comparisons cases (same schema, tables from empty and on one side, columns, identity and primary key, column order, objects, settings and three sides that are not schema texts). Go CompareSchemas, PHP Dbspec::compareSchemas, TypeScript compareSchemas and Rust compare_schemas implement it. Before the implementations, with a stub that returned no difference, Go TestCompareSchemas failed 10 of 11 cases, the PHP plan test stopped at plan/comparison/tables-from-empty, the TypeScript plan test failed 10 of 45 tests and Rust plan_comparisons reported 10 failures; now TestCompareSchemas passes 11 subtests, PHP prints comparisons=11, TypeScript passes 45 of 45 and Rust passes plan comparisons: 11 cases. The four compare runners print every comparison as plans/comparisons/<id> with | kind table name lines or ! compare 1 1 <message>; with the TypeScript runner of the commit before, make dbspec-compare-check failed with typescript 1 differs from go 1 in plans/comparisons/same-schema and runners.test.mjs failed its two new TypeScript cases (comparisons missing, comparisons[0].target not an array); now the 85 runner tests pass and the 8 runs agree on 213 cases. TestCaseHarnessReportsOnlyFailure also requires a FAIL line for a comparison case. dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, rust-check, rust-fmt-check, go-fmt-check, interface-check, docs-rules-check and checklist-check pass. | [o] |
| T8.5.6.1 | Reject a missing vector section or field in the dbspec compare runners (follows T8.5.6). Cause: the Go runner decodes the vectors into json structs and the Rust runner reads them with into_iter().flatten(), unwrap_or_default and filter_map, so a missing section, id or line becomes empty instead of an error, and runners that agree on the empty value pass the comparison. | Acceptance: every runner rejects a missing or mistyped section, id, document or line with a located error and a nonzero exit; a test removes a section and a field and sees each runner fail. | Evidence: tests/dbspec/compare/runners.test.mjs runs the Go, PHP, TypeScript and Rust runners (tests/dbspec/compare/runners.mjs, shared with check.mjs) on 19 copies of the vectors, each with one section or field removed or mistyped (canonical, hashes, canonical[0].id, invalid[0].main, the main document, a document line, crlf, hashes[0].documents, ddl cases and cases[0].id, plans chains, cases[0].source, invalid[0].plan, chains[0].plans[0] and parse[0].id, mermaid round_trip, export[0].documents, import[0].mermaid and invalid[0].mermaid[0]), and requires a nonzero exit with <file>: <location> <problem> on stderr. Before the change 76 of its 77 tests failed: 48 runs exited 0 (Go 13, PHP 12, TypeScript 6, Rust 17), and the other 28 failed without the located error (Go json decoder messages without the file, PHP warnings and a TypeError, TypeScript TypeErrors, Rust mermaid vectors: round_trip is not an array). Each runner now reads every vector file through a reader that requires each section, case object, id, main and its document, document set, line array and string line and the source key (null or lines), takes an absent crlf or mixed as false and rejects a non-boolean one, and names the file of a JSON syntax error; the 77 tests pass. make dbspec-compare-check runs the test after the builds and the 8 runs agree on 202 cases; make dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, rust-fmt-check, go-fmt-check, rust-check, interface-check, docs-rules-check and checklist-check pass. | [o] |
| T8.6 | Apply plans with lock, step journal, step events, verification and recovery in every client (requires T8.5). | Acceptance: apply then introspect then diff is empty on all three databases; an interrupted non-transactional step is detected from the catalog. | Evidence: T8.6.1 to T8.6.6 and their follow-ups are complete: docs/spec/plans.md "Apply" specifies the lock, history, state, statements, verification, events, recovery and failure shapes; make dbspec-apply-check and its PHP, TypeScript and Rust variants pass 17 runs each on MySQL, PostgreSQL and SQLite, including MySQL recovery from the catalog; make dbspec-apply-pairs-check continues a chain across every ordered pair of clients in 48 runs. | [o] |
| T8.6.1 | Specify applying plans in docs/spec/plans.md "Apply" and its Korean pair: the lock, the history table dbspec$plans that introspection leaves out, the state and drift checks, the transactions of each dialect, verification, events and MySQL recovery from statement effects (requires T8.5.1). | — | Evidence: docs/spec/plans.md "Apply" and docs/plans.ko.md state the MySQL and PostgreSQL locks and the SQLite transaction, the history table and its columns, the state, drift and chain errors, the transactions of each dialect, verification with nothing unsupported, the five events, idempotent apply and recover, and recovery from the catalog effect of the interrupted MySQL statement; docs/spec/dialects.md leaves dbspec$plans out of introspection; docs-rules-check passes. | [o] |
| T8.6.2 | Apply and recover plans in the Go engine (requires T8.6.1). | — | Evidence: engine/dbspec/apply.go Apply, Recover, ApplyEvent and ApplyError; the apply test did not build without them (undefined: dbspec.ApplyError); make dbspec-apply-check passes 16 runs: the two-plan chain with two done history rows, matching events and a second apply without events on MySQL, PostgreSQL and SQLite; drift, lock and verify errors on the three; rollback after a failed statement on PostgreSQL and SQLite; MySQL recovery after a stop before and after a statement, which fails when the effect check is disabled; introspection leaves dbspec$plans out and make dbspec-introspect-check, dbspec-plan-check and dbspec-ddl-check pass. | [o] |
| T8.6.2.1 | Report the cleanup errors of a failed Go apply (follows T8.6.2). Cause: when the apply body failed, Go dropped the error of RELEASE_LOCK, pg_advisory_unlock, ROLLBACK, COMMIT and PRAGMA foreign_keys = ON, ignored the restore after a failed BEGIN IMMEDIATE, accepted an advisory unlock that released nothing, read a MySQL effect query without a row as an absent effect, and dropped the error of closing the rows, against the rule that every error is reported. | — | Evidence: engine/dbspec/apply_test.go TestApplyReportsCleanupErrors and TestMySQLEffectRequiresRow wrap a SQLite connection to inject failures; before the fix they failed (stop, want both errors; locked: … begin failed, want locked and the restore error; no error) and they pass after apply joins every cleanup error with errors.Join, rejects an unlock that released nothing and reports a missing effect row; make dbspec-go-check runs them and make dbspec-apply-check still passes 16 runs. | [o] |
| T8.6.2.2 | Assert every apply cleanup error in every client (follows T8.6.2.1). Cause: only PHP has a case for a PostgreSQL unlock that released nothing (unlock_not_held); the Go, TypeScript and Rust cleanup behaviour is not asserted by equivalent cases, and the PHP Dbspec::apply documentation says every failure is an ApplyError although history write errors and event handler exceptions pass through unchanged. | Acceptance: Go, PHP, TypeScript and Rust each run the cleanup cases (failed ROLLBACK, failed restore after BEGIN IMMEDIATE, effect query without a row, unlock that released nothing) and the client documentation states the failure shapes. | Evidence: the new PostgreSQL run unlock_not_held of tests/dialects/apply_physical_test.go, clients/typescript/tests/dbspec-apply-physical.mjs and clients/rust/orm/tests/dbspec_apply.rs releases the advisory lock from an event; it failed in TypeScript (Missing expected rejection) and Rust (Ok(())), which ignored the result of pg_advisory_unlock, and passes after both reject an unlock that released nothing (Rust as ApplyError::LockNotHeld); the Go run failed (<nil>) with the check disabled once and passes restored. clients/typescript/tests/dbspec-apply-cleanup.mjs and clients/rust/orm/tests/dbspec_apply_cleanup.rs run 3 cases each (failed ROLLBACK, failed restore after a failed BEGIN IMMEDIATE, effect query without a row through a scripted MySQL connection); they passed at once and failed with the cleanup join and the row check broken once (Error: stop alone, Err(Event(Stop)), unexpected query UPDATE / unexpected statement UPDATE), then passed restored. Go returned a failure without cleanup errors wrapped by errors.Join; the new check of TestApplyReportsCleanupErrors failed (&errors.joinError{…}, want the event error itself) and passes after joinCleanup returns a single error unchanged. Rust ApplyError::Cleanup holds its cleanup error as an ApplyError; docs/spec/plans.md "Apply" states the failure shape of every client and the PHP Dbspec::apply docblock lists the errors that propagate unchanged. make dbspec-go-check, dbspec-ts-check, dbspec-rust-check (2 runs of 3 cases), dbspec-php-check, dbspec-apply-check (17 runs), dbspec-apply-php-check (17), dbspec-apply-ts-check (17), dbspec-apply-rust-check (17), rust-check, rust-fmt-check, go-fmt-check, interface-check and docs-rules-check pass. | [o] |
| T8.6.3 | Apply and recover plans in the PHP client (requires T8.6.1). | — | Evidence: clients/php/src/Dbspec/PlanApply.php behind Dbspec::apply and Dbspec::recover, with ApplyEvent and ApplyError (codes locked, interrupted, drift, chain, failed, verify); the apply test failed before them with Call to undefined method Orm\Dbspec\Dbspec::apply(); make dbspec-apply-php-check passes 16 runs: the two-plan chain with two done history rows, 2 plan, 2 verified and 2 done events with equal statement and applied counts, and a second apply without events on MySQL, PostgreSQL and SQLite; drift, lock and verify errors on the three; rollback after a stopped statement on PostgreSQL and SQLite; MySQL recovery after a stop before and after a statement, and recover_after_applied fails with Unknown column 'mail' when the effect check always reports the effect missing; PHP introspection leaves dbspec$plans out with the table queries of the Go engine byte for byte; make dbspec-php-check, dbspec-introspect-php-check, dbspec-plan-php-check and interface-check pass. | [o] |
| T8.6.3.1 | Report the cleanup errors of a failed PHP apply (follows T8.6.3). Cause: PlanApply copied the earlier Go behaviour: when the apply body failed it dropped the error of RELEASE_LOCK, pg_advisory_unlock, ROLLBACK, COMMIT and PRAGMA foreign_keys = ON, ignored the restore after a failed BEGIN IMMEDIATE, accepted an advisory unlock that released nothing, read a MySQL effect query without a row as the value false, and never checked closing a result, against the rule that every error is reported. | — | Evidence: clients/php/tests/dbspec_apply_cleanup_test.php wraps a SQLite PDO (FailingPdo, FailingStatement) to inject failures; before the fix its 4 cases failed (RuntimeException "stop" alone; want it with the cleanup errors; PDOException "restore failed" alone, the locked error lost; UnexpectedValueException "Catalog value 0 is bool, not an integer"; no error for a result that cannot be closed) and the new PostgreSQL run unlock_not_held of clients/php/tests/dbspec_apply_test.php failed with succeeded; they pass after PlanApply::finish runs every cleanup step and throws the single error unchanged or an ApplyCleanupError whose previous throwable is the first failure and whose cleanup lists the later errors, queryValue reports a missing row and a failed close, and the PostgreSQL unlock rejects a lock it did not hold; make dbspec-php-check runs the cleanup test twice and make dbspec-apply-php-check passes 17 runs; interface-check passes with the recorded PHP symbols. | [o] |
| T8.6.4 | Apply and recover plans in the TypeScript client (requires T8.6.1). | — | Evidence: clients/typescript/src/dbspec/apply.ts applyPlans, recoverPlans, DbspecApplyEvent and DbspecApplyError; the apply test did not load without them (does not provide an export named 'DbspecApplyError'); make dbspec-apply-ts-check passes 16 runs: the two-plan chain with two done history rows, two plan, verified and done events, equal statement and applied events and a second apply without events on MySQL, PostgreSQL and SQLite; drift, lock and verify errors on the three; rollback after a stop on PostgreSQL and SQLite; MySQL recovery after a stop before and after a statement, where recover_after_applied fails when the effect check always reports absent; introspection leaves dbspec$plans out with the Go query texts and make dbspec-introspect-ts-check, dbspec-plan-ts-check, dbspec-ts-check, ts-check and interface-check pass. | [o] |
| T8.6.5 | Apply and recover plans in the Rust client (requires T8.6.1). | — | Evidence: orm::dbspec apply, recover, ApplyConnection, ApplyEvent and ApplyError in clients/rust/orm/src/dbspec/apply.rs; the apply test did not build without them (unresolved imports orm::dbspec::apply, orm::dbspec::recover); make dbspec-apply-rust-check passes 16 runs: the two-plan chain with two done history rows, matching events and a second apply without events on MySQL, PostgreSQL and SQLite; drift, lock with a second session and verify errors on the three; rollback after a stopped statement on PostgreSQL and SQLite; MySQL recovery after a stop before and after a statement, and recover_after_applied fails when the effect check is disabled; Rust introspection leaves dbspec$plans out with the Go query texts, and make dbspec-introspect-rust-check (10 sets, 7 cases), dbspec-plan-rust-check (27 runs), dbspec-rust-check, rust-check and interface-check pass. | [o] |
| T8.6.6 | Continue a chain that another client applied, in every pair of clients (requires T8.6.2-T8.6.5). | — | Evidence: tests/dbspec/apply holds an apply runner per client (Go, PHP, TypeScript and clients/rust/orm/examples/dbspec_apply.rs) with the actions apply-first, apply, stop and recover; tests/dialects/apply_clients_physical_test.go TestApplyChainAcrossClients runs, in per-run databases named after the pid and a random value, the 12 ordered pairs on MySQL, PostgreSQL and SQLite (client A applies create-from-empty, client B the chain) and 12 MySQL recoveries (A stops after statement 1 of rename-table-and-column, the row is running 1, B's apply is error interrupted and B recovers), requiring the 2 history rows with their hashes, done, steps and applied_at, and the introspected schema text of the chain's target. It passed at once; with PHP writing an empty from_hash instead of empty once, the 12 runs where PHP applies first failed (go apply: "error chain" after 20ms, want "ok" and differing history rows) and the other 36 passed, then all passed restored. make dbspec-apply-pairs-check (48 runs, in the check aggregate), go-fmt-check, rust-fmt-check, rust-check, interface-check and docs-rules-check pass, and no dbspec_pair_ database remains on MySQL or PostgreSQL. | [o] |
| T8.6.7 | Scope the apply lock to one database and accept an empty plan chain (follows T8.6.6). Cause: MySQL GET_LOCK('dbspec$plans') is server-wide and the PostgreSQL advisory lock covers every schema of a database, so applies to two databases or schemas collide with locked; and chain rejects an empty list with "no plan starts from empty", although a project without plans is a valid empty chain for a database without tables. | Acceptance: docs/spec/plans.md names the lock per database or schema and the empty chain, and every client follows it with shared cases. | Evidence: docs/spec/plans.md "Lock" names the lock of one database: MySQL GET_LOCK(CONCAT('dbspec$plans$', LEFT(SHA2(DATABASE(), 256), 51)), 0), a 64-character name, the MySQL limit, for a database name of any length, and PostgreSQL pg_try_advisory_lock(hashtext('dbspec$plans'), hashtext(current_schema())); a result other than MySQL 1/0 or PostgreSQL true/false is an error, where Go, PHP, TypeScript and Rust had read a MySQL NULL as locked and PHP a PostgreSQL NULL as locked. "Chain" and "Recovery" state the empty chain: no plan is a valid chain, and its apply creates the history table, requires a database without a table and reports no event. tests/dbspec/plans.json adds the chain case empty (plans: [], order: []). Red: Go TestPlanChains/empty, PHP clients/php/tests/dbspec_plan_test.php, TypeScript plan chain empty and Rust plan_chains failed with "no plan starts from empty"; the new other_database scenario, which applies the chain to a second database (MySQL), schema (PostgreSQL) or file (SQLite) from an event of an apply that holds its lock, failed on MySQL and PostgreSQL in Go (make dbspec-apply-check), PHP, TypeScript and Rust with locked: another session holds GET_LOCK('dbspec$plans') and locked: another session holds the advisory lock of dbspec$plans, and the lock and unlock_not_held scenarios, which hold or release the new key, failed against the old one. Green: the four chain functions return the empty chain for no plan and the four appliers use the new lock name and key; make dbspec-apply-check passes, make dbspec-apply-php-check reports runs=23, make dbspec-apply-ts-check apply runs: 23 and make dbspec-apply-rust-check 23 runs on three databases, each including other_database and empty_chain (apply and recover of the empty chain without an event, zero history rows, drift after a table is created) on MySQL, PostgreSQL and SQLite. The Go, PHP and Rust probe harnesses create and remove the second database, schema or file <name>_b (tests/dialects/README.md). Gates passed: dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, dbspec-plan-check, dbspec-apply-check, dbspec-apply-php-check, dbspec-apply-ts-check, dbspec-apply-rust-check, dbspec-apply-pairs-check, client-unit-check, ts-check, rust-check, rust-fmt-check, go-fmt-check, interface-check, docs-rules-check and checklist-check. | [o] |
| T8.6.8 | Apply plans with one step-by-step guarantee and rollback statements on MySQL, PostgreSQL and SQLite (follows T8.6.2). Cause: docs/spec/plans.md "Apply" gives each database a different guarantee: PostgreSQL rolls a plan back in one transaction, SQLite rolls the whole apply back, and MySQL commits every statement because DDL commits implicitly, so the same plan fails and recovers differently per database; the one-transaction PostgreSQL apply also holds an ACCESS EXCLUSIVE lock on every changed table until commit, which blocks every read and write of those tables for the whole apply, and failed on the first plan of a 2000-table round trip (20014 statements) with out of shared memory. An all-or-nothing schema change needs a maintenance window or a blue-green switch, which the operator chooses and orm cannot enforce. | Acceptance: docs/spec/plans.md states one guarantee for the three databases: DDL runs outside a transaction, statement by statement, each followed by its recorded history step; an interrupted apply stops at a known step and recover decides from the catalog whether the last statement took effect and continues; every statement has a rollback statement, and a rollback undoes exactly the applied steps in reverse order and can itself be interrupted and continued; statements that destroy data move it aside and a final step removes it, so everything stays reversible until that step; lock waits fail fast instead of queueing; the documentation states that atomicity is the operator's choice (maintenance window or blue-green). Every client follows it with shared cases that interrupt and roll back at each step on the three databases, and a tracked case applies the 2000-table plan on all three. | Evidence: docs/spec/plans.md "Steps" and "Apply" and the Korean pair specify one guarantee for the three databases: every statement commits on its own, outside any transaction, with its recorded step; every step has a rollback statement and an effect; dropped tables and columns are hidden under dbspec$hold$<h>$<n> until finalize, added columns are hidden on rollback and restored by a second apply, SQLite rebuilds copy rows both ways through dbspec$rebuild, a rollback of an applied plan fills the NULL rows of a column it makes non-null with the source default or stops with nulls, the only irreversible steps are time and datetime precision widenings, lock waits end after 5 seconds, and atomicity is the operator's choice. Go PlanSteps, Apply, Recover, Rollback and Finalize, PHP Dbspec::planSteps, apply, recover, rollback and finalize, TypeScript planSteps, applyPlans, recoverPlans, rollbackPlans and finalizePlans, and Rust plan_steps, apply, recover, rollback and finalize replace PlanStatements, the plan transactions and the MySQL statement-effect table; the introspection of the four clients leaves out every dbspec$ table and column. tests/dbspec/plans.json lists the steps of 12 cases in three dialects, with the new cases representative and drop-required-column. Red: on the previous implementation the new tests did not build or load (Go undefined: PlanStep, PHP Call to undefined method Orm\Dbspec\Dbspec::planSteps(), TypeScript does not provide an export named 'effectText', Rust unresolved imports orm_schema::dbspec::plan_steps), and with recover and rollback ignoring the catalog effect make dbspec-apply-check failed 69 of 119 runs (mysql.apply.interrupt_00: the rollback left an unsupported trigger). Green: make dbspec-apply-check passes 119 runs: the chain with history and events, drift, lock, a second database, the empty chain through the four commands, an unlock that released nothing, verification, rows_between (rows written after the apply survive the rollback with the hidden column's value or default, a second apply brings back the added column's values, finalize leaves no dbspec$ table or column and a later rollback is irreversible), nulls (has 1 NULL rows, nothing changed) and the stop after every step of representative (25 MySQL, 31 PostgreSQL and 38 SQLite steps), each followed by a rollback to the source, a recover to the target and a stopped rollback that continues, with the history rows asserted each time; TestApplyStressPlan applies the 2000-table document as a first plan in 22000 steps on MySQL (15m9s) and on PostgreSQL with max_locks_per_transaction 64 (1m54s) and in 12000 steps on SQLite (46s), each ending with the history row applied. make dbspec-apply-php-check, dbspec-apply-ts-check and dbspec-apply-rust-check pass the same 119 runs; make dbspec-plan-check and its PHP, TypeScript and Rust variants pass 36 runs each and roll back and apply again every reversible case; make dbspec-apply-pairs-check passes 108 runs (chain, recover and rollback across every ordered pair of clients on three databases); make dbspec-compare-check requires equal steps with their rollback, effect, restore and null checks from the four clients. MySQL and PostgreSQL turn 03:04:05.6 into 03:04:06 when a datetime(6) column narrows to datetime(0) without an error, while narrowing a long varchar fails (MySQL ERROR 1265, PostgreSQL value too long), which makes precision widening the irreversible step; SQLite carries other tables' foreign keys through ALTER TABLE … RENAME TO only with legacy_alter_table off, which every command sets. Gates passed: dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, dbspec-plan-check, dbspec-plan-php-check, dbspec-plan-ts-check, dbspec-plan-rust-check, dbspec-apply-check, dbspec-apply-php-check, dbspec-apply-ts-check, dbspec-apply-rust-check, dbspec-apply-pairs-check, rust-check, rust-fmt-check, go-fmt-check, interface-check, docs-rules-check and checklist-check. | [o] |
| T8.7 | Export standard Mermaid erDiagram and import it with a complete list of dropped information (requires T8.2). | — | Evidence: T8.7.1 to T8.7.6 are complete: docs/spec/mermaid.md specifies export and import; Go, PHP, TypeScript and Rust pass the 13 cases of tests/dbspec/mermaid.json, including the schema/bench.dbs round trip with 17 objects left out by export and 9 by import; make dbspec-compare-check finds the four clients byte-identical on every Mermaid case. | [o] |
| T8.7.1 | Specify Mermaid export and import in docs/spec/mermaid.md and its Korean pair, with the shared cases of tests/dbspec/mermaid.json (requires T8.2). | — | Evidence: docs/spec/mermaid.md and docs/mermaid.ko.md state the export format (types with decimal(p-s), PK/FK/UK keys, column suffix comments, relationship lines and labels) and what export leaves out, and the import grammar, what import carries over and the dropped kinds; tests/dbspec/mermaid.json holds 1 export case, 2 import cases (the export read back and a diagram of another tool) and 4 invalid cases; docs-rules-check passes 24 pairs. | [o] |
| T8.7.2 | Export and import Mermaid diagrams in the Go engine (requires T8.7.1). | — | Evidence: engine/dbspec/mermaid.go ExportMermaid and ImportMermaid; the test did not build without them (undefined: ExportMermaid); make dbspec-go-check passes the export, import and invalid cases of tests/dbspec/mermaid.json; exporting schema/bench.dbs and importing it back reproduces its tables, columns, keys and foreign keys and reports the 16 objects that export leaves out. | [o] |
| T8.7.2.1 | Correct the Mermaid defects of every client (follows T8.7.2). Cause: Go ImportMermaid panics (index out of range) on a relationship label whose column count differs from its referenced column count; Go mermaidImportType ignores the strconv.Atoi error, so a length or precision above the int range becomes 0, and the clients decide such a type at different points, so their dropped lists differ for a key column; export reports only table and column comments and drops the comments of primary key, unique, index, foreign key, check, settings and use lines, closing comments of blocks and trailing comments of the document silently; the import comment pattern accepts nullidentity as null identity; two relationships over the same FK columns add the index ix_<table>_<columns> twice and report it twice; the T8.7.2 evidence claims 16 objects for the schema/bench.dbs round trip, the engines report 17, and no test asserts it. | — | Evidence: the cases of tests/dbspec/mermaid.json grew to 2 export, 6 import, 4 invalid and 1 round trip case. Before the fix Go panicked on import/label-column-count (index out of range [1] with length 1) and, with that corrected, failed export/every-comment, import/type-ranges (tables c and d dropped), import/comment-spacing (nullidentity read as null identity) and import/repeated-relationship (ix_b_a_id reported three times); PHP failed export/every-comment, stopped on import/label-column-count with TypeError (Argument #2 ($n) must be of type string, null given) and failed import/type-ranges, import/comment-spacing and import/repeated-relationship; TypeScript and Rust failed export/every-comment, import/type-ranges (table e with varchar(16384) code PK dropped), import/comment-spacing and import/repeated-relationship. All four pass the 13 cases after each client skips the columns of a label whose column count differs from its referenced column count, decides the dbspec type ranges (varchar 1-16383, time and datetime 0-6, decimal p 1-18 and s 0-p) before keys are read, reports every comment export leaves out with the kinds docs/spec/mermaid.md now states, requires single spaces between the comment parts, and adds the index of a column list once. The round trip case asserts in every client that exporting schema/bench.dbs drops exactly the 17 listed objects and importing the export drops exactly the 9 listed objects and gives back the tables, columns, primary keys and foreign keys of schema/bench.dbs. dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, interface-check, go-fmt-check, rust-fmt-check, rust-check, docs-rules-check and checklist-check pass. | [o] |
| T8.7.3 | Export and import Mermaid diagrams in the PHP client (requires T8.7.1). | — | Evidence: clients/php/src/Dbspec/Mermaid.php behind Dbspec::exportMermaid and Dbspec::importMermaid; the Mermaid test failed before them with Call to undefined method Orm\Dbspec\Dbspec::exportMermaid(); make dbspec-php-check runs clients/php/tests/dbspec_mermaid_test.php twice, and each run passes the 7 cases of tests/dbspec/mermaid.json: 1 export case with its Mermaid text and 7 dropped objects, 2 import cases with their emitted documents and 2 and 7 dropped objects, and 4 invalid cases with their mermaid diagnostics; interface-check passes. | [o] |
| T8.7.4 | Export and import Mermaid diagrams in the TypeScript client (requires T8.7.1). | — | Evidence: clients/typescript/src/dbspec/mermaid.ts exportMermaid and importMermaid; the test did not load without them (does not provide an export named 'exportMermaid'); make dbspec-ts-check passes the 1 export, 2 import and 4 invalid cases of tests/dbspec/mermaid.json in clients/typescript/tests/dbspec-mermaid.mjs, with the Mermaid text, emitted document, dropped [kind, table, name] and diagnostics [rule, line, column] equal to the vectors. | [o] |
| T8.7.5 | Export and import Mermaid diagrams in the Rust client (requires T8.7.1). | — | Evidence: orm_schema::dbspec export_mermaid, import_mermaid and RULE_MERMAID in clients/rust/orm-schema/src/dbspec/mermaid.rs; the Mermaid test did not build without them (unresolved imports orm_schema::dbspec::export_mermaid, orm_schema::dbspec::import_mermaid); make dbspec-rust-check passes twice the 7 cases of tests/dbspec/mermaid.json: 1 export case with its Mermaid text and dropped objects, 2 import cases with their emitted documents and dropped objects, and 4 invalid cases with their [rule, line, column] diagnostics; rust-check and interface-check pass. | [o] |
| T8.7.5.1 | Run the Rust dbspec tests twice, not four times (follows T8.7.5). Cause: merging the Rust Mermaid test and the public model test left make dbspec-rust-check with two pairs of the same cargo test line, one ending in dbspec_model and one in dbspec_mermaid, so every other Rust dbspec test ran four times. | — | Evidence: the recipe holds one cargo test line twice with --test dbspec_model --test dbspec_mermaid; make dbspec-rust-check passes and prints PASS mermaid/export/shop and pass dbspec_model twice each. | [o] |
| T8.7.6 | Compare the Mermaid results of the four clients in make dbspec-compare-check (requires T8.7.2-T8.7.5). | — | Evidence: tests/dbspec/compare/check.mjs takes tests/dbspec/mermaid.json as a fifth argument, and the Go, PHP, TypeScript and Rust runners (tests/dbspec/compare/go, php.php, typescript.mjs and clients/rust/orm-schema/examples/dbspec_compare.rs) print every export, import, invalid and round trip case: the Mermaid text or emitted document, the dropped objects as = kind<TAB>table<TAB>name without reasons, the diagnostics as ! rule line column, and for the round trip the export of schema/bench.dbs and then its import. make dbspec-compare-check prints 200 cases per run instead of 186 and its 8 runs agree; with the TypeScript Mermaid source of the commit before T8.7.2.1 built, it fails with typescript 1 differs from go 1 in mermaid/export/every-comment. tests/dbspec/compare/check.test.mjs passes 6 tests, including a different Mermaid dropped object, and docs/spec/mermaid.md and its Korean pair describe the comparison. dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, interface-check, go-fmt-check, rust-fmt-check, rust-check, docs-rules-check and checklist-check pass. | [o] |
| T8.7.6.1 | Make the dbspec test harnesses report failures and missing inputs (follows T8.7.6). Cause: Go TestMermaidVectors logs PASS mermaid/<case> after t.Errorf, so a failing case also prints a PASS line, and tests/dbspec/compare/php.php reads its inputs with (string) file_get_contents(...), so an unreadable file becomes empty text. | Acceptance: a failing case prints only its failure, and every runner rejects an unreadable input with its path and a non-zero exit. | Evidence: engine/dbspec/harness_test.go TestCaseHarnessReportsOnlyFailure runs TestFailingCaseFixture as a child go test with DBSPEC_FAILING_CASE_FIXTURE=1, which checks the first Mermaid export, import, invalid and round trip case and the first plan, invalid plan, chain and parse case each with one wrong expectation, and requires a FAIL <case> line and no PASS <case> line for each of the 8. Before the change it failed for all 8 (no FAIL line, PASS line for the failing case), because the Mermaid and plan checks logged PASS after t.Errorf and never a FAIL line; the Mermaid checks also logged deadline=5s without a deadline. The checks of TestMermaidVectors, TestPlanVectors, TestPlanChains and TestPlanParseErrors now return their mismatches joined with errors.Join to runTimed, which enforces the 5 s deadline and logs either PASS or FAIL, and report a failed JSON encoding; the test passes and make dbspec-go-check runs it. The other Go dbspec tests already logged through runTimed or logResult. tests/dbspec/inputs.test.mjs runs the four compare runners with a missing file and a directory in place of each of the 5 inputs and of the round trip document, the four apply runners with a missing and a directory plans file and the Rust stress example with a missing and a directory document, and requires a nonzero exit with <path>: on stderr. Before the change 11 of its 58 tests failed: the PHP compare runner exited 0 for a directory stress document and a directory round trip document, which PHP reads as empty text, and the PHP stress document, the PHP apply plans file, the TypeScript compare stress and round trip documents and the TypeScript apply plans file failed without <path>: (a PHP TypeError or cannot read <path>, uncaught Node errors whose EISDIR message has no path). tests/dbspec/input.php dbspec_read_input and tests/dbspec/input.mjs readInput reject a directory or an unreadable file with <path>: <reason> and exit 1; the PHP and TypeScript compare and apply runners read every input through them, and the 58 tests pass in make dbspec-compare-check, which builds the Rust apply and stress examples for them. make dbspec-compare-check (8 runs agree on 202 cases), dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, rust-fmt-check, go-fmt-check, rust-check, interface-check, docs-rules-check and checklist-check pass. | [o] |
| T8.7.6.2 | Read the engine test vectors strictly and comment the harnesses in Korean (follows T8.7.6.1). Cause: engine/dbspec/plan_test.go and mermaid_test.go decode tests/dbspec vectors into json structs, so a missing field becomes empty, and the comments of tests/dbspec/compare/{check.mjs, typescript.mjs, php.php, go/main.go}, clients/rust/orm-schema/examples/dbspec_compare.rs and the dbspec Makefile blocks are English, against the Korean comment rule. | Acceptance: the engine tests reject a missing or wrongly typed vector field with its location, and those comments are Korean. | Evidence: engine/dbspec/vector_reader_test.go reads a tests/dbspec vector file as JSON values and requires version 1, every section as a nonempty array of case objects with a string id, and every field the engine tests use with its type: string lines, line maps, [kind, table, name] triples, diagnostics of 4 items ([rule, line, column, message or null]) or 3 items for Mermaid, {line, column, rule} errors, a present source that is null or lines, crlf and mixed as absent or boolean, and a chain case with exactly one of order and errors; a missing or mistyped value is tests/dbspec/<file>: <location> <problem>. The loaders of plans.json (plan_test.go, also used by apply_test.go), mermaid.json (mermaid_test.go), cases.json (vectors_test.go, manifest_test.go) and ddl.json (render_test.go) decode through it instead of json structs. TestVectorLoadersRejectMalformedVectors removes or mistypes 15 values across the four files and requires that exact error; with the json struct decoding it failed all 15, 12 without any error and 3 with a json error that names neither the file nor the location, and now passes 15 of 15 in make dbspec-go-check with every vector test. The comments of tests/dbspec/compare/{check.mjs, typescript.mjs, php.php, go/main.go}, clients/rust/orm-schema/examples/dbspec_compare.rs, the 20 dbspec Makefile blocks and the English comments of engine/dbspec/{vectors, render, manifest, harness}_test.go are Korean. dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, rust-check, rust-fmt-check, go-fmt-check, interface-check, docs-rules-check and checklist-check pass. | [o] |
| T8.9 | Name dbspec files .dbs and make the header the file signature (requires T8.7). Cause: dbspec files used the extension dbspec only because DbSchema also uses .dbs; with a header signature, .dbs tells the two formats apart. | Acceptance: every client, the CLI, the build tools, the fixtures, the shared vectors and the documents use .dbs and no path with the extension dbspec remains; docs/spec/dbspec.md and its Korean pair state that the first line dbspec 1 <document> is the file signature, that a tool which reads a file checks that the file starts with the bytes dbspec before parsing, and that a file without them is a signature error naming the file as not a dbspec document; a shared vector with a DbSchema project file (XML) and one with an empty file give the signature error in all four clients. | Evidence: the ten tracked documents (schema/bench, contracts/fixtures audit, clock, clock_mark, decimal_schema, refusal, rollback, secret_config, zone, clients/rust/tests/nonnull) are moved to .dbs with git mv, and every reference follows: the Go orm-gen gen --document, PHP orm-gen gen, TypeScript orm-gen gen --schema usages, orm-build MANIFEST_FILE orm_manifest.dbs, the Makefile stress document clients/rust/target/dbspec/stress.dbs, contracts/features.json, contracts/rules.json, contracts/interfaces.json, tests/dbspec/mermaid.json, the scripts, tests, README files and documents with their Korean pairs; a git grep for paths with the extension dbspec lists only one CHANGELOG line about a past change. Each client has one reader: Go dbspec.ReadFile (engine/dbspec/file.go), PHP Dbspec::readFile with ReadResult, TypeScript readDbspecFile (src/dbspec/file.ts) and Rust orm_schema::dbspec::read_file with ReadError; a file that does not start with the bytes dbspec gives exactly one signature diagnostic at 1:1 with the message <path> is not a dbspec document and no text, before any parse. The tools use it: Go runtimemodel.LoadFiles (orm-gen, the interface checker), PHP RuntimeModel::files (orm-gen) and scripts/decimal-db-setup.php, the TypeScript orm-gen, Rust orm-build and the integration binary, the four compare runners for the stress and round trip documents, the Rust stress harness, and the Go, PHP, TypeScript and Rust tests that read schema documents. Dbspec.readFile is an interface entry of contracts/interfaces.json and the four symbol records list the readers. tests/dbspec/cases.json gains a files section with tests/dbspec/files/dbschema.dbs (a DbSchema project XML with a table), empty.dbs and shop.dbs. Red before the readers existed: Go undefined: ReadFile, PHP Call to undefined method Orm\Dbspec\Dbspec::readFile(), TypeScript does not provide an export named 'readDbspecFile', Rust cannot find function read_file in module dbspec; through the tools, Go LoadFiles reported 1:1 header: document ?>, PHP orm-gen 1:1: header, TypeScript orm-gen a dbspec text does not start with its header and orm-build 1:1 header for the XML and empty files. Green: Go TestFileVectors and TestLoadFilesRejectsFileWithoutSignature, PHP dbspec_test.php (files=3) and orm_gen_test.php, TypeScript dbspec.mjs and generate.mjs, Rust shared_dbspec_files and orm-build rejects_a_file_without_the_signature pass; dbspec-compare-check prints ! signature 1 1 and = <path> is not a dbspec document identically from the four runners (8 runs agree), and inputs.test.mjs rejects an unsigned stress document in the four runners and the Rust stress harness. docs/spec/dbspec.md "Files" and the rule table state the signature. With TEST_ENV of the main checkout docs-rules-check, checklist-check, git-check, go-fmt-check, rust-fmt-check, interface-check, client-unit-check, rust-check (76 s), ts-check (26 s), client-db-check (403 s), dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check (43 s), dbspec-compare-check (88 s), dbspec-ddl-check, the four dbspec-introspect checks, dbspec-introspect-compare-check (started at 75.6% CPU idle and 7.3 MB/s; 2000 tables in 0.74-2.45 s per client and database), dbspec-plan-check and its PHP, TypeScript and Rust variants, dbspec-apply-check (325 s) and its PHP, TypeScript and Rust variants, dbspec-apply-pairs-check and conformance-result-check pass. make conformance-check passes with 25 vectors × 4 languages identical on the three databases. | [o] |
| T8.9.1 | Check the dbspec signature on given bytes (follows T8.9). Cause: the file reader of each client takes only a path, so a caller that must read files under its own rules (such as one that reads with O_NOFOLLOW under a root and does not follow symbolic links) reads every file twice; the second read ignores the caller's rules, can judge a different version than the caller parses, and its message names whatever path it opened. The readers also differed on bytes that are not UTF-8 after the signature: Go and PHP passed them to the parser, TypeScript replaced them with U+FFFD and Rust returned an I/O error, although docs/spec/dbspec.md requires an encoding error before parsing. | Acceptance: each client offers a byte check that takes the file bytes and a caller-given name and returns the signature diagnostic <name> is not a dbspec document at 1:1, an encoding diagnostic <name> is not valid UTF-8 at the line and column of the first invalid byte, or the UTF-8 text: Go dbspec.ReadBytes, PHP Dbspec::readBytes, TypeScript readDbspecBytes, Rust dbspec::read_bytes; the path reader of each client reads the file and gives its bytes to the byte check with the path as name; the shared file vectors run through both and make dbspec-compare-check compares them; docs/spec/dbspec.md and its Korean pair, contracts/interfaces.json (rule Dbspec.readBytes) and the four symbol records state it. | Evidence: tests/dbspec/cases.json adds the file vector invalid-utf8-file (tests/dbspec/files/invalid-utf8.dbs, encoding at 3:8). Red: the Go file test did not build (undefined: ReadBytes), and on that file the compare runners printed ! encoding 3 8 from the Go and PHP parsers without the reader message, ! name.format 3 7 from TypeScript, which had decoded the byte as U+FFFD, and the Rust runner exited with "invalid utf-8 sequence of 1 bytes from index 22". Green: every runner prints files/<id> from the path reader and files/<id>/bytes from the byte check with the same rule, position and <name> message, and make dbspec-compare-check passes ("8 runs agree", 1116 output cases per run); the four file vectors pass through both readers in Go TestFileVectors (8 subtests), PHP dbspec_test.php (files=8), TypeScript dbspec.mjs (245 tests, 0 failed) and Rust shared_dbspec_files. Owner checks run: go-fmt-check, rust-fmt-check, interface-check, client-unit-check, ts-check, dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, rust-check, docs-rules-check, checklist-check and git-check; node scripts/features/check.mjs passes with 17 feature contracts. | [o] |
| T8.8 | Merge feat/dbspec-T8 into main and remove the worktree (requires T8.0–T8.7, T8.9). | Acceptance: obsolete development rules (auto, COLUMN_UNSELECTED, Mermaid as source, full-suite timing) are rewritten. | Evidence: T8.8 merges feat/dbspec-T8 into main after T8.8.1-T8.8.5 brought main's commits through N19.1 onto the branch; the last full make check passed 43 of 44 steps on T30 and the remaining feature-check passed after T30; the obsolete auto rule of AGENTS.md (+ko) now states the dbspec identity rule (T8.8), and COLUMN_UNSELECTED and the Mermaid diagram rule remain current. The worktree orm-dbspec-T8 and the branch are removed. | [o] |
| T8.8.1 | Integrate main into feat/dbspec-T8 (requires T8.0–T8.7). Cause: main gained 11 commits after the branch point (N3.1..N15): N3.1 loads the PHP models of several schemas, N3.2 states one schema rule for all clients, N3.3 registers installed schemas, N10, N14 and N15 write the client clock, the database clock and the migration ledger with microseconds, N11 reports every driver error with a code, N12 types only model chain calls in the TypeScript scan, and N13 reports failed rollbacks with both errors. Several of them edit paths that T8.2.6.4 removed (the Mermaid fixtures and schema.json, PHP SchemaMigrate, TypeScript tools/migrate.ts and tools/plan.ts, Rust orm-gen migrate, internal/ormgen migrate and its ledger orm_schema_migrations), N13 and T8.0.14.1/T8.0.14.3 change the same transaction-end reporting, and the microsecond clocks of N10, N14 and N15 meet the local datetime(p) and UTC connection rule of T8.0.9 and T8.0.9.1. | Acceptance: main is merged into chore/integrate-T8.8 with every conflict resolved by meaning: the Mermaid source path stays removed; the schema sets and registration of N3.1 and N3.3 work on dbspec document sets and manifests and N3.2 is restated for them; one transaction-end error form exists in every client; the clock rule is stated once and agrees with T8.0.9; every test of the 11 commits runs on its path or is replaced by a named dbspec test; the four symbol records, the gates of the item's evidence and the full Go test suite pass. | Evidence: merge commit "Merge branch 'main' into chore/integrate-T8.8" has parents T8.8.1 and N15. Resolutions: (1) the Mermaid fixtures and schema.json paths, PHP SchemaMigrate, TypeScript tools/migrate.ts and tools/plan.ts, Rust orm-gen migrate and cli.rs, and internal/ormgen migrate, rollback and ledger stay removed; (2) schema sets work per process: PHP Registry and Engine are keyed by manifest hash and every generated PHP model carries manifest_hash; a connection plans each request with the model of its hash; a request of an unloaded manifest, or generated text whose hash differs from its declared hash, is SCHEMA_HASH_MISMATCH before execution in Go, PHP, TypeScript and Rust, also with a cached engine or plan (Go engineFor and Rust Db::plan checked the cache first, and Rust reported SCHEMA_INVALID); register() is not provided because generated code registers its set, and docs/spec/protocol.md restates N3.2; (3) the branch transaction-end code stays with main's error form: ROLLBACK transaction failed (<cause>) and rollback failed (<error>) keeps both errors in every client, is not retried, and is reported also when the server closed the connection (Go no longer treats driver.ErrBadConn as a finished rollback; TestWithContextCancelsInsideTransaction expects ROLLBACK keeping context.Canceled on MySQL and PostgreSQL); (4) docs/spec/protocol.md states one clock rule: UTC client clock with microseconds, MySQL CURRENT_TIMESTAMP(p) from Now(precision) and NOW(6), SQLite relative forms with the six fraction digits of a second slot, SQLite binds the client clock for an omitted default now column (the SQLite database clock has milliseconds: clock_microseconds failed on SQLite in Go, PHP, TypeScript and Rust without it), and every now slot carries the fraction digits of its column (Go TestSoftDeleteClockSlotPrecision); (5) dbspec$plans.applied_at is YYYY-MM-DDTHH:MM:SS.ffffffZ, the TypeScript apply clock is microseconds since the epoch, and docs/spec/plans.md states it. Tests: main's clock, refusal (trigger_refused, check_refused) and rollback (rollback_failed, savepoint_rollback_failed) cases run in the four clients on MySQL, PostgreSQL and SQLite with contracts/fixtures clock, clock_mark, refusal and rollback .dbs; several_schemas stays, register_schema is replaced by installed_elsewhere, edited_manifest and register_edited_manifest by edited_manifest, and unregistered_schema stays in PHP and TypeScript; the ledger cases migration_ledger_microseconds, migration_ledger_whole_seconds and migration_ledger_earlier are replaced by the applied_at assertions of make dbspec-apply-check, dbspec-apply-php-check, dbspec-apply-ts-check, dbspec-apply-rust-check and dbspec-apply-pairs-check with the clock 2026-10-01T00:00:00.123456789Z, because the history column is text and no earlier history with whole seconds exists; the SQLite conformance expectations write_cycle and now_defaults were recorded from four agreeing outputs. Gates on "Merge branch 'main' into chore/integrate-T8.8" with TEST_ENV of the main checkout: checklist-check, docs-rules-check, docs-check (48 pages, 550 internal targets; four raw <nil> and <value> quotes of the checklist became code spans), interface-check, go-fmt-check, rust-fmt-check, rust-check, rust-driver-check, client-unit-check, ts-check, ts-model-check, go-model-check, conformance-check (25 vectors × 4 languages identical on each database), client-db-check, dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, dbspec-ddl-check, the four dbspec-introspect checks, dbspec-plan-check and its PHP, TypeScript and Rust variants, dbspec-apply-check and its PHP, TypeScript and Rust variants, dbspec-apply-pairs-check, example-check, timing-check, go vet ./..., go test -p 1 ./... and git-check pass; feature-check fails only with the 15 "missing coverage kind" errors of T15, as at T10. | [o] |
| T8.8.2 | Integrate main into feat/dbspec-T8 again (follows T8.8.1). Cause: main gained N16 (N16) after T8.8.1: the test entry point of each client arms a rollback fault (Go FailNextRollback with the build tag ormtest, Rust orm::testing with the feature test-faults, TypeScript @polyspec/orm-typescript/testing under the Node condition orm-test, PHP Orm\Testing\Faults), and the next rollback of a transaction whose callback failed is reported with the catalog code FAULT inside ROLLBACK; it edits the transaction ends of Go, PHP and TypeScript that T8.8.1 changed to one ROLLBACK form, the TypeScript and PHP rollback tests that run on dbspec fixtures, and the Rust and client database gates. | Acceptance: main is merged into feat/dbspec-T8; each client reports the fault through its single transaction-end error form (transaction failed (<cause>) and rollback failed (<error>)), the case rollback_fault runs on contracts/fixtures/rollback in the four clients on MySQL, PostgreSQL and SQLite, the four symbol records list the fault symbols, and docs-rules-check, checklist-check, git-check, go-fmt-check, rust-fmt-check, rust-check, client-unit-check, ts-check, client-db-check, conformance-check, interface-check and every dbspec check pass on the merged tree. | Evidence: the merge resolved 41 paths; the Go, PHP and TypeScript transaction ends report the fault through rollbackFailed, and the comments taken from N16 are in Korean. The fault cases pass on MySQL, PostgreSQL and SQLite: Go TestRollbackFault* 4 tests, PHP 3 cases on each database, TypeScript 3 cases on each database under --conditions=orm-test, Rust --features test-faults --test rollback 10 tests. On the merged tree docs-rules-check, checklist-check, git-check, go-fmt-check, rust-fmt-check, interface-check, client-unit-check, ts-check, client-db-check (500 s), conformance-check, rust-check and every dbspec check pass, among them dbspec-apply-check, dbspec-apply-pairs-check and dbspec-compare-check. dbspec-introspect-compare-check failed twice on the 5 s budget while other runs held the machine (PostgreSQL PHP 8.0-9.0 s, TypeScript 5.5-6.8 s, Rust 6.8 s, MySQL PHP 7.6 s, with the 1-minute load rising to 96). Measured one client and one database at a time, each run started when the CPU was at least 60% idle and the disk moved under 10 MB/s, the 2000-table introspection took 0.73-0.95 s in Go, 1.93-2.56 s in PHP, 0.86-1.22 s in TypeScript (one run at 21.7% idle took 2.58 s) and 0.86-1.20 s in Rust over 5 runs per pair; the process CPU time was 0.43-0.82 s in Go and 1.66-2.04 s in PHP. Started from the same idle state, make dbspec-introspect-compare-check passed with every pair within 0.86-2.64 s. | [o] |
| T8.8.3 | Integrate main into feat/dbspec-T8 again (follows T8.8.2). Cause: main gained N17 (N17) after T8.8.2: clients/php/composer.json requires no PDO driver extension and suggests pdo_mysql, pdo_pgsql and pdo_sqlite, and the case clients/php/tests/sqlite_without_mysql.php, which scripts/php-without-mysql.sh and make php-without-mysql-check run in the official PHP image php:8.5.10-cli-trixie without pdo_mysql, installs the schema through Config(schemaPath:) and schema/schema.json, which the branch removed. | Acceptance: main is merged into feat/dbspec-T8; the case installs schema/bench.dbs, read through Dbspec::readFile, with utils()->schema()->install() and creates and reads a row through the generated Polyspec\Orm\Tests\Model\Service on SQLite in the official PHP image; make check and the .PHONY list name php-without-mysql-check; the comments taken from N17 are in Korean; php-without-mysql-check, docs-rules-check, checklist-check, git-check, go-fmt-check, rust-fmt-check, interface-check, client-unit-check, ts-check, client-db-check, conformance-check, rust-check and every dbspec check pass on the merged tree. | Evidence: the merge resolved CHANGELOG.md, CHANGELOG.ko.md, Makefile and clients/php/README.md; composer.json equals main's. Red: on the merged tree the case taken from main failed in the container with Unknown named parameter $schemaPath at line 38 (make php-without-mysql-check exit 255). Green: make php-without-mysql-check passes three times (2-3 s each, the case 48.3 ms), and the same case run by the host PHP, which loads pdo_mysql, fails with this PHP loads pdo_mysql. On the merged tree php-without-mysql-check, docs-rules-check, checklist-check, git-check, go-fmt-check, rust-fmt-check, interface-check, client-unit-check, ts-check (31 s), client-db-check (447 s), conformance-check (205 s), rust-check (76 s) and every dbspec check pass, among them dbspec-apply-check (453 s), dbspec-apply-pairs-check (98 s) and dbspec-compare-check (38 s). dbspec-introspect-compare-check failed once on its 5 s budget while other runs held the machine (CPU 0-40% idle, 1-minute load 50): MySQL PHP took 7.6 s, with Go 3.4 s, TypeScript 3.0 s and Rust 3.5 s on MySQL; N17 changes no introspection code, and the check passed on the parent merges T8.9 (322 s) and T15 (357 s). contracts/features.json lists the case under dsn_connection with the verification make php-without-mysql-check, because node scripts/features/check.mjs rejected it with "php test file belongs to no feature"; the feature check then passes with 17 feature contracts. | [o] |
| T8.8.4 | Integrate main into feat/dbspec-T8 again (follows T8.8.3). Cause: main gained N18 (N18), N19 (N19) and N19.1 (N19.1) after T8.8.3. N18 connects to MySQL with TLS through ssl-mode=VERIFY_IDENTITY and an absolute ssl-ca and makes the PHP and TypeScript clients refuse a DSN parameter outside the set of each scheme; it also changed the PHP SchemaImport and TypeScript tools/db.ts tool connections, which T8.2.6.4 removed. N19 and N19.1 add utils().schema().addColumns(manifestJson) on the old schema path: the Mermaid import of the live tables with manifest facts (internal/ormgen/add_columns.go, PHP SchemaColumns, TypeScript schema_columns.ts, Rust orm_schema::catalog and orm_schema::live), the JSON fixtures contracts/fixtures/add_columns_*, and type, default, CHECK and index-name alignments; this branch removed every one of those paths, so a merge that resolves them as deleted drops the feature, its Go side included. | Acceptance: main is merged; each feature works on the dbspec design in Go, PHP, TypeScript and Rust with its tests on MySQL, PostgreSQL and SQLite; the version stays 0.0.2; no old schema format comes back; the mapping and every part without a counterpart are recorded here. | Evidence: Mapping. N18: Go parseDSN/mysqlTLS and openSQL (clients/go/orm/dsn.go, db.go), PHP Orm::parseDsn, mysqlTls, mysqlTlsOptions with DSN_PARAMETERS (parseDsn now returns the CA path as its sixth element), TypeScript parseDsn, mysqlTls, mysqlSsl with DSN_PARAMETERS (driver.ts), Rust unchanged (sqlx reads ssl-mode and ssl-ca); tests clients/go/orm/mysql_tls_test.go (TestParseDSNMySQLTLS, TestMySQLTLSConnection), clients/php/tests/mysql_tls.php, clients/typescript/tests/mysql_tls.mjs, clients/rust/orm/tests/mysql_tls.rs; scripts/test-servers.sh tls and make test-servers-tls are taken as they are. The PHP SchemaImport and TypeScript tools/db.ts changes have no counterpart: no tool of this branch opens a connection from a DSN; the dbspec introspect and apply functions take a connection that the caller opened. N19: utils().schema().addColumns(schema) (Go AddColumns, Rust add_columns) takes the generated schema value instead of manifest JSON, introspects the database of the connection, compares the existing tables of the document set with compareSchemas of their schema texts, and, when every difference is an add_column of a column that is null or has a default, runs the plan steps from those tables to the set; a new pure function writes those steps in each client's dbspec module: Go dbspec.AddColumnSteps (engine/dbspec/add_columns.go), PHP Dbspec::addColumnSteps, TypeScript addColumnSteps (src/dbspec/add_columns.ts, with introspectCatalog so the client introspects through its own pool), Rust orm_schema::dbspec::add_column_steps. The plan steps replace the audit triggers of a changed table (the dbspec audit trigger copies every column, so the history table gains the column too), add the renderer CHECK of each column, and on SQLite rebuild the table. Every other difference, including reorder_columns for a column added before an existing one and an unsupported object of a table of the set, is SCHEMA_DIFFERS with each <kind> <table>[.<name>] before any statement; docs/spec/errors.yaml keeps the code with the dbspec wording, and docs/spec/schema.md §5 states the call. The import fix for a foreign key of a table to its own key has its counterpart in dbspec introspection, which restores fk_addcol_tag_parent of addcol_tag on the three databases. Tests clients/go/orm/add_columns_test.go (TestAddColumns*), clients/php/tests/add_columns_test.php, clients/typescript/tests/add-columns.mjs, clients/rust/orm/tests/add_columns.rs with the cases add_columns, add_columns_differs, add_columns_transaction and add_columns_edited_manifest, on the dbspec fixtures contracts/fixtures/add_columns/*.dbs (v1, v2, the other set log, and required, removed, changed, nullable, default, index, unique and reorder); Go writes the rows and the audited update through the client with the version 1 and version 2 models, and the other clients through SQL. N19.1: no counterpart is needed: dbspec has one bytes type and no char(n), a name has at most 63 bytes and is never shortened, and introspection restores the canonical type, default, check and index name; the fixture holds a bytes column and the 63-byte index ix_addcol_item_enabled_created_at_at_the_sixty_three_byte_limit, and the cases pass on PostgreSQL. Parts without a counterpart: SQLite adds columns outside a transaction and returns CONFIG inside one, as MySQL does, because the plan step that adds a column rebuilds the table with foreign keys off and SQLite does not change foreign_keys inside a transaction; it runs the steps in one BEGIN IMMEDIATE transaction on one connection, requires PRAGMA foreign_key_check to return no row and turns foreign keys on again (the cases check that the same connection refuses a tag of a missing item afterwards). install after addColumns does not create a table that a set gains, because install here creates a set only whole and returns CONFIG for a partly installed set; the case requires that CONFIG. A schema-qualified table name (CAPABILITY_UNSUPPORTED on main) cannot occur in dbspec. The added columns come in table name order and then column order instead of manifest order. The Rust move of the catalog reads to orm_schema::catalog has its counterpart in orm::dbspec::introspect, which the runtime and the tools share; the TypeScript DriverPool.session of N19 replaces unprepared for MySQL install, and the SQLite pool has a session for the foreign key change. N16 and N17: their Go parts are in this branch: clients/go/orm/fault_ormtest.go, rollback_fault_test.go, rollback_fault_entry_test.go and the fault check in tx.go (N16); N17 has no Go part; nothing of either lived in internal/ormgen. Red: Go TestAddColumns* failed 12 of 12 with AddColumns = [], <nil> and no SCHEMA_DIFFERS against a stub that adds nothing; PHP failed 12 of 12 with Call to undefined method Orm\SchemaUtils::addColumns() on the branch utilities; TypeScript failed 12 of 12 against a stub (addColumns = [], no such column: note, Unknown column 'note', column "note" does not exist, required: no error); Rust failed 4 of 4 against a stub. For N18 the PHP and TypeScript cases on the branch parsing accepted all 17 refused DSNs, read an empty Ssl_version and accepted both wrong servers, and the Go case did not compile without sslCA and sslHost. Green: the add-columns cases pass in the four clients on MySQL, PostgreSQL and SQLite (Go 12 tests in 1.8 s, PHP 12 cases in 1.4 s, TypeScript 12 cases in 1.3 s, Rust 4 tests in 0.7 s) and the TLS cases pass in the four clients (TLSv1.3, both wrong servers refused); node scripts/features/check.mjs --run --feature schema_install and --feature dsn_connection pass with the new verification commands install-add-columns-* and dsn-mysql-tls-*; interface-check passes with the re-recorded symbol inventories and the SchemaUtils.addColumns entry of contracts/interfaces.json that takes GeneratedSchema. Check: every make check step passes on the merged tree. The free disk space fell to 1 GB while other worktrees built, so a full run was stopped twice to keep the shared servers writable and the Rust target was removed between runs: one run passed its first 29 steps (client-db-check 163 s, client-pooler-check 134 s, conformance-check 92 s, rust-check 66 s, ts-check 28 s) and was stopped in dbspec-introspect-rust-check after it passed, and make check CHECK_TARGETS=<the other 15> passed dbspec-introspect-compare-check (49 s), the dbspec plan and apply checks, dbspec-apply-pairs-check (32 s), ts-model-check, feature-check (448 s) and go-test-check; the databases of both stopped runs were dropped. | [o] |
| T8.8.5 | Create the tables a new version of an installed set adds when the set is upgraded (follows T8.8.4). Cause: on main a module was upgraded by addColumns and then install, which created the tables new in the module (N19); on this branch install creates a set only whole and addColumns only adds columns, so a module version that adds a table could not be applied. | Acceptance: the additive upgrade of an installed set creates every table of the set that the database lacks with its indexes, foreign keys, checks and audit and immutable triggers through the same plan steps, adds the missing null or defaulted columns, and refuses every other difference with SCHEMA_DIFFERS before any statement, in Go, PHP, TypeScript and Rust on MySQL, PostgreSQL and SQLite. | Evidence: the call states what it does and is renamed everywhere without an alias: utils().schema().addTablesAndColumns(schema) (Go AddTablesAndColumns, Rust add_tables_and_columns) with the pure step functions Go dbspec.AddTablesAndColumnsSteps (engine/dbspec/add_tables_and_columns.go), PHP Dbspec::addTablesAndColumnsSteps, TypeScript addTablesAndColumnsSteps (src/dbspec/add_tables_and_columns.ts) and Rust orm_schema::dbspec::add_tables_and_columns_steps; each compares the tables of the set that exist with the whole set, accepts create_table and the add_column of a null or defaulted column, and writes one plan from those tables (or from empty when none exists) to the set, so a created table gets the same steps as an applied plan. It returns a created table as table and an added column as table.column in table name order. The fixtures moved to contracts/fixtures/add_tables_and_columns: v2 adds the audited table addcol_extra (index, foreign key to addcol_item, check) with its history table, and required adds the same table next to a non-null column without a default. Tests clients/go/orm/add_tables_and_columns_test.go (TestAddTablesAndColumns*), clients/php/tests/add_tables_and_columns_test.php, clients/typescript/tests/add-tables-and-columns.mjs and clients/rust/orm/tests/add_tables_and_columns.rs require the two tables in the result, an insert into addcol_extra recorded in its history with action insert, a refused insert of a missing item, a refused empty label and a refused delete, a repeated call that adds nothing, an install of version 2 afterwards that changes nothing, and after the eight SCHEMA_DIFFERS fixtures the full result again, so the refused required version created no table. docs/spec/schema.md §5 (+ko), docs/usage, docs/interfaces, docs/spec/errors.yaml with the regenerated Go, PHP and Rust codes, the PHP and Rust READMEs, contracts/features.json (verification install-add-tables-and-columns-*), the SchemaUtils.addTablesAndColumns entry of contracts/interfaces.json with the output AddedTablesAndColumns, and the re-recorded symbol inventories state it. Red: with the fixture and tests changed and the old implementation, Go failed 12 of 12 (AddColumns = [addcol_item.note ...] without addcol_extra addcol_extra_history), PHP 12 of 12 and TypeScript 12 of 12 (the same result and no such table: addcol_extra, Table '...addcol_extra' doesn't exist, relation "addcol_extra" does not exist), Rust 4 of 4. Green: Go 12 tests in 2.4 s, PHP 12 cases, TypeScript 12 cases and Rust 4 tests in 0.7 s pass on MySQL, PostgreSQL and SQLite; node scripts/features/check.mjs --run --feature schema_install, interface-check, go-fmt-check and rust-fmt-check pass. | [o] |
| T9 | Implement the whole dbspec surface of the PHP client in a PHP extension written in C, as an independent fifth implementation of the same interface (requires T8.2.5). Cause: the item named only Dbspec.parse, Dbspec.emit and DbspecDiagnostic, so T9.1 to T9.5 built an ext-php-rs extension over the Rust dbspec with six methods and an opaque Document; the PHP client Orm\Dbspec\Dbspec (clients/php/src/Dbspec/Dbspec.php) is the specification, and the extension must provide every public method of it and every result and value class with the same names and property order, written in C like the ordered-json php-extension. | Acceptance: Orm\Dbspec\Native\Dbspec provides parse, readFile, readBytes, emit, render, manifest, introspect, externalDifferences, parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps, apply, recover, rollback, finalize, exportMermaid and importMermaid; Native\Document has the public mutable property graph of the PHP Document; contracts/interfaces.json declares every rule for the four languages and the extension; make dbspec-compare-check compares the extension on every input, the apply and introspect cross checks run it, its tests mirror the PHP plan, Mermaid and apply cleanup tests, the Rust extension and its cargo steps are gone, and CI passes on the last commit of the sub-items (T9.6 onward). | — | [o] |
| T9.1 | Build the PHP extension orm_dbspec over the Rust dbspec and test it against the shared vectors and the PHP client (follows T9). | — | Evidence: clients/php-extension is an ext-php-rs 0.15.15 cdylib, a Cargo workspace of its own with its Cargo.lock, that registers Orm\Dbspec\Native\Dbspec with readFile, readBytes, parse, emit, manifest and render over orm_schema::dbspec, the opaque Document, and the result classes Diagnostic, ReadResult, ParseResult, Manifest, ManifestResult and RenderResult as final readonly classes with typed readonly properties in declaration order and a private constructor, declared through the Zend API because ext-php-rs properties are untyped and writable and the order of its getter properties follows a HashMap of the proc macro; PHP code selects it by that namespace, with no automatic switch. A text that is not UTF-8 gives the encoding diagnostic of the PHP client. make dbspec-php-extension-check runs clippy, builds and copies the library, and runs twice clients/php-extension/tests/declarations_test.php (the Reflection of the loaded extension equals stubs/orm_dbspec.stub.php in 40 declarations; a stub with ?string for emit failed) and dbspec_test.php (10 cases: 118 parse cases, 4 files read twice, 5 manifests and 51 renderings equal the vectors and the PHP client, encoding, argument and object rules; with the bare CR order reversed the bytes case failed). CI installs libclang in the setup step php-extension-tools (need libclang of feature-check); make install-rust fetches the crates of its Cargo.lock; repo-check, full-run-check, feature-unit-check, version-check, rust-fmt-check and interface-check pass. | [o] |
| T9.2 | Run the PHP extension as the fifth runner of make dbspec-compare-check (follows T9.1). | — | Evidence: tests/dbspec/compare/php-interface.php holds the vector readers and dbspec_write_interface, which prints the shared cases, the stress document, the files, hashes and statement vectors through one Dbspec class; php.php calls it with Orm\Dbspec\Dbspec and printed the same 3378 lines as before the split, and php-extension.php calls it with Orm\Dbspec\Native\Dbspec under php -d extension=<debug build>, which dbspec-compare-check/prepare builds and copies. runners.mjs declares the inputs each runner reads (reads) and the extension runner's until: 'plans/'; compare.mjs compares such an output with the first Go output up to its first plan case, so a case missing, added or different fails, and a reference without that line is an error; check.test.mjs failed 3 of its 4 new cases with until ignored and passes 10. runners.test.mjs and inputs.test.mjs apply a mutation or an unreadable input only to the runners that read it; the extension runner rejected a missing cases file, a directory as stress document, an unsigned stress document and a ddl case without id with their paths. The extension output equals the PHP output up to plans/ (1125 lines). repo-check, full-run-check and feature-unit-check pass. | [o] |
| T9.3 | Declare the PHP extension in the interface and symbol contracts (follows T9.2). | — | Evidence: contracts/interfaces.json declares the extension php-extension under extensions (PHP syntax, the stub root clients/php-extension/stubs, the snapshot contracts/symbols/php-extension.json and its hash, the six Dbspec rules and the owners DbspecDiagnostic and DbspecManifest), and those rules and owners carry its adapters. contracts/validate.go checks each extension adapter against a php-extension column of the common inputs and outputs and refuses an adapter on a rule the extension does not list, a listed rule without one and an unknown rule (TestExtensionContractRejectsDrift: with the check bypassed 4 of its 5 mutations were accepted). The interface check runs interfaces/php-extension with the PHP extractor on the stub and compares rules, owners, prohibited symbols, the snapshot and its hash (39 symbols, 6 rules, 2 owners); TestExtensionDeclarations refuses a stub whose emit returns ?string, a Manifest without externalText, a Model.gets adapter of the extension and a changed snapshot. --language php-extension checks it alone. go test ./contracts ./tests/interfaces/check, make interface-check, repo-check, feature-unit-check and docs-rules-check pass. | [o] |
| T9.4 | Document the PHP extension and suggest it in the PHP client (follows T9.3). | — | Evidence: docs/spec/dbspec.md and its Korean pair have the section "PHP extension": Orm\Dbspec\Native\Dbspec and its result classes, selection by namespace with no automatic switch, the encoding and argument rules, the Dbspec component as its scope, the Cargo build for the PHP of php-config and the php -d extension= load, the stub as its declarations and the two checks; the Verification section names the extension among the implementations that make dbspec-compare-check compares. clients/php/composer.json suggests ext-orm_dbspec, and composer validate reports the lock file current. docs-rules-check, docs-check, docs-verify-idempotent, repo-check, version-check and feature-unit-check pass. | [o] |
| T9.5 | Give the apply runners and the stress harness of the input test their own inputs (follows T9.2). Cause: the CI run of G5.83 failed features/schema_definition/schema-compare in tests/dbspec/inputs.test.mjs with runRunner takes the 5 inputs cases, stress, ddl, plans, mermaid; given 4 for the apply runners and given 1 for the Rust stress harness: T9.2 made runRunner select the inputs of a compare runner by its reads and refuse any other count, but the same test runs the apply runners (four arguments) and the stress harness (one) through runRunner. The five compare runners, the PHP extension among them, agreed in that run. | — | Evidence: runRunner selects inputs by reads only for a runner that declares it, refuses another count for such a runner with the fix, and passes the inputs of a runner without reads unchanged; a runner without reads received its four arguments, a compare runner its declared two, and a compare runner given one input was refused. check.test.mjs passes its 10 cases. | [o] |
| T9.6 | Declare the plan, Mermaid, apply and introspect methods of Dbspec as interface rules (follows T9). Cause: contracts/interfaces.json had rules only for readFile, readBytes, parse, emit, manifest and render, so parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps, externalDifferences, apply, recover, rollback, finalize, introspect, exportMermaid and importMermaid were in the PHP symbol snapshot only and no check compared their signatures across the four languages. | — | Evidence: the Dbspec component lists the 16 methods and contracts/interfaces.json has a Dbspec.<method> rule for each with its Go, PHP, Rust and TypeScript adapter and state, and the owners DbspecPlan, DbspecPlanStep, DbspecEffect, DbspecNullCheck, DbspecChange, DbspecDifference, DbspecUnsupported, DbspecTableRename, DbspecColumnRename, DbspecColumnName, DbspecApplyEvent and DbspecApplyError with the fields of each language; contracts/validate.go maps the new common inputs and results to each language and to php-extension; docs/spec/interfaces-model.md (+ko) is regenerated; go test ./contracts passes and make interface-check passes for the four languages and the extension (its six rules). | [o] |
| T9.7 | Stop the PHP parser's endless recursion on a use cycle through a renamed document and its TypeError on digit-only column names of an audited table (follows T9.6). Cause: a differential run of the PHP client against the C extension of T9 over mutated shared vectors crashed the PHP client twice: a used document whose header names another document than its set entry and that uses its own entry recursed until memory ran out (Parser::usedDocument checked cycles by header names only), and an audited table with a column named 10 made checkAuditHistories pass the int key 10 to the string closure of auditLists; the Go, TypeScript and Rust parsers reported both documents with diagnostics. | — | Evidence: tests/dbspec/cases.json gained the invalid cases use-cycle-through-another-header-name (one use error at 3:5) and audit-digit-column-names (name.format at 7:3 and 18:3, two setting errors at 24:16); clients/php/tests/dbspec_test.php failed the first with Allowed memory size ... exhausted before the fix; Parser::usedDocument now marks a document while it is read and reports a use cycle when it is used again in that time, and checkAuditHistories reads column names as strings; dbspec_test.php passes 100 invalid cases, go test ./engine/dbspec, node --test clients/typescript/tests/dbspec.mjs (141 tests) and cargo test -p orm-schema --test dbspec pass the new cases, and the php interface check, make repo-check and gofmt pass. | [o] |
| T9.8 | Replace the Rust PHP extension with an independent C implementation of the PHP client's document model, parse, emit, readFile, readBytes, manifest and render (follows T9.7). Cause: the ext-php-rs extension of T9.1 ran the Rust dbspec and gave PHP an opaque Document, so a document could not be built or changed in PHP code, its classes differed from the PHP client's and its build needed Rust and libclang. | — | Evidence: clients/php-extension is C (src/*.c, config.m4, composer.json for PIE with build path src): a per-call arena C model, a parser, check expression reader, literal rules, emitter, document set check, manifest and renderer translated from clients/php/src/Dbspec, and convert.c, which turns the public mutable Orm\Dbspec\Native document graph into the C model and back; stubs/orm_dbspec.stub.php declares every class of the PHP client's dbspec surface (document, result and value classes with their constructors, methods and constants) and Dbspec with the six methods, and scripts/arginfo.php writes src/orm_dbspec_arginfo.h with gen_stub.php from it, leaving the array constants to src/orm_dbspec.c; scripts/build.sh builds with phpize into a temporary directory, refuses compiler warnings and publishes by rename; make install-php-extension-tools installs gen_stub.php with its PHP-Parser into .runtime/bin/gen-stub instead of libclang, and the Rust sources, Cargo files, clippy and rustfmt steps and the cargo fetch are gone. clients/php-extension/tests/dbspec_test.php now compares diagnostics with messages, emissions, files, manifests, statements, exceptions and documents built in PHP code with the PHP client (11 cases pass), declarations_test.php finds the 299 declarations of the loaded extension equal to the stub and checks the PIE package, make dbspec-php-extension-check passes, a differential fuzz of 40000 mutated documents and 15000 generated check and default lines found no difference from the PHP client after T9.7, repeated calls leave no memory behind, and make interface-check (with the re-recorded php-extension snapshot), make repo-check, go test ./contracts ./tests/interfaces/check, the owners, downloads, full-run and compare check node tests, make docs-check and gofmt pass. | [o] |
| T9.9 | Implement the plan methods of the PHP client in the C extension (follows T9.8). Cause: after T9.8 Orm\Dbspec\Native\Dbspec lacked externalDifferences, parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps and planSteps. | — | Evidence: src/plan.c (plan text, chain, diff and object comparison, schema comparison, the set comparisons) and src/steps.c (the steps of three dialects with the SQLite rebuild) translate PlanText, PlanChain, PlanDiff, PlanObjects, SchemaComparison, PlanSteps and the set methods of Dbspec.php, and convert.c converts plans, steps, changes, differences and unsupported objects, returning the given Plan objects from chain; the stub declares the nine methods. tests/dbspec/compare/php-interface.php now holds the plan and Mermaid printers with the Dbspec class as argument, php.php calls them with Orm\Dbspec\Dbspec, and the extension runner reads the plan vectors (runners.mjs reads cases, stress, ddl and plans, until mermaid/); its output equals the PHP client's up to the first Mermaid case (2918 lines). clients/php-extension/tests/dbspec_plan_test.php has the body of clients/php/tests/dbspec_plan_test.php (51 cases pass), declarations_test.php checks that the bodies stay equal, and make dbspec-php-extension-check runs it twice; contracts/interfaces.json gives the nine rules and the owners DbspecPlan, DbspecPlanStep, DbspecEffect, DbspecNullCheck, DbspecChange, DbspecDifference, DbspecUnsupported, DbspecTableRename, DbspecColumnRename and DbspecColumnName their php-extension adapters with a re-recorded snapshot. A differential fuzz found no difference from the PHP client in 17000 mutated plans (parse, emit, diff, steps of three dialects, chain, comparison, installed differences, added tables and columns) and in 18000 random source and target schema pairs with renames, drops, widenings and new tables (diff and steps, 16444 with steps); repeated calls leave no memory behind; make interface-check (15 rules, 12 owners for the extension), make repo-check, make docs-check, the owners and compare check node tests and gofmt pass. | [o] |
| T9.10 | Implement exportMermaid and importMermaid in the C extension and compare the extension on every input (follows T9.9). Cause: after T9.9 the extension lacked the Mermaid methods, so its compare runner stopped before the Mermaid vectors through until. | — | Evidence: src/mermaid.c writes the erDiagram and reads one with scanners for the line forms of the PHP client's patterns (entity start, attribute, relationship, label, column suffix, Go TrimSpace white space, known types and the %q quoting), and src/catalog.c is the neutral intermediate model of the PHP client's Catalog and CatalogTable, which writes the document text, parses it and removes the rejected objects (introspection uses it too); the stub declares both methods. The extension runner now reads all five inputs, its output equals the PHP client runner's (3385 lines), and the until comparison of tests/dbspec/compare/compare.mjs, check.mjs and its four check.test.mjs cases are gone because no runner stops early; runners.test.mjs and inputs.test.mjs no longer say the extension skips vectors. clients/php-extension/tests/dbspec_mermaid_test.php has the body of the PHP client's Mermaid test (13 cases pass), declarations_test.php checks both mirrored bodies, and make dbspec-php-extension-check runs it twice; exportMermaid and importMermaid carry their php-extension adapters in contracts/interfaces.json (17 rules). A differential fuzz of 16000 mutated erDiagrams (876 imported documents per 4000) and the export of every shared case found no difference from the PHP client; repeated calls leave no memory behind; make interface-check, make repo-check, make docs-check, the owners and compare check node tests (6 cases) and gofmt pass. | [o] |
| T9.11 | Implement introspect in the C extension and run it in the introspection cross checks (follows T9.10). Cause: after T9.10 Orm\Dbspec\Native\Dbspec had no introspect, so the extension could not read a database. | — | Evidence: src/introspect.c sends the catalog queries of the PHP client's MysqlCatalog, PostgresCatalog and SqliteCatalog by calling query, fetchAll, errorCode and errorInfo on the given PDO through the Zend API (no pdo_dbh_t internals), wraps a PDOException in the RuntimeException "Catalog query failed: ..." with it as the previous exception, reports a failing statement in every error mode, rejects catalog values of another shape with the UnexpectedValueException messages of CatalogRows, decodes check expressions like CheckDecoder, recognizes immutable and audit triggers like CatalogTriggers, and builds the document with src/catalog.c; the stub declares introspect(\PDO $connection, string $dialect, string $name): IntrospectResult and the module requires pdo. clients/php-extension/tests/dbspec_test.php gains php-extension/introspect: the 5 ddl.json cases and the 4 SQLite cases of tests/dbspec/introspect.json applied to in-memory SQLite databases give the PHP client's document and unsupported objects and the vector's, and failing queries in three error modes, values of another type, exceptions thrown by a PDO subclass, an unknown dialect and a connection that is not a PDO give the PHP client's exceptions and previous exceptions. tests/dbspec/introspect/php-extension.php is the fifth runner of TestIntrospectCompare (ORM_DBSPEC_EXTENSION, built by make dbspec-introspect-compare-check), and clients/php-extension/tests/dbspec_introspect_test.php mirrors the body of the PHP client's introspection test, run on MySQL, PostgreSQL and SQLite by the new make dbspec-introspect-php-extension-check; declarations_test.php checks three mirrored bodies, and Dbspec.introspect carries its php-extension adapter (18 rules). A differential fuzz of 24000 introspections (SQLite databases from the shared documents with mutated statements, and scripted MySQL, PostgreSQL and SQLite catalogs with mutated rows, value types and query failures) found no difference from the PHP client in documents, unsupported objects, exceptions or the sent queries, and 2000 repeated calls leave no memory behind. The server suites run in CI; locally make interface-check, make repo-check, make docs-check, the extension's declarations and dbspec tests, the owners and check node tests and gofmt pass. | [o] |
| T9.12 | Stop rollback at an applied row whose recorded step lies outside its plan, in every client (follows T9.11). Cause: a differential run of the PHP client against the C extension over apply, recover, rollback and finalize on SQLite, with the history row changed between commands, found that rollback of an applied or done row read step without the range check that recover and an interrupted rollback apply: a row at step 99 of a 4-step plan made the PHP client read missing steps (warnings, then LogicException "step 98 has no effect to read"), Go and Rust panic with an index out of range, and TypeScript throw TypeError: Cannot read properties of undefined (reading 'rollback'). | — | Evidence: engine/dbspec/apply.go, clients/php/src/Dbspec/PlanApply.php, clients/typescript/src/dbspec/apply.ts and clients/rust/orm/src/dbspec/apply.rs now report such a row as the chain error "the recorded step 99 is outside the plan's 4 steps" before the drift check, and docs/spec/plans.md (+ko) states the rule. The SQLite cleanup tests of each client gained the case apply/rollback-step-outside (apply create-from-empty, set the row's step to 99, roll back): before the fix Go failed with index out of range [98] with length 4, PHP with the LogicException, TypeScript with the TypeError and Rust with index out of bounds: the len is 4 but the index is 98; after it go test ./engine/dbspec (the three apply tests), clients/php/tests/dbspec_apply_cleanup_test.php (5 cases), make dbspec-ts-check (277 tests) and make dbspec-rust-check/apply-cleanup pass, and gofmt and make docs-check pass. | [o] |
| T9.13 | Implement apply, recover, rollback and finalize in the C extension and compare its public classes with the PHP client's (follows T9.12). Cause: after T9.12 the extension lacked the four apply commands, the last methods of the PHP client's Dbspec. | — | Evidence: src/apply.c follows PlanApply: the session lock, settings and their restore for MySQL, PostgreSQL and SQLite, the history table, position, settled state, verify through the extension's own introspection and manifest, resolve, forward, finalize, rollback with null checks, events through the $events closure, applied_at from the $now closure through DateTimeImmutable, and finish, which runs every cleanup step and throws the first error alone or an ApplyCleanupError with the cleanup errors in order (a nested ApplyCleanupError is flattened); it calls getAttribute, prepare, exec and the statement methods of the given PDO, so a PDO subclass sees the PHP client's calls, and a connection outside ERRMODE_EXCEPTION is the InvalidArgumentException of PlanApply.php. The stub declares the four methods and an @internal PlanApply with EFFECT_QUERIES (a nested constant that src/orm_dbspec.c registers) and effectOn; contracts/interfaces.json gives Dbspec.apply, recover, rollback and finalize their php-extension adapters, so the extension carries all 22 Dbspec rules. Tests: clients/php-extension/tests/dbspec_apply_cleanup_test.php and dbspec_apply_test.php have the bodies of the PHP client's tests (declarations_test.php checks 5 mirrored bodies from their first const), make dbspec-php-extension-check runs the cleanup test twice, the new make dbspec-apply-php-extension-check runs the apply scenarios on the three databases, tests/dbspec/apply/php-extension.php is the fifth client of TestApplyChainAcrossClients (180 pairs), and dbspec_test.php gains php-extension/apply: 108 scripts on SQLite files (each chain applied plan by plan, stopped at every event of the second plan and then recovered or rolled back, drift, changed history rows, finalize) give the PHP client's events, exceptions with their codes, cleanup errors and previous errors, history rows and schema, and so do the error mode, an unknown dialect, a split chain and a clock that returns text. declarations_test.php gains php-extension/client: the 297 public declarations of the PHP client's 43 public dbspec classes equal the extension's, in property order, and every other extension class is @internal in the stub; for that, the PHP client's helper classes CheckDecodeFailure, Expression, ExpressionFailure, Literal, ParseStop and SchemaComparison are now marked @internal. A differential fuzz of 3600 random command scripts on SQLite files (stops at random events, stray tables, changed history rows, row inserts) found no difference from the PHP client. The server suites run in CI; locally make dbspec-php-extension-check, make interface-check, make repo-check, make docs-check and gofmt pass. | [o] |
| T10 | Depend on an ordered-json revision with one Cargo manifest for its package. Cause: every cargo command in clients/rust warns "skipping duplicate package ordered-json", because the ordered-json revision of clients/rust/orm/Cargo.toml and ordered-json main both define the package in Cargo.toml (library rust/src/lib.rs) and again in rust/Cargo.toml; cargo uses the first and skips the second. | Acceptance: ordered-json keeps one manifest for the package, clients/rust/orm/Cargo.toml pins that revision, and cargo test in clients/rust prints no duplicate package warning. | Evidence: ordered-json removes the root Cargo.toml so rust/Cargo.toml is the one manifest of the package, and its repository test test_each_cargo_package_has_one_manifest fails on two manifests; the Go, PHP, TypeScript and Rust clients all use that ordered-json; cargo test -p orm --lib prints no "skipping duplicate package" line, and rust-check, client-unit-check, ts-check, conformance-check and the four dbspec checks pass. | [o] |
| T11 | Fail the check when a tracked Go source is not gofmt formatted, and make the Go and Rust formatting checks pass. Cause: make check runs rust-fmt-check but no Go formatting check, rust-fmt-check fails with 82 rustfmt differences across the Rust workspace (rustfmt 1.9.0 of the default and of the 1.98.1 toolchain agree), and six tracked Go files are not gofmt formatted (bench/seedaes/main.go, clients/go/decimalmodel/decimal_physical_test.go, clients/go/orm/decimal_model_test.go, clients/go/orm/exec.go, engine/plan/plan.go, engine/planner/planner.go), among them "feat: preserve exact decimal model values" code with struct fields out of alignment and an if body on one line. | — | Evidence: make go-fmt-check lists the tracked Go files that gofmt would change and fails; it failed with the six files before formatting and passes after gofmt -w, and make check runs it next to rust-fmt-check. cargo fmt --all formatted the Rust workspace and make rust-fmt-check passes. go build ./..., go vet of engine, clients/go and bench, cargo check --workspace --all-targets --all-features pass after the change. rustfmt added a trailing comma to two multi-line parameter lists of orm-build catalog/mod.rs (read_only_query, read_only_grid_query), whose recorded token signatures then differed; contracts/symbols/rust.json records them again and the four-language interface-check passes. | [o] |
| T12 | Fail the check when the committed TypeScript models differ from the generator output, and scan only the sources that call models. Cause: npm run build of clients/typescript runs npm run models, whose --scan tests reads every test file, so the Playwright call browser.newPage() of tests/physical-document-render.mjs adds newPage(value: unknown) to the Author and User models; the committed models.ts lacks it and no target compares the two. | — | Evidence: clients/typescript/tests/models-scan.mjs failed because the models script scanned 44 sources while only 8 import a generated model; the other 36 include tests/physical-document-render.mjs; the script now names those 8 and the test passes. With that scan the same test failed with differs: src/models/models.ts; regenerating removed the 12 newPage(value: unknown) declarations and it passes. make ts-model-check, part of make check, builds the client and runs both tests: the scan equals the sources that import a model module or class, and orm-gen gen --check with the script arguments exits 0 with no output. contracts/symbols/typescript.json drops the 12 newPage symbols and go run ./tests/interfaces/check passes; make ts-check passes with TEST_ENV. | [o] |
| T13 | Check commit subjects against the commit rule of AGENTS.md. Cause: make git-check required type: description with the types build, perf and merge and 72 characters, while AGENTS.md requires type(scope): Subject (#id) with the types feat, fix, docs, style, refactor, test and chore and a 50-character subject, so the check failed on every commit that follows the rule and on the earlier commits without a type. | — | Evidence: scripts/git/check.test.mjs failed because subjectErrors did not exist and passes 3 tests that accept type(scope): Subject (#id) and reject a missing type, scope or id, the types perf and the untyped form, a lower-case subject, a final period and a subject over 50 characters; contracts/rules.json holds the AGENTS.md types, the 50-character limit and the baseline main; merge commits are skipped; make git-check passes. | [o] |
| T14 | Run the environment-bound Go tests only from their owning targets. Cause: make client-db-check runs go test ./clients/go/..., which also ran TestDecimalPhysical* and TestCoverage*; they need the DSNs that decimal-physical-check and feature-check provide, so client-db-check failed with "DECIMAL_MYSQL_DSN is required" and "selected database and DSN are required". | — | Evidence: TestDecimalPhysical* carries the build tag decimalphysical and TestCoverage* the tag featurecoverage; scripts/decimal-physical-check.mjs and scripts/features/coverage.mjs pass the tags. go test -count=1 ./clients/go/... with TEST_ENV passes every package; make decimal-physical-check passes 24 runs (4 clients, 3 databases, 2 runs) in each of three consecutive runs after make decimal-db-setup; node --test scripts/features/coverage.test.mjs passes 10 tests. make feature-check stops before running tests on T15. | [o] |
| T14.1 | Run the Rust DSN coverage test only from feature-check (follows T14). Cause: cargo test --workspace in client-db-check and client-pooler-check ran coverage_dsn_connection, which needs ORM_FEATURE_DATABASE and ORM_FEATURE_DSN, and failed with "ORM_FEATURE_DATABASE is required". | — | Evidence: coverage_dsn_connection is #[ignore] with its owner named in the reason; cargo test -p orm --test coverage_dsn reports 1 ignored and passes; with ORM_FEATURE_DATABASE=sqlite, the bench SQLite DSN and -- --exact --include-ignored, the form scripts/features/coverage.mjs now runs, it passes 1 test; node --test scripts/features/coverage.test.mjs passes 10 tests. | [o] |
| T14.2 | Run the Rust decimal and generated-model coverage tests only from their owners (follows T14.1). Cause: once coverage_dsn_connection stopped failing first, cargo test --workspace ran decimal_mysql, decimal_postgres, decimal_sqlite and coverage_generated_model_connection, which need the variables of decimal-physical-check and feature-check, and failed with "DECIMAL_SQLITE_DSN is required". | — | Evidence: the three decimal tests and coverage_generated_model_connection are #[ignore] with their owners in the reason; scripts/decimal-physical-check.mjs passes --include-ignored; cargo test --workspace with TEST_ENV exits 0 with 1 and 3 ignored; make decimal-physical-check passes with the Rust runs. | [o] |
| T15 | Declare a coverage kind and owner tests for every feature of contracts/features.json. Cause: make feature-check fails with "missing coverage kind" for 16 of the 17 features; only the DSN connection feature declares coverage, and no item tracked the rest. | — | Evidence: every feature of contracts/features.json declares coverage.kind (11 database, 6 independent), shared case IDs and an owner test per supporting client under clients/<language>, 152 owner and dependent-part requirements that scripts/features/coverage.mjs executes twice each with equal results and equal database digests; database cases run on MySQL, PostgreSQL and SQLite against the seeded bench database and write only to composite_account, composite_membership and the audit tables they install and drop. With TEST_ENV, make feature-check passes: "feature coverage: 17 contracts executed" and all 71 verification commands, "features: 17 feature contracts passed". The shared fixtures contracts/fixtures/planner.json, schema_definition.json and authenticated_encryption.json give the independent cases one expectation for four clients. Manifest errors are corrected at their cause: decimal_model.json takes the fixture form (feature, operation, expected) and belongs to model_writes, and its four readers failed on the old form and pass on the new one; the paths of removed tests are dropped and every unmapped test file names its feature; the dsn_connection and schema_install texts state the current behaviour and docs/features(.ko).md is generated from contracts/features.json; the new feature catalog_connection (Rust partial) owns the orm-build catalog tests; a verification command runs node …/typescript/bin/tsc instead of a .bin link. Owner tests must lie under clients/<language>, so bench/go moved to clients/go/bench. The four conformance runners take a repeatable vector selection. Defects found by the new cases and fixed Red then Green: the checker read the second Go run from the test cache (the new checker test counted 1 run of 2; go test -count=1 now runs both); a SQLite RESTRICT foreign key violation reported as SQLite 1811 was DRIVER and is FOREIGN_KEY in the four clients and docs/spec/errors.yaml, while another 1811 of a trigger RAISE stays DRIVER; CHECK violations were not CONSTRAINT in TypeScript, PHP and Rust; PHP kept the unsplittable plan of an IN list beyond the bind limit in its plan cache (memory held after the count fell from 87 MB to 5 MB); the Rust catalog read a MySQL _bin VARCHAR as binary and failed its insert check with ROW_WRITE_MISMATCH; the PHP schema install case read the manifest text without its hash after the T8.8.1 merge. Raw <value> and <nil> quotes of the checklists are code spans, because VitePress stopped on them and docs-check failed. Found and recorded: T18 to T22. On the tree merged with T8.6.8 and T8.8.2, make feature-check passes in one run: "feature coverage: 17 contracts executed", the 72 verification commands and "features: 17 feature contracts passed" in 1453 s, while clients/rust/target grows to 11.8 GB. Two shared-database defects that the merged tree exposed are fixed: the Rust rollback_fault case (clients/rust/orm/tests/rollback.rs) drops rollback_probe at its end, because a table left in the test database failed the schema().empty() case of the Rust integration binary ("mysql: schema().empty() on a database without tables"); and the schema set databases of clients/rust/tests/src/decimal_physical.rs are named with the process id and a counter, because two parallel MySQL cases created the same timestamp name and one failed with MySQL error 1007 "database exists"; the 9 cases of decimal_physical then passed three times in a row. | [o] |
| T16 | Open the send-savepoint SQLite file beside TEST_ENV. Cause: the Makefile named it .runtime/servers/send-savepoint.sqlite under the checkout, which a worktree does not have, so sqlite_send_savepoint failed with "unable to open database file" in client-db-check and client-pooler-check. | — | Evidence: with the old path cargo test -p orm --lib sqlite_send_savepoint failed with "unable to open database file" in this worktree; SEND_SQLITE_DSN is now $(dir $(abspath $(TEST_ENV)))send-savepoint.sqlite, which make -n client-db-check expands to the servers directory of TEST_ENV, and the three send_savepoint tests pass there. | [o] |
| T17 | Fail the Rust client benchmark when its DSN is unset. Cause: clients/rust/tests/src/client_bench.rs replaces a missing ORM_BENCH_MYSQL_DSN with a built-in local DSN, a fallback that no specification names. | — | Evidence: clients/rust/tests/tests/client_bench_dsn.rs runs the built client_bench with ORM_BENCH_MYSQL_DSN removed and with it empty. Before the change both tests failed: without the variable the bench ran against the built-in socket and exited 0, and with it empty the bench panicked on the DSN scheme without naming the variable. client_bench now exits 1 and prints "ORM_BENCH_MYSQL_DSN is required" in both cases, and the 2 tests pass in cargo test -p orm-tests --test client_bench_dsn and in cargo test --workspace with TEST_ENV. With TEST_ENV the bench ran 100 iterations against the seeded bench database and exited 0. No make target runs client_bench: make perf-check runs only the Go and PHP gates, and the DSN tests run in the cargo test --workspace of client-db-check, which stops on T17.1. make rust-check and make rust-fmt-check pass. | [o] |
| T17.1 | Make the orm-schema time bounds pass in client-db-check. Cause: cargo test --workspace in client-db-check failed in two orm-schema targets under a load average of about 70: the debug build of physical_json::tests::preflight_bounds measured node-limit 15.15 s and physical_graph_vectors measured 45.9 s and 51.9 s, each against a 15 s bound. | — | Evidence: the bound measured the wrong quantity. docs/spec/schema.md fixes Rust debug builds and a 15-second graph-test deadline, and the tests compared wall-clock time with it; on this shared machine (11 cores, load average 16-92 during this work) wall-clock time includes the time other processes hold the processors. Run alone at a load average near 30, physical_graph_vectors failed twice at 17.31 s and 26.48 s wall-clock while the process used 10.60 s and 11.86 s of CPU (user plus system), and preflight_bounds used 6.11 s of CPU for 19.23 s wall-clock; the Go TestPhysicalGraphVectors failed at 64.46 s and 81.71 s wall-clock inside go test ./clients/go/... and took 2.92 s wall-clock and 4.13 s CPU alone. The deadline stays 15 s and now bounds CPU time: Rust CaseClock (clients/rust/orm-schema/tests/common/case_clock.rs, clock_gettime(CLOCK_THREAD_CPUTIME_ID) through a libc dev-dependency) measures the thread that runs the case in the eight physical graph, document and JSON-limit tests and the 1-second record case bound, Go caseClock (clients/go/orm/case_clock_test.go, getrusage) and PHP processCpuNs (clients/php/tests/process_cpu.php) measure the test process, whose tests run one at a time, and every case prints both times. After the change physical_graph_vectors reported cpu=10.25 s wall=13.43 s and node-limit cpu=2.05 s, the Go graph test cpu=5.19 s, the three PHP tests pass, and ORM_CLIENT_DB_LANGS=rust make client-db-check (78 passing test results) and ORM_CLIENT_DB_LANGS=go make client-db-check pass with TEST_ENV at load averages 16-31. docs/spec/schema.md states the corrected criterion. The other wall-clock deadlines are T17.6. | [o] |
| T17.2 | Fail the example programs when ORM_BENCH_MYSQL_DSN is unset. Cause: examples/complex and examples/thin-slice replace a missing ORM_BENCH_MYSQL_DSN with a built-in local socket DSN in their Go, PHP and Rust programs, and both README files name that DSN as the default. | — | Evidence: the Go, PHP and Rust programs of examples/complex and examples/thin-slice exit with status 1 and print "ORM_BENCH_MYSQL_DSN is required; it names the seeded bench database" without connecting when the variable is unset or empty, and both README files state it. Go TestExamplesRequireBenchDSN (examples/dsn_test.go) builds the Go programs and runs them and the PHP programs with the variable removed and empty; Rust examples_fail_when_dsn_is_unset and examples_fail_when_dsn_is_empty (clients/rust/tests/tests/example_dsn.rs) run the built complex and demo. Before the change all 8 Go subtests failed: the Go programs exited 0 against the built-in socket, and the PHP programs exited 255 with "Unknown named parameter $schemaPath", because they still passed the schemaPath that T8.2.6.3.4 removed from the PHP Config; after removing it and the schema argument the PHP programs also exited 0 against the built-in socket. Both Rust tests failed ("left: Some(0), right: Some(1)"). After the change the 8 subtests and the 2 Rust tests pass, and with TEST_ENV the six programs exit 0; the thin-slice outputs are byte-identical and the complex outputs are equal JSON values. The complex outputs differ in key order and indentation although its README compares them with diff, which is T17.4. | [o] |
| T17.3 | Read the Rust native benchmarks' database from a declared DSN. Cause: bench/rust/src/native.rs and bench/rust/src/driver_compare.rs connect to the fixed socket /tmp/mysql.sock and database orm_bench instead of a declared DSN. | — | Evidence: bench/rust/src/native.rs and bench/rust/src/driver_compare.rs read the database from ORM_BENCH_MYSQL_DSN, the DSN that the Go bench and client_bench use: sqlx parses it with MySqlConnectOptions::from_str and mysql_async with Opts::from_url, an unparsable DSN exits with status 1 naming the variable, and an unset or empty one exits with status 1 and "ORM_BENCH_MYSQL_DSN is required; it names the seeded bench database" without connecting. benchmarks_fail_when_dsn_is_unset and benchmarks_fail_when_dsn_is_empty (bench/rust/tests/dsn.rs) run the built native and driver_compare; before the change both failed because native connected to /tmp/mysql.sock and stopped with status 101 ("left: Some(101), right: Some(1)"), and after it both pass. make rust-driver-check now runs these tests. With TEST_ENV both programs connected to the declared database and stopped with status 101 at the first row: "Rust type u64 (as SQL type BIGINT UNSIGNED) is not compatible with SQL type BIGINT", which is T17.5. | [o] |
| T17.4 | Make the complex example outputs byte-identical. Cause: examples/complex/README.md compares the Go, PHP and Rust outputs with diff, but the outputs are equal JSON values with different key order and indentation, so the documented comparison fails. | — | Evidence: make example-check builds the Rust complex and demo programs and runs TestExampleOutputsAreIdentical (examples/outputs_test.go, build tag examples), which runs the Go, PHP and Rust programs of examples/complex and examples/thin-slice against the seeded bench database and requires byte-equal stdout. Before the change the complex subtest failed: Go wrote 2241 bytes with sorted top-level members and two-space indentation, PHP 3121 bytes in declared order with four-space indentation, and Rust 2243 bytes with every object's members sorted and "read_sum": 456000.0, because json! turns each row into a sorted map and serde_json writes an integral f64 with a fraction; thin-slice passed with 200 bytes each. The three complex programs now write compact JSON whose members follow one declared order (a Go struct, the PHP array, a Rust Serialize struct that keeps the client's row JSON) and write read_sum and like_avg as their shortest decimal without a forced fraction, as Go and PHP do; Rust returns serialization errors from main instead of unwrapping them. After the change both subtests pass with 1183 bytes and 200 bytes from each language, and the README states the format and the check. make example-check is part of make check. | [o] |
| T17.5 | Decode the bench schema types in the Rust native benchmarks. Cause: bench/rust/src/native.rs and bench/rust/src/driver_compare.rs decode seq and the other keys as u64 and the booleans as u8, while schema/bench.dbs declares signed i64 keys, so both programs stop at the first row against the seeded bench database; make rust-driver-check only compiles them. | — | Evidence: native_reads_the_seeded_database and driver_compare_reads_the_seeded_database (bench/rust/tests/seeded.rs) run the built programs with 30 and 10 iterations against ORM_BENCH_MYSQL_DSN and require status 0 and a statistics line for every workload. Before the change both failed with status 101 at column 0: "Rust type u64 (as SQL type BIGINT UNSIGNED) is not compatible with SQL type BIGINT". Both programs now decode the declared types: i64 keys, i32 counts, bool, datetime(6) as NaiveDateTime through the sqlx chrono feature, and varchar as String; native propagates every column decode error instead of replacing it with an empty value, its relation step binds only the keys its SQL has placeholders for instead of two extra values, and both programs reject an iteration argument that is not an integer of at least 3 (native) or 10 (driver_compare) instead of replacing it with the default. After the change make rust-driver-check runs the two DSN tests and the two seeded tests one at a time with TEST_ENV, and all 4 pass. docs/operations/perf.md states what the check runs. | [o] |
| T17.6 | Bound the other test deadlines on CPU time. Cause: the deadlines that the remaining tests assert after a case finishes (for example the 1-10 s dbspec and 1-3 s physical bounds of orm-schema and their Go and PHP counterparts) still compare wall-clock time, which on a shared machine includes the time other processes hold the processors, and the TypeScript graph tests bound wall-clock time with their 15-second node:test timeouts. | — | Evidence: make timing-check runs tests/timing/preempted.test.mjs, which runs each test below in its own process group and stops the group with SIGSTOP for 90 ms after every 10 ms of running, so it receives one tenth of wall-clock time. Before the change the four stress budgets failed there: Go median parse 523 ms over 100 ms, Rust 518 ms over 300 ms, PHP 3110 ms over 400 ms and TypeScript 1232.5 ms over 250 ms; the Rust orm-schema vector deadlines and the six other PHP dbspec tests passed. Every limit that a test sets on its own computation now bounds CPU time and every case prints CPU and wall-clock time: the new workspace crate clients/rust/case-clock (CaseClock, clock_gettime(CLOCK_THREAD_CPUTIME_ID)) measures the case thread in the seven orm-schema vector tests, the dbspec_stress example and the two orm-build accessor and page tests; Go caseClock (engine/dbspec/case_clock_test.go) measures the OS thread that the stress test locks, because the process CPU time also holds the garbage collector workers (median 135 ms and 120 ms of process CPU against 71-80 ms of thread CPU); clients/php/tests/case_clock.php (caseClockStart, caseClockElapsed) measures the single-threaded PHP process in the seven dbspec tests; the TypeScript stress test measures process.threadCpuUsage (process CPU median 228 ms against 135 ms of thread CPU). After the change all 11 preempted tests pass with medians of 76.7 ms (Go), 49.1 ms (Rust), 391.0 ms (PHP) and 153.1 ms (TypeScript) of CPU time, and make dbspec-go-check, make dbspec-php-check, make dbspec-ts-check and make dbspec-rust-check pass. The other deadlines keep wall-clock time: the timers that stop a stuck case (Go runTimed, runWithDeadline and exec.CommandContext, Rust tokio::time::timeout and run_probe, the node:test timeouts, including the 15000 ms dbspec vector timeouts) measure a hang, which uses no CPU; the limits on work that a database or another process does (the Rust 300-600 s apply and introspection suites, the PHP apply, plan-apply, introspection and runtime tests, the introspection comparison budget) measure time spent outside the test process; and the cancellation and lock-wait assertions test timing behavior itself. The unused Go caseClock that T8.2.6.4 left in clients/go/orm is removed, AGENTS.md states the rule, and docs/spec/dbspec.md states the CPU budgets and the check. | [o] |
| T17.7 | Make the native benchmark writes and binds match the bench schema. Cause: relation4 and listN in clients/go/bench/native_test.go bind two bench-salt values that their SQL has no placeholder for, so go test ./clients/go/bench -bench Relation4 fails with "sql: expected 1 arguments, got 3", and the native insert of bench/rust/src/native.rs writes the plain text ins@example.com into the AES column aes_hex_email, which the Go baseline encodes with HostEncode. | — | Evidence: Go TestNativeStatementsEqualClient (clients/go/bench/statements_test.go) compares the native PK and 100-row statements with the statements the generated client runs, and TestNativeWorkloadsRead runs every native read workload against the bench database; before the change both failed: the native statements selected raw hex AES columns and ip while the client selects UNHEX columns, price and the hidden aes_key_version, and listN failed with "sql: expected 1 arguments, got 3". The native statements now equal the client's, the native scan decodes the AES columns with the row's key version through one keyring, and relation4 and listN bind only their keys; both tests pass and make perf-check runs them before the hot-path checks. The mismatch was what the list100 hot-path check measured: at the 2026-09-27 commit "docs: record client overhead measurements" the ratio was 1.13 to 1.18 and at HEAD 1.22 to 1.32 under the same load, with client allocations unchanged (4438 against 4440 per call); with the corrected baseline five runs measured 1.10 to 1.15 against the bound 1.25 and PK 1.08 to 1.13 against 1.35. Rust insert_email_is_an_aes_hex_envelope (bench/rust/src/native.rs) failed because the value was not hex and passes now that native writes the hex AES ciphertext of bench-salt, as the Go baseline's HostEncode does. docs/operations/perf.md states the same-SQL check. | [o] |
| T18 | Load a relation over every component of a composite foreign key. Cause: a relation request carries one key pair, so composite_membership cannot be loaded from composite_account by tenant_id and account_id together in any client; the T15 composite_key_rows cases join on one component and filter the other, while contracts/features.json composite_keys claims relations that preserve every key component. | — | Evidence: a relation request carries keys, one {left, right} pair per key component in key order, in place of left and right (docs/spec/protocol.md 1.2 and its Korean pair; IRRelation and the new IRKeyPair in contracts/interfaces.json). The Go, PHP, Rust and TypeScript validators reject an empty keys, a pair without a column and a column used twice on one side with IR_INVALID and an unknown column with COLUMN_UNKNOWN; the planners bind every pair, so the relation step reads (tenant_id, account_id) IN ((...)). In generated code each match<L>With<R>() adds one pair in call order (Rust Core::add_match replaces set_match, which overwrote the pair), and the relation of a child with its own connection keys and filters by every component (a tuple condition for two or more). The composite_key_rows cases of Go, PHP, Rust and TypeScript now write tenant 990004 (accounts 1 and 2, memberships member and owner) and tenant 990006 (account 2, membership guest), load the memberships of the tenant 990004 accounts by matchTenantIdWithTenantId().matchAccountIdWithAccountId() in the same statement and through a child with its own connection, and expect exactly 990004/1:990004/1/member and 990004/2:990004/2/owner; they delete every row they write. Red: before the change all four failed on MySQL, PostgreSQL and SQLite with the extra row 990004/2:990006/2/guest, because the second match replaced the first and the relation joined by account_id alone. Green, each on MySQL, PostgreSQL and SQLite against the seeded bench database: Go TestCoverageCompositeKeyRows 3/3, PHP and TypeScript composite_key_rows 3/3 each, Rust coverage_composite_keys::coverage_composite_key_rows 3/3, and the model_query_rows relation cases of the four clients 12/12. Owner checks of the changed code, all passing: go test ./engine/... (7 packages, with the new TestCompositeRelationKeys), 14 Go clients/go/orm relation, key and shape tests, Go TestJoinsAndRelations on 3 databases; TypeScript tsc --noEmit, tests/engine.mjs (new composite relation cases), tests/generate-scan.mjs (2 cases), tests/models-scan.mjs (2 tests), tests/model.mjs on 3 databases; PHP tests/engine_test.php (new composite relation cases), tests/relation_keys.php, tests/model_test.php (11 tests × 3 databases); Rust engine::tests (4 tests, new composite_relation_keys), orm-build generate (17 tests), integration joins_and_relations and writes on 3 databases, clippy -D warnings of orm, orm-build and orm-tests; interface-check with the recorded symbols and hashes, docs-rules-check, checklist-check, git-check, go-fmt-check and rust-fmt-check. The regenerated Go and TypeScript bench models carry MatchTenantIdWithTenantId and matchAccountIdWithAccountId. | [o] |
| T19 | Report a failed type assertion in the generated Go relation getters. Cause: generator output such as GetMemberships in clients/go/model/composite_account.go reads v, _ := x.m.Related(...).(...), which drops a mismatched relation value instead of reporting it. | Acceptance: a generated Go relation getter returns (<result>, error): the stored relation value, a nil result without error when the row holds no related row, and an INTERNAL error naming the relation result and the requested type when the stored value has another type; every generated Go model is regenerated and none reads a relation with v, _ :=; the Rust relation getters, which return None for a failed downcast_ref or the other relation kind, follow the same rule with orm::Result<Option<..>>; tracked Go and Rust unit cases store a mismatched relation value and receive INTERNAL, and the Go and Rust generator tests check the emitted getter; PHP and TypeScript return the stored relation value without a type conversion, so they cannot drop it. | Evidence: Red before the change: go test ./generator -run '^TestGoRelationGetterReportsMismatch$' failed because the generated getters returned one value ("not enough return values, have (*model.BrandModel) want (*model.BrandModel, error)"); TestRelatedAsReportsMismatch of clients/go/orm did not compile ("undefined: RelatedAs"); the orm-build test relation_getters_report_a_mismatched_relation_value failed with generated source lacks pub fn get_owner(&self) -> orm::Result<Option<&super::user::User>>; the orm unit test relation_results_report_a_mismatched_value did not compile because related_one returned Option<&M>. The Go generator writes return orm.RelatedAs[<result>](../x.m, "<name>"), and orm.RelatedAs returns the zero value for a row without the result or without a related row and INTERNAL relation result <name> holds <stored type>, not <requested type> for another type; related_one and related_many return Result<Option<..>> and report another model, a collection read as a row or a row read as a collection as INTERNAL with the result name and the requested type. clients/go/model is regenerated (author, composite_account, service and user; clients/go/decimalmodel has no relation and stays equal), no Go source reads a relation with v, _ :=, the call sites of the Go and Rust tests and conformance runners propagate the error, docs/interfaces(.ko).md states the rule, and contracts/symbols/go.json and rust.json record the new signatures with their hashes in contracts/interfaces.json. The new clients/go/orm/related_test.go belongs to the model_queries feature of contracts/features.json. Owner checks run, all green: the four Red cases; go test ./generator (all generator tests, 70.6 s); go test ./contracts ./tests/interfaces/check; TestJoinsAndRelations of clients/go/model on SQLite and on MySQL and PostgreSQL databases created for the run and dropped after it; the featurecoverage cases TestCoverageModelQueryRows and TestCoverageCompositeKeyRows on the three bench databases; the Rust orm-build generate tests (18 passed); the Rust integration case joins_and_relations on SQLite and on created and dropped MySQL and PostgreSQL databases; the Rust coverage cases coverage_model_query_rows and coverage_composite_key_rows on the three bench databases; cargo clippy -p orm -p orm-build -p orm-tests --all-targets -D warnings; make interface-check, go-fmt-check, rust-fmt-check, docs-rules-check, checklist-check and git-check. | [o] |
| T20 | Reject an invalid iteration argument of the Rust client_bench. Cause: clients/rust/tests/src/client_bench.rs replaces an argument that is not a number with 3000 (.parse().ok().unwrap_or(3000)), a default that no specification names. | Acceptance: client_bench requires its iterations argument: without it, or with a value that is not an integer of at least 1, it exits with status 1 before it connects and prints an error naming the iterations argument and the value it received; the bench/rust native and driver_compare, which reject a non-number but run 3000 and 1000 iterations without an argument, require the argument the same way; tracked process cases run the three programs with a missing, a non-number and a too small argument; the Go bench and the PHP performance test take no iteration argument and TypeScript has no bench program, so they hold no such default. | Evidence: Red before the change: clients/rust/tests/tests/client_bench_args.rs failed both cases, because client_bench without an argument and with "many" went on to the DSN check ("ORM_BENCH_MYSQL_DSN is required ..."); in bench/rust/tests/dsn.rs benchmarks_require_the_iterations_argument failed because native without an argument reached the DSN check, and benchmarks_reject_an_invalid_iterations_argument failed on the old message "iterations must be an integer of at least 3, got "many"", which did not name the argument. Each of the three programs reads its first argument with one iterations(minimum) function and has no default count: a missing argument prints usage: <program> <iterations>; the iterations argument is required and a value that is not an integer of at least the minimum (1 for client_bench, 3 for native, 10 for driver_compare) prints the iterations argument must be an integer of at least <minimum>, got <value>, both with status 1 before connecting; docs/perf(.ko).md states it, and the new clients/rust/tests/tests/client_bench_args.rs belongs to the performance_gate feature of contracts/features.json. Owner checks run, all green: cargo test -p orm-tests --test client_bench_args --test client_bench_dsn (4 passed); in bench/rust cargo test --test dsn (4 passed) and cargo test --test seeded on the seeded MySQL bench database (2 passed); client_bench 10 on the bench database printed its three statistics lines with n=10; cargo clippy -D warnings of orm-tests and of bench/rust; rust-fmt-check, docs-rules-check, checklist-check and git-check. | [o] |
| T21 | Decode MySQL and PostgreSQL date and time cells in the Rust catalog grid. Cause: GridCell has no temporal value, so table_page of a table with a datetime column fails with "Unsupported catalog cell type" on MySQL and PostgreSQL; the T15 catalog_read case pages user for that reason. | Acceptance: GridCell has Date, Time and DateTime cells that hold the dbspec text forms YYYY-MM-DD, HH:MM:SS and YYYY-MM-DD HH:MM:SS with fraction digits; MySQL DATE, TIME and DATETIME and PostgreSQL date, time and timestamp without time zone decode into them, while the time zone types (MySQL TIMESTAMP, PostgreSQL timestamptz and timetz) stay unsupported, because every connection reads zone-less values in UTC; a read of a described table (table page, row lookup and insert reads) writes exactly p fraction digits, p being the declared precision of the column (MySQL datetime without p is 0, PostgreSQL timestamp without p is 6), and a read-only grid query, which has no column declaration, writes six; a time of day outside 00:00:00 to 23:59:59.999999 fails instead of wrapping; a temporal primary key cell fails explicitly as a row identity bind; SQLite has no temporal storage class, so its cells keep the storage class of the stored value as its decimals do, and a dbspec column reads as its checked text; a tracked live-db test pages a table with date, time(0), time(3), datetime(0) and datetime(6) columns and reads the same values through a grid query on the three databases; the catalog_read case pages author, which has datetime(6) columns, on MySQL, PostgreSQL and SQLite; only the Rust client has a catalog connection (feature catalog_connection), so no other client holds the defect. | Evidence: Red before the decoders, with the three cells added to GridCell: the new live-db test clients/rust/orm-build/tests/grid_temporal.rs failed on MySQL and PostgreSQL with "table page: Err("error occurred while decoding: Unsupported catalog cell type")" and the same error for the temporal key page, while SQLite passed; coverage_catalog_read, changed to page author, failed on the MySQL and PostgreSQL bench databases with "author page: "error occurred while decoding: Unsupported catalog cell type"" and passed on SQLite. The MySQL decoder reads DATE, DATETIME and TIME through sqlx and rejects a negative or 24-hour TIME; the PostgreSQL decoder reads the binary date, time and timestamp values itself, because the sqlx chrono decoders turn 24:00:00 into 00:00:00; clients/rust/orm-build/src/catalog/temporal.rs cuts the six decoded fraction digits to the declared p for the table page, the row lookup and the three insert reads and fails with GRID_TEMPORAL_PRECISION when a cut digit is not zero. The grid query of the test reads 03:04:05.000000 and 2026-01-02 03:04:05.000000 where the page reads 03:04:05 and 2026-01-02 03:04:05; a row with temporal cells takes an update of its key; CAST('25:00:00' AS TIME), CAST('24:00:00' AS time), PostgreSQL infinity values, a MySQL TIMESTAMP column and a PostgreSQL timestamptz fail; a DATE primary key update fails with "temporal row identity is not supported". A catalog write of a temporal column still binds text, which the read-back temporal cell does not equal, so it fails with ROW_WRITE_MISMATCH instead of succeeding. docs/interfaces(.ko).md and docs/schema(.ko).md state the cells, the new test belongs to the catalog_connection feature of contracts/features.json, and contracts/symbols/rust.json with its hash records the new functions. Owner checks run, all green: grid_temporal on SQLite, MySQL and PostgreSQL (each run dropped the tables it created); coverage_catalog_read and coverage_catalog_row_mutation on the three bench databases; the whole cargo test -p orm-build --features live-db (21 test targets, unit and doc tests included); the two unit tests of the precision cut; cargo clippy -D warnings of orm-build with live-db and of orm-tests; make interface-check and go test ./contracts ./tests/interfaces/check; node scripts/features/build.mjs --check; rust-fmt-check, docs-rules-check, checklist-check and git-check. node scripts/features/check.mjs lists only clients/php/tests/sqlite_without_mysql.php of the T8.8.3 merge as a test file without a feature. | [o] |
| T21.1 | Give SQLite date and time cells of a described table the temporal grid cells (follows T21). Cause: on SQLite a date, time or datetime column of a described table reads as Text, while MySQL and PostgreSQL give Date, Time or DateTime for the same dbspec column, so the catalog grid differs by database for one neutral type. | Acceptance: reading a described table (table page, row lookup, insert result) gives the same temporal cell on the three databases for the same column and value; a read-only grid query, whose columns have no declaration, keeps the stored text on SQLite. | Evidence: Red before the change: grid_temporal, now expecting the temporal cells from the SQLite page, failed on SQLite with "sqlite: table page: Ok(TablePage … Text(…))" and with loose values that the page accepted as text, and coverage_catalog_read on the SQLite bench database failed with "sqlite: first author page" (left Text("2026-06-01 00:00:00.000000"), right DateTime(…)). clients/rust/orm-build/src/catalog/temporal.rs turns a Text cell of a SQLite column declared DATE, TIME or DATETIME into Date, Time or DateTime when it is in the dbspec form (a calendar date from 0001-01-01 to 9999-12-31, a time of day with 0 to 6 fraction digits), keeps NULL, and fails every other value with GRID_TEMPORAL_VALUE: <column> holds a value that is not a dbspec <kind>; the SQLite renderer CHECK fixes the fraction digits at p, so the cell equals the MySQL and PostgreSQL cell of the same column and value. The read-only grid query still returns Text on SQLite. A SQLite DATE key now fails as a row identity bind like the others. docs/interfaces(.ko).md states it, and contracts/symbols/rust.json with its hash records the new functions. Owner checks run, all green: grid_temporal on SQLite, MySQL and PostgreSQL (it rejects 2026-01-02T03:04:05Z, 20260102, 2026-02-30 03:04:05, 2026-01-02 24:00:00 and seven fraction digits on a SQLite page; the tables it created were dropped); the orm-build unit tests (12, among them the dbspec form cases); coverage_catalog_read and coverage_catalog_row_mutation on the three bench databases; the whole cargo test -p orm-build --features live-db; cargo clippy -D warnings of orm-build with live-db and of orm-tests; make interface-check and go test ./contracts ./tests/interfaces/check; rust-fmt-check, docs-rules-check, checklist-check and git-check. | [o] |
| T21.2 | Write date and time cells through the catalog grid (follows T21). Cause: a catalog write binds a temporal value as text, so reading the row back gives a temporal cell that differs from the written one and the write fails with ROW_WRITE_MISMATCH on every database. | Acceptance: a catalog insert and update of date, time and datetime columns succeed on MySQL, PostgreSQL and SQLite and the read-back cell equals the written cell at the column's declared precision. | Evidence: Red before the change, with the new write case of grid_temporal binding the values as text: the insert failed with "ROW_WRITE_MISMATCH: stored inserted values differ" on SQLite and MySQL and with "column "d" is of type date but expression is of type text" on PostgreSQL, and the update of a DATE key failed with "ROW_UPDATE_INVALID: temporal row identity is not supported" on the three databases. P and ParamType gain Date, Time and DateTime; their text must be in the dbspec form (TOOL_BIND_INVALID otherwise, before execution); MySQL and PostgreSQL bind chrono date, time and timestamp values and PostgreSQL declares those parameter types, SQLite binds the text; a temporal grid cell converts to and from these binds, so a temporal row identity binds too. The case inserts 2026-01-02, 03:04:05.120 and 2026-01-02 03:04:05.123456 into date, time(3) and datetime(6) columns, updates them to 2027-12-31, 23:59:59.999 and 0001-01-01 00:00:00.000001, reads each back as the written cells from the insert, the update and the page, rejects 2026-01-02T03:04:05, and updates a row keyed by a DATE. The typed NULL case covers the three kinds. docs/interfaces(.ko).md states the binds, and contracts/symbols/rust.json with its hash records them. Owner checks run, all green: grid_temporal on SQLite, MySQL and PostgreSQL (the tables it created were dropped); the whole cargo test -p orm-build --features live-db, typed_binds included; coverage_catalog_read and coverage_catalog_row_mutation on the three bench databases; cargo clippy -D warnings of orm-build with live-db and of orm-tests; make interface-check and go test ./contracts ./tests/interfaces/check; rust-fmt-check, docs-rules-check, checklist-check and git-check. | [o] |
| T22 | Remove the root schema:check npm script. Cause: package.json runs node scripts/schema/check.mjs, which T8.2.6.4 removed. | Acceptance: the root package.json has no schema:check script and no tracked file outside the history files names it; make repo-check fails when a root npm script names a repository path (a word with a /) that is neither a tracked file nor a directory of tracked files, naming the script and the path; a tracked node test covers a missing path, a tracked file, a tracked directory and a word without a /, and make repo-check runs it. | Evidence: Red before the removal: with the new path check in scripts/repo/check.mjs, make repo-check failed with "package.json script schema:check names scripts/schema/check.mjs, which is not a tracked file or directory" and "repository check: 1 script path problems remain". The schema:check line is removed from package.json, and no other tracked file names the script or scripts/schema/check.mjs outside CHANGELOG and the checklists. scripts/repo/scripts.mjs holds scriptPathErrors, which treats every word with a / of a root npm script as a repository path and accepts a tracked file or a directory that holds one after a leading ./ is removed; scripts/repo/check.test.mjs covers the four cases and the Makefile target runs it before the check. Owner checks run, all green: make repo-check (the node test, then "repository check: 890 tracked paths inspected"), docs-rules-check, checklist-check and git-check. | [o] |
| T24 | Make every check report each case as it runs, with its own time limit (AGENTS testing rule). Cause: make feature-check runs about 25 minutes and prints nothing until coverage.mjs ends; dbspec-introspect-compare-check runs 5-11 minutes and prints only the start and end of each database; the physical Go checks in the Makefile are bounded only by whole-run limits (-timeout 70m for dbspec-apply-check, 40m and 35m for the conformance runner checks, 0 for the dialect facts check). | Acceptance: every check prints, for each case, its start, its result (passed or failed with the reason) and its elapsed time while it runs; every case carries its own time limit and no whole-run limit replaces it; a case that runs longer than a minute prints its steps with their elapsed time; a test asserts the output form of each runner. | Evidence: T24.1-T24.5 survey every Makefile target and runner script. Go tests (check, the dbspec physical checks, dialect-facts, the conformance checks, example, fuzz, dbspec-go, client-db and perf scripts, feature commands): GO_TEST (go test -v -timeout 0) and internal/testcase cases. Node tests and runners (checklist, repository, git, docs-rules, docs-build, docs-check, docs-static, docs-verify-idempotent, feature-check, feature-docs, dbspec-ts and the TypeScript physical checks, dbspec-compare, ts-check, ts-min-check, ts-model, timing, group-rows, unselected-column and decimal physical checks): tests/testcase.mjs. PHP tests (client-unit, dbspec-php and the PHP physical checks, client-db, perf, php-without-mysql, conformance-result, tests/codec/check.php): tests/testcase.php; tests/conformance/run_result_tests.py is removed. Rust tests and programs (dbspec-rust, the Rust physical checks, rust-send-savepoint, rust-driver, client-db's integration program): orm-testcase. Interface and conformance CLIs: testcase.Run. Build, format, lint and package commands: tests/run-case.mjs. Not checks and unchanged: test-servers, test-servers-stop, docs-dev, decimal-db-setup, decimal-bench-sqlite, scripts/bench-db.sh, scripts/build-artifacts.sh, scripts/typescript/node-min.sh and the stress and vector generators. make testcase-check asserts the form of the Go, JavaScript, PHP, Rust and command harnesses, and coverage.test.mjs that of coverage.mjs. The full suite runs once after the checklist is complete. | [o] |
| T25 | Give every case that needs an empty database a database of its own. Cause: the schemaEmpty, conditions and joinsAndRelations cases and the other cases that assert schema().empty() before installing run on the fixed shared database of ORM_TEST_MYSQL_DSN and the public schema of ORM_TEST_POSTGRES_DSN, so any table that another run or an interrupted run left there fails them; on 2026-10-03 the full run failed five feature commands (model_queries PHP and TypeScript, parameter_chunking Rust, PHP and TypeScript) and ts-check on a database without tables while the clients were correct. | Acceptance: every case that asserts an empty database or installs a schema creates its own uniquely named database (MySQL) or schema (PostgreSQL), or its own SQLite file, and drops it when it ends, also after a failure; no case assumes the shared database is empty; a test leaves a table in the shared database and shows that the cases still pass and leave nothing of their own behind. | Evidence: T25.1 (Go, internal/testdb), T25.2 (PHP, clients/php/tests/case_database.php), T25.3 (TypeScript, clients/typescript/tests/case-database.mjs) and T25.4 (Rust, clients/rust/case-database) give each case that checks an empty database or installs a schema the database orm_case_<pid>_<n>, created through ORM_TEST_MYSQL_DSN or ORM_TEST_POSTGRES_DSN as the admin connection, or the SQLite file orm-case-<pid>-<n>.sqlite, and drop it when the case ends, also after a failure, reporting both as STEP lines. PostgreSQL gets a database instead of the schema this item named, because schema().empty() counts every schema other than public as content, so a case schema inside a shared database cannot pass an empty check. Survey (grep for empty(), install(, ORM_TEST_*_DSN and DROP TABLE in the tests of the four clients and in the feature and coverage runners): the cases that checked emptiness of a shared database were TypeScript schemaEmpty and install() (every installing model.mjs case), PHP schema empty and Rust schema_empty; the cases that installed into a shared database are listed in T25.1-T25.4; the feature commands model_queries and parameter_chunking failed through their verification commands, which run model_test.php, model.mjs and the Rust integration program; the coverage cases (coverage_*) run on the seeded bench DSN that feature-check supplies and whose state it reads before and after each run, and stay there; the dbspec physical tests and the schema-set tests already used databases of their own (the schema-set tests now use the helpers). The statement timeout cases through the PgBouncer entry orm_test_single, which reaches only orm_test, install under a table name of their own there and drop only that table. make case-database-check (scripts/case-database-check.mjs, part of make check) builds the TypeScript client and the Rust integration program, creates a table orm_leftover_<pid> with one row in the shared MySQL database and PostgreSQL public schema, reads the shared state digests of tests/conformance/check, the PostgreSQL schemas, the orm_case_ databases and the orm-case- files, runs Go TestSchemaEmpty and ./clients/go/model, PHP model_test.php, TypeScript model.mjs and the Rust integration program, requires each to pass and every value read before to be unchanged, and drops its table in finally. With that table and the leftover tables routed and unowned of other runs in the shared databases it passed: Go TestSchemaEmpty 1 and ./clients/go/model 18 cases, PHP 66, TypeScript 57 and Rust 30 passed with no FAIL, the cases created and dropped 2, 18, 35, 33 and 20 databases, the shared state stayed mysql 238574e7… and postgres 636d2bfd…, no orm_case_ database and no orm-case- file remained, and the table was dropped. | [o] |
| T24.1 | Report every Go test and Go check runner case by case with its own deadline (part of T24). Cause: Go tests printed only package results without -v, the physical checks logged ad hoc start/result lines or none, and their only limits were whole-binary -timeout values. | Acceptance: every Go test case prints its start with its deadline, its steps when it can run longer than a minute, and its result with the elapsed time while it runs; every case carries its own deadline; no Makefile, script or feature command uses a whole-binary limit in its place; a test asserts the output form. | Evidence: the new package internal/testcase gives one form: RUN <case> deadline=<d>, STEP <case> elapsed=<d>: <step>, PASS <case> elapsed=<d>, FAIL <case> elapsed=<d>: <reason> (a test failure names the errors logged above it), SKIP <case> elapsed=<d>, and RUN <group> group for a test whose subtests carry the deadlines. Start(t, deadline) gives a context that ends at the deadline and a watchdog that, 5 s (Grace) after it, prints FAIL … deadline <d> exceeded with every goroutine stack and ends the binary; Run(name, deadline, work) reports a case of a program that is not a test; StepWriter turns each nonblank output line of a child process into a STEP line. The deadline classes state their basis: Compute 1m (memory and repository files only; the largest such case, TestStressDocument, took 2.4 s), Database 2m, Process 5m (cold go run/go build). Every top-level Test and Fuzz function of the 120 other Go test files starts a case: a class by what the test does, or its own deadline: probe cases of TestDialectFacts, TestDDLVectors, TestSchemaDocumentsApply, TestIntrospectRoundTrip, TestIntrospectUnsupported, TestPlanApply, TestApplyChain and TestSQLiteFileNameWithQuery 1m (probe 30 s plus cleanup 30 s) with a start step and each observed note as a step; TestApplyStressPlan 21m per database with a step every 2000 applied steps; TestIntrospectCompare 15m per database with a step every 1000 statements and per runner; TestApplyChainAcrossClients 7m per client pair with a step per runner action; the conformance physical tests a build case (40m and 30m) and a 32m case per language and database with first run, repeated run and comparison steps, and 6m per database for the state checks; TestExampleOutputsAreIdentical 5m per example; the hot path gates 5m; TestCPULoad starts a case only in the parent process, because its load child runs until the parent ends it. go run ./tests/conformance/check run reports the cases conformance/<db>/build, /<language> and /compare, with each command and its output lines as steps; go run ./tests/interfaces/check reports interfaces/manifest, interfaces/rust-build, interfaces/<language> and interfaces/results, with a failure's reasons in its FAIL line. The Makefile runs every Go test with GO_TEST = go test -v -timeout 0 in place of -timeout 10m, 70m, 30m, 40m, 35m, 3m and 0; scripts/client-db-test.sh, scripts/perf-test.sh and the Go commands of contracts/features.json use -v -timeout 0 (the -timeout 45s and 90s are gone). make testcase-check runs TestReportForm, which runs fixture tests in a child go test -v and asserts the lines and their order for a passing case with a step, a failing case after its error, a skipped case, a group with two cases, Run passing with a step and child output lines without the blank one, failing with its reason, ending at its context deadline and stuck past it, and a case past its deadline that prints FAIL, a goroutine stack and no PASS. Owner checks run, all green: make testcase-check; go test -v -timeout 0 of ./engine/..., ./contracts, ./tests/interfaces/..., ./tests/conformance/..., ./tests/dialects and ./generator (24 generator cases, longest 11.0 s); TestSQLiteFileNameWithQuery; the SQLite probes of TestDialectFacts, TestApplyChain and TestPlanApply on TEST_ENV; TestApplyStressPlan on SQLite (STEP TestApplyStressPlan/sqlite elapsed=2.249s: 2000 of 12000 steps applied … PASS TestApplyStressPlan/sqlite elapsed=1m15.03s); TestIntrospectCompare on SQLite (13 statement steps and four runner steps, PASS TestIntrospectCompare/sqlite elapsed=1m17.936s); TestPhysicalCounterCleanup on SQLite; TestClockMicrosecondsSQLite; go run ./tests/conformance/check run -driver sqlite twice (PASS conformance/sqlite/build elapsed=3m7.386s on a cold build, then each language and the comparison); go run ./tests/interfaces/check -language go --self-test; go vet with the tags physical, featurecoverage, ormtest, examples and decimalphysical; go-fmt-check, docs-rules-check, checklist-check and git-check. | [o] |
| T24.2 | Report every JavaScript test and Node check runner case by case with its own deadline (part of T24). Cause: make feature-check printed nothing until coverage.mjs ended; the TypeScript tests and Node runners printed ad hoc start/result/ok lines or only a summary, and plain test scripts stopped at their first failure without a deadline. | Acceptance: as T24.1 for every Node test file and Node runner, with a test that asserts the output form. | Evidence: tests/testcase.mjs gives the Go form (RUN <case> deadline=<d>, STEP, PASS, FAIL … : <reason>, durations written like Go's time.Duration) with runCase (an AbortSignal ends at the deadline and the case fails 5 s later), caseTest (a node:test test that runs a case), cases() (a script's sequential cases and its exit code), sections() (uninterruptible code regions: FAIL and exit after the deadline, FAIL when the process ends inside one), runGroup (a command whose runner reports its own cases) and stepLines (child output lines as steps). Classes: COMPUTE 1m, DATABASE 2m, PROCESS 5m. coverage.mjs reports each native run as <feature>/<role>/<language>/<database> run <n> with a deadline of the run's process limits added up (state readers and native commands, each 10m), a step per state digest and per command, and the stderr lines of cargo and go as steps, DSN redacted; features/check.mjs reports each verification command as a group with its output lines as steps and gains --feature <id>. Every node:test file uses caseTest with its former timeout or COMPUTE (clients/typescript/tests dbspec, dbspec-rules, dbspec-render, dbspec-plan, dbspec-mermaid, dbspec-apply-cleanup, dbspec-apply-physical, dbspec-plan-physical, dbspec-introspect, dbspec-stress, dbspec_runtime_db, models-scan; scripts checklist, git, repository and features tests; tests/conformance/result_typescript.test.mjs; tests/dbspec/compare check and runners tests; tests/dbspec/inputs.test.mjs; tests/timing/preempted.test.mjs, which streams the preempted child's lines as steps); the plain scripts aggregate_numeric, decimal_model, group_rows, dsn, engine, generate, generate-scan, codec-vector, styled-value, clock, driver-error, rollback, schema-set, sqlite-concurrency and model run each section as a case and set the exit code after all of them; the group-rows, unselected-column and decimal physical runners, tests/dbspec/compare/check.mjs and the checklist, git, repository, docs-rules, feature-docs, docs build, docs idempotence and docs static checks report their cases. The owner files whose output coverage.mjs or a physical runner parses (coverage_*.mjs, group_rows_db.mjs, decimal_model_db.mjs) keep their exact CASE <id> PASS output and are reported by that runner. The docs build stopped on raw <program> and <name> placeholders in the T19 and T20 records; they are code spans now. make testcase-check also runs tests/testcase.test.mjs, which asserts in child processes the lines and their order of runCase passing with a step, failing with its reason, ending at its signal and stuck past the deadline, of runGroup with child lines and without the blank one, of sections passing, failing, ending inside a case and stuck past the deadline with exit 1, and of caseTest under node --test; coverage.test.mjs asserts the nine lines of two passing runs and a failing one. Owner checks run, all green: make testcase-check; the node:test files above (265 tests); the TypeScript physical tests on their SQLite cases (83); the compare and input tests of the TypeScript runner (34); the preempted Go and TypeScript timing cases; dsn, engine, generate, generate-scan, codec-vector, styled-value, aggregate_numeric, decimal_model, group_rows and sqlite-concurrency; clock, driver-error, rollback and schema-set (33 cases on SQLite, MySQL and PostgreSQL); model.mjs (57 cases, longest 1.724 s); node scripts/features/coverage.mjs --feature planner (8 runs); node scripts/features/check.mjs --run --feature planner; the unselected-column (9) and group-rows (15) physical checks; docs build, idempotence (two builds equal in 179 files) and static check; checklist, git, repository and docs-rules checks. | [o] |
| T24.3 | Report every PHP test case with its own deadline and drop the Python result runner (part of T24). Cause: the PHP tests printed RUN/PASS lines in several forms (cpuMs=, elapsedMs=, ok , plain summaries), most stopped at the first failure without a FAIL line, the render, manifest, introspect and runtime cases had no deadline, and runtime_model and runtime_db bounded the whole script (30 s, 120 s) instead of each case; tests/conformance/run_result_tests.py printed its own form. | Acceptance: as T24.1 for every PHP test script, with a test that asserts the output form. | Evidence: tests/testcase.php gives the Go form with testcase_run (a closure case), TestCases (a script's cases and exit code), testcase_begin/testcase_step/testcase_end (a code section; an uncaught error that ends the script prints its FAIL line from the shutdown handler with the message without the Uncaught <class>: prefix and the file) and the classes TESTCASE_COMPUTE 60 s, TESTCASE_DATABASE 120 s and TESTCASE_PROCESS 300 s. With pcntl a SIGALRM at the deadline interrupts the case (system calls are not restarted) and 5 s later prints FAIL and exits; without pcntl (the official PHP image of php-without-mysql-check) and for deadlines below the alarm's second, a case past its deadline fails when it ends. The full error goes to stderr and its message to the FAIL line. case_clock.php's cpuCaseBegin/cpuCaseEnd keep each CPU-time limit as the assertion and stop a stuck case after ten times that limit in wall-clock time, because timing-check gives the process a tenth of the CPU. Converted: dsn, relation_keys, hostcodec, engine, runtime_model (its 30 s whole-script limit is gone), orm_gen, aggregate_numeric, decimal_model, group_rows, unselected_column, styled_value, aes_json, runtime_db (its 120 s whole-script limit is gone), sqlite_concurrency, sqlite_without_mysql, perf_gate (5 min per measured query, with the ratios as steps), model (each of 13 sections), clock, driver_error, rollback, schema_set, and every dbspec test (vectors and rules with their CPU totals as cpu-total cases, manifest, render, mermaid, plan, stress, apply cleanup, introspect, introspect stress, plan apply, apply); tests/conformance/result_php.php is a case. The coverage owner files and the group_rows_db, decimal_model_db and unselected_column_db owners keep their exact CASE <id> PASS output. make conformance-result-check runs the TypeScript build, the PHP result test, the TypeScript result test and go test -v -timeout 0 of tests/conformance/runner_go directly; run_result_tests.py is removed. make testcase-check also runs tests/testcase_test.php, which asserts in child processes the lines and their order of a passing case with a step, a failing case, a case stuck in PHP code and one blocked in sleep past a 1 s deadline, a case that ignores the interruption until the grace ends, sections that pass, report failed checks, end with an uncaught error and stay stuck, and of late cases and sections with pcntl disabled. Owner checks run, all green: php tests/testcase_test.php; make client-unit-check (28 cases); make dbspec-php-check (502 PASS lines over the two runs of each script); clock, driver_error, rollback and schema_set (33 cases), aes_json (3), runtime_db (9), model (66), sqlite_concurrency (3), dbspec_plan_apply (36), dbspec_apply (120), dbspec_introspect (57) and dbspec_introspect_stress on TEST_ENV; perf_gate on the bench database (ratios 1.22 and 1.17); the preempted timing cases of the seven PHP dbspec scripts; make conformance-result-check. | [o] |
| T24.4 | Report every Rust test case with its own deadline (part of T24). Cause: cargo test printed only per-binary results; the tests printed ad hoc RUN, PASS, start, pass, passed, running, finished and result lines through println!, which libtest hides without --nocapture, and had no per-test deadline. | Acceptance: as T24.1 for every Rust test and Rust check program, with a test that asserts the output form. | Evidence: the workspace crate clients/rust/testcase (orm-testcase, a dev-dependency of every crate and of bench/rust, a dependency of orm-tests) gives the Go form on stderr, written directly so libtest's capture does not hide it and stdout's test <name> ... ok lines stay parseable: case!(deadline) starts a case named like the libtest test, start(name, deadline) an inner case, Case::step, step (the innermost case of the thread, for helper functions), Case::fail (a FAIL without a panic); a panicking case prints its panic message as the FAIL reason, and a watchdog thread prints FAIL and exits 101 at the deadline plus GRACE (5 s), so tokio::time::timeout inside a case fails first. Classes COMPUTE 60 s, DATABASE 120 s, PROCESS 300 s, and wall_for_cpu(limit) = ten times a CPU-time limit for tests that assert one (timing-check gives the process a tenth of the CPU). Every #[test] and #[tokio::test] function (228 in 91 files, among them the in-crate unit tests and bench/rust) starts a case; the orm-schema dbspec tests use wall_for_cpu of their CPU limits for the test and for each vector, rule, Mermaid and plan case; the probe cases of the physical dbspec tests, the cleanup cases and the SQLite path vectors are inner cases; the ad hoc lines became steps or were dropped where the case's own RUN and PASS lines say the same; the integration program reports <case>/<database> cases (2 min each) and the dbspec_stress example reports one case with its parse and emit steps. make testcase-check also runs cargo test -p orm-testcase, whose report_form reruns the test binary on fixtures and asserts the lines and their order for a passing case with a step and a helper step, a panicking case with the first line of its message, an inner failed case inside a passing one, an async case, and a case past its deadline that prints FAIL, exits 101 and prints no PASS. Owner checks run, all green: cargo test -p orm-testcase; cargo test -p orm-schema -p orm-case-clock (240 PASS lines); orm tests zone, sqlite_file_name, dbspec_apply_cleanup and clock on TEST_ENV (37 PASS lines); orm-build --features live-db grid_cells and tool_columns with a step per database; bench/rust cargo test -- --test-threads=1 on the seeded bench database (6 tests); the release integration program with --case conditions on the three databases; the dbspec_stress example; cargo clippy --workspace --all-targets, -p orm-build --features live-db and -p orm --features test-faults with -D warnings; rust-fmt-check; go-fmt-check, which failed on internal/testcase/testcase_test.go as committed in T24.1 (one unformatted expression) and passes after gofmt in this item. | [o] |
| T24.5 | Report the build, format, lint, package and remaining runner commands of the checks as cases (part of T24). Cause: commands that do not report cases of their own (tsc and npm builds, Rust release builds and clippy, go test -c, gofmt, cargo fmt, go generate, the package, rust-150 and php-without-mysql scripts) printed their tool output without a start, a result line or a deadline, and the codec cross-check and the feature manifest validation printed only a summary. | Acceptance: every such command in a check prints its start with a deadline, its output lines as steps with the elapsed time and its result, and stops at its deadline; a test asserts the form. | Evidence: tests/run-case.mjs runs one command as a case (node tests/run-case.mjs <name> <n>s|<n>m [--cwd <dir>] -- <program> [args]): RUN with the deadline, every nonblank stdout and stderr line as a STEP, PASS, or FAIL with the exit status; at the deadline it ends the command's process group and fails with deadline <d> exceeded. The Makefile defines RUN_CASE, BUILD_DEADLINE (30m: release builds, clippy and go test -c compile every dependency from an empty target in 2.5-4 min here), TOOL_DEADLINE (5m: one run of tsc, gofmt, cargo fmt, go generate, npm or a container start) and TSC_BUILD, and wraps the eight tsc builds, the Rust example builds of dbspec-apply-pairs, dbspec-introspect-compare, dbspec-compare, dbspec-rust (the stress example is built once and run twice directly), example and timing checks, the clippy of rust-send-savepoint-check, npm run typescript:build of feature-check, timing-check and typescript-build, npm run typescript:check of ts-check, go-model-check, go-fmt-check, rust-fmt-check, the four commands of rust-check, rust-150-check, package-check and php-without-mysql-check; scripts/client-db-test.sh wraps the integration build. scripts/php-without-mysql.sh no longer prints its own RUN and PASS lines; the PHP case inside the official image reports itself without pcntl. tests/codec/check.php reports its vector, error and other-language cases; scripts/features/check.mjs reports the manifest validation as features/contracts; the CI workflow runs go test -v -timeout 0. tests/testcase.test.mjs asserts for run-case.mjs a passing command with its stdout and stderr lines as steps, a command that exits 3, a command killed past a 1 s deadline, and the usage error. Owner checks run, all green: make testcase-check; make go-fmt-check, rust-fmt-check, go-model-check (19.6 s), typescript-build, package-check (1m7.853s, its cargo, composer and go lines as steps) and php-without-mysql-check (the container's RUN sqlite_without_mysql deadline=2m0s and PASS lines as steps); php tests/codec/check.php; node scripts/features/check.mjs. | [o] |
| T24.6 | Write a duration that rounds up to the next unit in that unit (follows T24.5). Cause: observed in make decimal-physical-check: PASS decimal/postgres/go/2 elapsed=1000ms; the JavaScript, PHP and Rust harnesses chose the unit from the unrounded value, so 999.6 ms printed 1000ms and 999.6 µs 1000µs, while Go prints 1s and 1ms. | Acceptance: every harness writes the Go form for values that round to the next unit, with tracked cases in each. | Evidence: Red before the change: the new duration cases failed with "Expected values to be strictly equal" (JavaScript), "duration 0.0009996: 1000µs, want 1ms" (PHP) and left: "1000µs" (Rust). tests/testcase.mjs, tests/testcase.php and clients/rust/testcase choose the unit from the rounded value; 0.9996 ms is 1ms and 999.6 ms is 1s in all three. Owner checks run, all green: the three form tests (node --test tests/testcase.test.mjs, php tests/testcase_test.php, cargo test -p orm-testcase), cargo clippy -p orm-testcase --all-targets -D warnings, rust-fmt-check, docs-rules-check, checklist-check and git-check. | [o] |
| T25.1 | Give every Go database case that installs a schema or checks an empty database a database of its own (part of T25). Cause: databases() of clients/go/model and the clients/go/orm cases TestAESJSONColumn, TestWithContextCancels, TestWithContextCancelsTransaction, TestWithContextCancelsInsideTransaction, TestMySQLInstallInsideTransaction, TestJsonTextOrderPreservation, TestSQLKeywordNames, TestStyledFixturePhysicalCells, TestConnectionsUseUTC, TestStatementTimeout and TestStatementTimeoutThroughAPooler dropped their tables in the shared databases and installed there, and TestPrimaryAndReplica wrote to the shared database; only the cases of the orm package helper newDatabase (copied into clients/go/decimalmodel) had databases of their own. | Acceptance: as T25 for the Go cases. | Evidence: internal/testdb New(t, driver) creates orm_case_<pid>_<n> through ORM_TEST_MYSQL_DSN or ORM_TEST_POSTGRES_DSN as the admin connection, or the SQLite file orm-case-<pid>-<n>.sqlite in the temporary directory, returns the DSN with only its path replaced, and drops it in t.Cleanup (DROP DATABASE, on PostgreSQL WITH (FORCE)), also after a failure; a failed drop fails the case with the database name. It reports database <name> created and database <name> dropped as STEP lines of the case that the new testcase.Of(t) finds (the case that Start began for t or for its nearest enclosing test; TestReportForm asserts the line), and Retarget names the same database on the replica. PostgreSQL gets a database, not a schema, because Empty() counts every schema other than public as content. The cases above, every former newDatabase case (TestSchemaEmpty, TestInstallAppliesRenderedStatements, the clock, audit, refusal, rollback, runtime value and ormtest TestRollbackFault cases) and decimalmodel TestSchemaSet use it, and the drops of shared tables are gone. TestStatementTimeoutThroughAPooler reaches only the database that the PgBouncer entry orm_test_single is pinned to, so it installs the zone document there under a table name of its own (testdb.Name()) and drops only that table, also after a failure. Cases left on the shared DSNs neither install a schema nor check emptiness: TestCancelledTransactionClearsSessionState, TestTransactionReportsFailedLocalReset, TestTransactionReportsFailedLockRelease, TestPoolSize and TestPoolIdleSizeAndLifetime use session state, named locks and user variables; the featurecoverage cases stay on the bench DSN that feature-check supplies and reads before and after. No Go case had failed on a leftover table: none checked the emptiness of a shared database. With the leftover tables routed and unowned of other runs in both shared databases, go test -v -count=1 of the changed tests passed: ./clients/go/orm 36 cases, ./clients/go/model 9, ./clients/go/decimalmodel 9, -tags ormtest TestRollbackFault 4, and the same counts with ORM_TEST_MYSQL_DSN and ORM_TEST_POSTGRES_DSN set to the ProxySQL and PgBouncer DSNs; afterwards no orm_case_ database or table and no orm-case- file remained. make testcase-check and go-fmt-check passed. | [o] |
| T25.2 | Give every PHP database case that installs a schema or checks an empty database a database of its own (part of T25). Cause: model_test.php (schema empty, database() and every case using it, the replica case), aes_json_test.php, clock_test.php, driver_error_test.php, rollback_test.php and runtime_db_test.php dropped their tables in the shared databases and installed there, and schema empty required that database to have no table at all. | Acceptance: as T25 for the PHP cases. | Evidence: clients/php/tests/case_database.php case_database($driver, $step) creates orm_case_<pid>_<n> through ORM_TEST_MYSQL_DSN or ORM_TEST_POSTGRES_DSN as the admin connection, or the SQLite file orm-case-<pid>-<n>.sqlite in the temporary directory, and returns a CaseDatabase with the DSN (only its path replaced), related() for the same database on the replica and drop() (DROP DATABASE, on PostgreSQL WITH (FORCE); a failed drop throws an error naming the database); with_case_database() drops it after the body also when the body fails, and both report database <name> created and dropped through testcase_step. PostgreSQL gets a database, not a schema, because empty() counts every schema other than public as content. model_test.php drops every case database of a section in modelEnd(); the cases schema empty, statement timeout, install inside a transaction, connection time zone ('', +00:00, UTC), every case of $tests, primary and replica (the replica connection opens the same database after the replica has it), and every case of the other five scripts use it, and schema_set_test.php uses the helper instead of its own copy; the drops of shared tables are gone. statement timeout through a pooler/postgres reaches only the database that the PgBouncer entry orm_test_single is pinned to, so it installs a one-table runtime document under a table name of its own there and drops only that table in finally. Cases left on the shared DSNs neither install a schema nor check emptiness: pool size, the rejected time zones, failed local reset/mysql and failed lock release/mysql; the coverage_*.php scripts stay on the bench DSN that feature-check supplies and reads before and after. Red: before the change, with ORM_TEST_MYSQL_DSN and ORM_TEST_POSTGRES_DSN naming a database orm_t25_red that held one table t25_leftover (made for this run so that the old cases dropped no shared table), model_test.php failed schema empty/mysql on "a database without tables" and schema empty/postgres on "a database without tables" and "the empty schema dropped". Green with the leftover tables routed and unowned of other runs in both shared databases: model_test.php 66 cases, aes_json_test 3, runtime_db_test 9, schema_set_test 9, clock_test 9, driver_error_test 6 and rollback_test 9 passed, and the same through ProxySQL and PgBouncer except rollback_test's rollback_failed/mysql and savepoint_rollback_failed/mysql, which the unchanged script also fails through ProxySQL (T26); afterwards no orm_case_ database or table and no orm-case- file remained; php -l passed on the changed files. | [o] |
| T25.3 | Give every TypeScript database case that installs a schema or checks an empty database a database of its own (part of T25). Cause: model.mjs (schemaEmpty and install(), which checks schema().empty() before it installs, for every case that installs), clock.mjs, dbspec_runtime_db.mjs, driver-error.mjs and rollback.mjs dropped their tables in the shared databases and installed there, so a table left by another run failed a database without tables and schema().empty() before install. | Acceptance: as T25 for the TypeScript cases. | Evidence: clients/typescript/tests/case-database.mjs createCaseDatabase(dialect, step) creates orm_case_<pid>_<n> through ORM_TEST_MYSQL_DSN or ORM_TEST_POSTGRES_DSN as the admin connection, or the SQLite file orm-case-<pid>-<n>.sqlite in the temporary directory, and returns its name, DSN (only the path replaced) and drop() (DROP DATABASE, on PostgreSQL WITH (FORCE); a failed drop throws an error naming the database); withCaseDatabase() drops it in finally, keeping both reasons when the body and the drop fail, and both report database <name> created and dropped as STEP lines of the running case; relatedDsn() names the same database on the replica. PostgreSQL gets a database, not a schema, because empty() counts every schema other than public as content. In model.mjs schemaEmpty, conditions, joinsAndRelations, columnsAndSubqueries, writes, styledStates, transactions, aesRotation, bindLimitSplitting, statementTimeout, cancellation, aesJsonColumn, installInsideTransaction, connectionsUseUtc ('', UTC, +00:00), sqlite/readOnly and primaryAndReplica (the replica connection opens the same database after the replica has it) run in a case database, and every case of the other four scripts and schema-set.mjs uses the helper; the drops of shared tables are gone. postgres/statementTimeoutThroughAPooler reaches only the database that the PgBouncer entry orm_test_single is pinned to, so it installs a one-table runtime document under a table name of its own there and drops only that table in finally. Cases left on the shared DSNs neither install a schema nor check emptiness: poolSize, failedLocalReset and failedLockRelease; the coverage_*.mjs scripts stay on the bench DSN that feature-check supplies and reads before and after. Red: before the change, with ORM_TEST_MYSQL_DSN and ORM_TEST_POSTGRES_DSN naming a database orm_t25_red that held one table t25_leftover (made for this run so that the old cases dropped no shared table), model.mjs passed 25 cases and failed 32: mysql/schemaEmpty and postgres/schemaEmpty on "a database without tables" (PostgreSQL also "the empty schema dropped"), and every installing case on MySQL and PostgreSQL, among them conditions and joinsAndRelations, on "schema().empty() before install". Green with the leftover tables routed and unowned of other runs in both shared databases, after npm run typescript:build: model.mjs 57 cases, node --test dbspec_runtime_db.mjs 24, schema-set.mjs 9, clock.mjs 9, driver-error.mjs 6 and node --conditions=orm-test rollback.mjs 9 passed, and the same through ProxySQL and PgBouncer except rollback.mjs rollback_failed/mysql and savepoint_rollback_failed/mysql, which the unchanged script also fails through ProxySQL (T26); afterwards no orm_case_ database or table and no orm-case- file remained; npm run typescript:check passed. | [o] |
| T25.4 | Give every Rust database case that installs a schema or checks an empty database a database of its own (part of T25). Cause: the integration program (Env::without_tables and databases for schema_empty and every installing case) and the orm tests aes_json, audit, clock, refusal, rollback (also with test-faults), value_types and zone dropped their tables in the shared databases and installed there, so a table left by another run failed schema_empty. | Acceptance: as T25 for the Rust cases. | Evidence: the workspace crate clients/rust/case-database (orm-case-database, a dev-dependency of orm and a dependency of orm-tests) gives CaseDatabase::create(driver), which creates orm_case_<pid>_<n> through ORM_TEST_MYSQL_DSN or ORM_TEST_POSTGRES_DSN as the admin connection, or reserves the SQLite file orm-case-<pid>-<n>.sqlite in the temporary directory, with dsn() (only the path replaced), related_dsn() for the same database on the replica and drop() (DROP DATABASE, on PostgreSQL WITH (FORCE); a failed drop panics with the database name); a Drop guard drops the database on its own thread and runtime when the case panics or passes its deadline, and both report database <name> created and dropped through orm_testcase::step. PostgreSQL gets a database, not a schema, because empty() counts every schema other than public as content. In integration Target::empty, installed and finish own the case database of schema_empty, primary_and_replica (it connects to the same database on the replica after the replica has it), conditions, joins_and_relations, columns_and_subqueries, writes, transactions, aes_rotation, json_values and bind_limit_splitting, and every case of the orm tests above uses it; the table list and the drops of shared tables are gone. statement_timeout_through_a_pooler reaches only the database that the PgBouncer entry orm_test_single is pinned to, so it installs the zone document there under a table name of its own (CaseTable) and drops only that table, also from its Drop guard. Cases left on the shared DSNs neither install a schema nor check emptiness: zone pool_size_bound and postgres_float_round_trip, value_decode, and the orm unit tests of src/tx.rs and src/tx_send_tests.rs, which create and drop their own probe or temporary tables; the coverage_* tests stay on the bench DSN that feature-check supplies and reads before and after. Red: before the change, with ORM_TEST_MYSQL_DSN and ORM_TEST_POSTGRES_DSN naming a database orm_t25_red that held one table t25_leftover (made for this run so that the old cases dropped no shared table), integration schema/bench.dbs --case schema_empty passed schema_empty/sqlite and failed schema_empty/mysql with "mysql: schema().empty() on a database without tables". Green with the leftover tables routed and unowned of other runs in both shared databases: cargo test --locked -p orm --test aes_json 1, audit 1, clock 9, refusal 6, rollback 7 and zone 11 cases, rollback with --features test-faults 10, value_types 1, the release integration program 30 cases (schema_empty on the three databases) and cargo test -p orm-tests --bin integration passed, and the same through ProxySQL and PgBouncer except the four rollback failure cases (T26); afterwards no orm_case_ database or table and no orm-case- file remained. rust-fmt-check and cargo clippy -p orm-case-database -p orm -p orm-tests --all-targets -- -D warnings passed. | [o] |
| T26 | Make the rollback failure cases pass through the poolers. Cause: through ProxySQL, the cases rollback_failed/mysql and savepoint_rollback_failed/mysql of clients/php/tests/rollback_test.php and clients/typescript/tests/rollback.mjs and the MySQL rollback failure cases of clients/rust/orm/tests/rollback.rs end without the error they assert, because the KILL that the case sends through ProxySQL does not end the server session that holds the transaction; through PgBouncer the Rust case's own session connection fails with unsupported startup parameter: extra_float_digits; so make client-pooler-check fails while the Go cases pass. | Acceptance: the rollback failure cases of every client pass in make client-pooler-check with the failure they assert on MySQL and PostgreSQL. | Evidence: ProxySQL takes a text-protocol KILL <id> as a command for its own client sessions: a probe through ORM_TEST_PROXYSQL_DSN got OK for KILL <id> of a backend thread in a transaction and the session stayed alive, while a prepared KILL ? reached the server and ended it, which is why the Go case (raw.Exec("KILL ?", id)) passed. The Rust case opened its session connection with sqlx::PgPool::connect, whose defaults send the startup parameter extra_float_digits, which PgBouncer rejects unless ignore_startup_parameters lists it; the client itself sends none (extra_float_digits(None) in orm::db, since PostgreSQL 12 prints float8 exactly by default), so the client is right for deployments behind a default PgBouncer and the pooler configuration stays unchanged. Change: the make targets export ORM_TEST_MYSQL_SERVER_DSN and ORM_TEST_POSTGRES_SERVER_DSN, the server DSNs of TEST_ENV, in WITH_TEST_ENV, which client-pooler-check does not replace; the rollback failure cases of Go, PHP, TypeScript and Rust open the case database on that server and end the session that holds the lock on its rollback_probe (the case database name marks the transaction behind the pooler; PostgreSQL also filters pg_locks by the current database) with KILL or pg_terminate_backend, and fail naming the variable when it is unset; the Rust session connection is a client connection (Db::connect). The cases still assert the same ROLLBACK errors. Red before the change through ProxySQL and PgBouncer (ORM_TEST_MYSQL_DSN and ORM_TEST_POSTGRES_DSN set to the pooler DSNs): PHP rollback_test.php failed rollback_failed/mysql and savepoint_rollback_failed/mysql on "transaction raises nothing", TypeScript rollback.mjs the same two on "transaction raises undefined", and Rust rollback_failed_mysql and savepoint_rollback_failed_mysql on "transaction: ()" and rollback_failed_postgres and savepoint_rollback_failed_postgres on "unsupported startup parameter: extra_float_digits", with and without test-faults. Green, directly and through ProxySQL and PgBouncer: PHP rollback_test.php 9 cases, TypeScript node --conditions=orm-test rollback.mjs 9, Rust cargo test -p orm --test rollback 7 and with --features test-faults 10, Go TestRollbackFailed*, TestSavepointRollbackFailed* and -tags ormtest TestRollbackFault* 10; without ORM_TEST_MYSQL_SERVER_DSN the Rust MySQL cases fail with "ORM_TEST_MYSQL_SERVER_DSN is required"; no orm_case_ database, rollback_probe table or orm-case- file remained. go-fmt-check, rust-fmt-check, cargo clippy -p orm --features test-faults --test rollback -- -D warnings and php -l passed. | [o] |
| T27 | Make no check run for tens of minutes (follows T24; AGENTS testing rule). Cause: T24 made checks report each case but kept their durations: make feature-check takes about 40 minutes (the full run of 2026-10-03 had 107 cases done after 12 minutes; the first Rust coverage case took 2 m 40 s because each Rust coverage command runs cargo and compiles), and case limits of 30 minutes remain (rustBuildDeadline of the conformance check, coverage case deadlines). | Acceptance: every check finishes within minutes on the development machine with the record of the measured total per check; builds happen once per check before the cases instead of inside cases; repeated runs reuse built binaries; every case limit is derived from what the case does and none is tens of minutes; the totals are recorded in the Evidence. | Evidence: T27.1-T27.6 are complete. One make check on T29.1, started from a clean Rust target, ran 43 steps through scripts/check/run.mjs and all passed in 906 s (15 min 6 s): the longest steps were feature-check 3 min 59 s, client-pooler-check 1 min 54 s, conformance-check 1 min 18 s and client-db-check 1 min 8 s, and every other step took under one minute (before T27 the same checks took about 100 minutes, with feature-check at 34 minutes). | [o] |
| T27.1 | Build the feature coverage binaries once and run each feature command once (part of T27). Cause: each of the 304 coverage runs started the state reader twice with go run, ran its Go test through go test (compile and link) or cargo test once per symbol over every test binary of the crate (the first Rust run compiled for 2 m 40 s), with a 10-minute limit per process (30 minutes per run); the state digest JSON-encoded 100000 bench rows (1.2-1.6 s per read); the verification commands rebuilt the TypeScript client 18 times and ran 13 identical commands twice; and the manifest check failed because the T25 helpers clients/php/tests/case_database.php and clients/typescript/tests/case-database.mjs belonged to no feature. | — | Evidence: coverage.mjs builds the state reader, test2json, one test binary per Go package and the test binaries of each Rust crate once, each as a case with an 8-minute limit, and every run executes those binaries: a Go test through test2json, and the symbols of a Rust entry one at a time in one process per test binary (results are read from stdout) whose dep-info compiles the declared file (a file no binary compiles fails). Each process has the DATABASE limit of 2 minutes. The databases run as concurrent lanes, so a state comparison sees only its own database's runs. The state digest hashes typed values (0.45-0.9 s per read). An identical verification command runs once and its later features report that run. Before: coverage 1103 s and verification 943 s (34 min, red); after: 1020 s (17 min) from an empty Rust target, green. coverage.test.mjs passes its 12 cases. | [o] |
| T27.2 | Build the conformance runners once for the three databases (part of T27). Cause: make conformance-check ran the checker once per database, each run building the Rust and TypeScript runners and starting the Go runner with go run six times, under 30-minute build and 10-minute runner limits, with one global lock /tmp/orm-conformance.lock that fails any other run. | — | Evidence: one run takes -driver/-dsn pairs, builds the Rust runner (8-minute limit), the TypeScript client (2 minutes) and the Go runner binary (3 minutes) once, and runs each runner under 1 minute (measured 1-3 s); a state read has 1 minute (measured under 1 s) and a language case 8 minutes. The lock is per bench database DSN. The Rust runner is a debug build that shares the test dependencies. Before 197 s; after 176 s in make check. | [o] |
| T27.3 | Run make check target by target on databases of its own, with one Rust toolchain and target (part of T27). Cause: make check ran its prerequisites without a report per target and stopped at the first failure; targets used the shared seeded orm_bench and the fixed orm_decimal_case (through a hand-made .runtime/decimal-env); case-database-check compared the server-wide orm_case_ list and the shared orm_test database while the tests of another run were running; the Rust tx tests dropped and created the fixed tables orm_once_probe and orm_timeout_probe in orm_test; Rust ran on 1.98.0 and 1.98.1 side by side, bench/rust and rust-150 used their own targets, and the targets kept incremental data and full debug info (clients/rust/target 10.4 GiB plus bench/rust/target 1.4 GiB after half of the targets, and client-db-check alone added about 10 GB); build limits were 30 minutes and the 2000-table stress limits 15 and 20 minutes. | — | Evidence: scripts/check/run.mjs creates and seeds orm_check_<pid>_<random>_bench and _decimal per run (scripts/check/databases.sh), passes them as TEST_ENV and DECIMAL_ENV, runs each target as a group with its free disk, keeps going after a failure, prints every result and drops the databases. Targets that feature-check already runs are not repeated. case-database-check counts only leftovers of finished processes (its shared database stays the one behind PgBouncer's orm_test_single, so its digest assumes no other run tests on the same server); the tx probes use case databases. One toolchain (RUSTUP_TOOLCHAIN) and one CARGO_TARGET_DIR serve every cargo command without incremental data and with line tables only: after the whole run the one target holds 3.9 GiB and bench/rust/target no longer exists. Build limits are 8 minutes (twice the longest clean build); the stress limits are 12 and 10 minutes from the measured MySQL work (8m10s and 6-7.7 min), which DDL durability settings change by 14% only; the Rust dbspec limits are 5 minutes. Before: the targets took about 100 minutes in total (5985 s, feature-check red, client-pooler-check red); after: one make check run of 42 steps took 1897 s and reported every target (database creation 77 s; client-db-check 449 s, feature-check 570 s in its rerun, conformance-check 176 s, go-test-check 79 s, package-check 63 s, testcase-check 43 s, every other target under 50 s, for example rust-check 31 s instead of 215 s, example-check 29 s instead of 165 s, dbspec-apply-check 37 s instead of 646 s, dbspec-introspect-compare-check 19 s instead of 456 s); with the reruns of feature-check and case-database-check (83 s) the targets total about 2130 s (36 min) instead of about 100 minutes. client-pooler-check fails as before (T29); the first run also found two defects that are corrected and passed in the reruns: the Rust coverage symbols of one entry ran concurrently in one process, and case-database-check gave the PgBouncer case a database the pooler does not reach. | [o] |
| T27.4 | Move the 2000-table and timing-budget cases to make bench (part of T27). Cause: the 2000-table stress document made dbspec-apply-check (646 s, MySQL 8m10s) and dbspec-introspect-compare-check (456 s) the longest checks, the four clients' stress parse budgets ran in their dbspec checks, and the functional runners were release builds compiled apart from the test builds; these are performance measurements. | — | Evidence: make bench runs dbspec-stress-bench (the Go, PHP, TypeScript and Rust stress parse budgets twice each and the PHP introspection stress; Go TestStressDocument has the build tag bench), dbspec-apply-stress-bench, dbspec-introspect-compare-bench (2000 tables, release Rust runner), dbspec-compare-bench (2000 tables) and timing-check with the same assertions. make check runs dbspec-compare-check and dbspec-introspect-compare-check on a 20-table document of the same shape (node tests/dbspec/stress.mjs 20; the default output is unchanged) and builds the dbspec, integration, conformance and example runners in debug. make bench with dbspec-stress-bench (58 s), dbspec-compare-bench (48 s) and timing-check (73 s) passes; dbspec-apply-stress-bench and dbspec-introspect-compare-bench are not run after the change. | [o] |
| T27.5 | Bring feature-check and client-db-check to a few minutes (part of T27). Cause: after T27.1-T27.4, feature-check takes 570 s in make check: coverage 190 s (the Rust test builds 60-90 s and four lanes of runs) and verification 380 s, led by catalog-rust 92 s (the orm-build live-db test build and run, the same command client-db-check runs), generation-go 55 s, schema-compare 30 s and queries-php 20 s; client-db-check takes 449 s: Go 29 s, PHP and TypeScript about 1 minute, then Rust: the workspace test build 97 s and its run about 2 minutes, the test-faults build and run 49 s, the live-db build 57 s and run about 50 s. | — | Evidence: every cargo test command uses --workspace --features $ORM_RUST_TEST_FEATURES (orm/test-faults, orm-build/live-db), so the coverage build, client-db-check, the dbspec Rust checks and the feature commands share one test build, and client-db-check runs one Rust test command instead of three builds; go-test-check skips the packages that client-db-check, feature-check and testcase-check run; client-db-check runs its four clients as parallel lanes (ORM_CLIENT_DB_LANES=parallel; client-pooler-check stays sequential for the pooler limits); feature-check runs its verification commands in ORM_FEATURE_LANES lanes (default 4), and a command marked exclusive in contracts/features.json (generation-go, which regenerates tracked files and runs git diff) runs alone before them. The TypeScript primary-and-replica case opened the replica's case database before the replica had created it (Unknown database) because mysqlConnection(dsn, undefined) takes the default database of the DSN; a connection without a database is now requested with null, the wait asserts SELECT DATABASE() is NULL (before: the wait connection opened orm_case_58817_49), and the primary connection of the wait is closed when the replica connection fails (before: model.mjs did not exit within 60 s with an unreachable replica; after: exit 1 in 10 s). Commit T27.5 was made while client-db-check failed in this case and feature-check failed in generation-go, whose git diff saw the uncommitted Go replica test. Measured on this tree: client-db-check 86 s (449 s before T27.5), feature-check 251 s (570 s before T27.5; 457 s with sequential verification). Known limit: case-database-check compares the shared test database, its PostgreSQL schemas and the server-wide case databases, so another run testing on the same servers at the same time can change what it sees; the shared database is fixed by PgBouncer's orm_test_single, and the case database names carry the test process id, which the check cannot know for Go because go test starts the test binary as a grandchild. | [o] |
| T27.6 | End a stuck JavaScript or PHP case at its limit (part of T27). Cause: runCase and caseTest of tests/testcase.mjs printed FAIL after the limit plus GRACE and returned, so a case stuck while holding a timer or a connection kept its process alive (the Go and Rust harnesses end the process; the stuck case of the harness test held no handle); testcase_run and testcase_begin of tests/testcase.php could not end a stuck case without pcntl (the official PHP image); and the PHP fixture of a case that ignores its deadline exited 0 when the deadline error arrived between two loop iterations. | — | Evidence: runCase prints FAIL <case> elapsed=<elapsed>: deadline <limit> exceeded and the case did not stop after the limit plus GRACE and ends the process with exit code 1. Without pcntl each case and each section starts a watchdog process that prints the same FAIL line and ends the test process with SIGTERM (posix_kill, otherwise the shell's kill) when the case has not ended at the limit plus TESTCASE_GRACE, and that ends when its stdin closes (the case ended or the process ended). testcase.test.mjs runs a case stuck with an interval timer (before: the fixture did not exit within 30 s; after: exit 1 with the FAIL line), testcase_test.php runs a stuck case and a stuck section without pcntl (before: the fixture did not exit within 30 s; after: both end with the FAIL line) and keeps the ignoring loop inside its try (6 of 6 runs pass); make testcase-check passes. | [o] |
| T28 | Release version 0.0.2 (requires every other open item of this branch except T9, and one passing full make check). | Acceptance: every package manifest and version constant of the four clients, the CLI and the contracts states 0.0.2 (Go module tags or version constants, PHP composer, TypeScript package.json and lockfile, Rust Cargo.toml and Cargo.lock of every workspace crate), the CHANGELOG pair moves Unreleased to a 0.0.2 section, and a check fails when any of them disagree. | Evidence: the root VERSION file states 0.0.2; the nine orm Rust manifests (package versions and the =0.0.2 path dependencies on orm and orm-schema), the orm crate entries of the three Cargo.lock files (regenerated offline with cargo metadata), the PHP composer file and the content hash of composer.lock, the TypeScript package and lockfile root, contract_version of contracts/features.json, the README, SECURITY, AGENTS and plan pairs and the PHP README state 0.0.2; ordered-json keeps its own 0.0.1; scripts/build-artifacts.sh and scripts/package-check.sh read VERSION, and scripts/features/check.mjs compares contract_version with it. make version-check (in make check) runs scripts/version/check.test.mjs (3 tests: before scripts/version/check.mjs existed the test failed to load it; the repository agrees, a changed package.json version and a removed contract_version are reported with their files) and scripts/version/check.mjs, which first reported the 12 stale lockfile entries and then 25 declarations agree on 0.0.2; make package-check passes with @polyspec/orm-typescript@0.0.2, composer validate and the Go module at v0.0.2. The CHANGELOG pair moves its unreleased entries under ## 0.0.2. | [o] |
| T29 | Make the Rust tx unit tests pass through ProxySQL. Cause: make client-pooler-check fails in cargo test --workspace with failed_local_reset_is_reported (the reset of a killed connection returns Ok), every_failed_cleanup_step_is_reported (one of two cleanup failures) and dropped_transaction_closes_its_connection (the ProxySQL connection is not TLS); these tests end server sessions and inspect the server connection through ORM_TEST_MYSQL_DSN, which the check points at ProxySQL. | Acceptance: the tests reach the server through the server DSN where they test server behavior, and make client-pooler-check passes. | Evidence: failed_local_reset_is_reported and every_failed_cleanup_step_is_reported connect through ORM_TEST_MYSQL_SERVER_DSN, and dropped_transaction_closes_its_connection through ORM_TEST_MYSQL_SERVER_DSN and ORM_TEST_POSTGRES_SERVER_DSN; all three pass in make client-pooler-check (before: the three failures above). The check then failed in dbspec_apply (postgres.apply.lock: apply under another session's lock: Ok(()), want locked through PgBouncer), which T29.1 covers; with T29.1 make client-pooler-check passes in 120 s. | [o] |
| T29.1 | Stop dbspec apply, recover, rollback and finalize when the connection does not keep one server session (follows T29). Cause: the lock of a command must cover statements that run outside transactions one by one, and a session advisory lock does not survive transaction pooling; through PgBouncer (pool_mode = transaction) apply ran without the lock and no error: an apply whose server connection another client had left holding the lock succeeded, and an apply whose statements moved to another server connection ran every step and failed only at the unlock. | — | Evidence: in Go, PHP, TypeScript and Rust the command reads the server session (MySQL CONNECTION_ID(), PostgreSQL pg_backend_pid()) and whether it already holds the lock before taking it, the lock statement returns the server session, and the statement of every step is preceded by a read of the server session; a shared or changed session is a session error whose message names the requirement (a direct or session-pooled connection), raised before the next statement; docs/spec/plans.md (+ko) state the requirement. Each client's apply check runs both cases through PgBouncer in a database of its own and requires the session error (before: the shared lock applied without error, the moved session ended in the advisory lock of dbspec$plans was not held at unlock); the other apply scenarios and the dbspec probe servers of the Rust tests use ORM_TEST_MYSQL_SERVER_DSN and ORM_TEST_POSTGRES_SERVER_DSN, and the Rust schema-set cases create and drop their databases through them (PgBouncer rejected the raw sqlx extra_float_digits startup parameter). dbspec-apply-check, dbspec-apply-php-check, dbspec-apply-ts-check, dbspec-apply-rust-check, rust-check, rust-fmt-check and make client-pooler-check pass. Commit T29.1 was made without the native symbol snapshots of the new private symbols, so feature-check failed in interface_contract (go: unexpected engine/dbspec/apply.go::applier.checkSession and the same in the other three clients) until contracts/symbols and the hashes of contracts/interfaces.json were recorded in the next commit. | [o] |
| T30 | Let an audit setting select the recorded columns. Cause: the audit triggers copied every column of an audited table into its history table, so a large or sensitive column could not be kept out of the history. | Acceptance: audit into <history> operation <col> action <col> previous <col> [exclude (col, ...) | include (col, ...)] with one list at most; the operation column is always recorded; the history table holds exactly the recorded columns with their names and types besides its identity key, action and previous columns; the triggers copy only the recorded columns on MySQL, PostgreSQL and SQLite; introspection restores the setting from the triggers without a marker; invalid uses are diagnostics with rules; docs/spec/dbspec.md Audit (+ko) states that a value that needs encryption is encrypted by the codec before the write, so the triggers copy ciphertext; shared vectors compared by dbspec-compare-check and physical cases on the three databases in Go, PHP, TypeScript and Rust. | Evidence: Go (engine/dbspec: AuditSetting.Exclude, Include, Records, Excluded, the parser lists and validator.auditLists), PHP (Setting::$exclude, $include, records, excluded, auditLine, Parser::auditListTokens, auditLists), TypeScript (src/dbspec/audit.ts with auditRecords, auditExcluded, auditLine, and the parse lists) and Rust (Setting::Audit { lists }, AuditList, Setting::records, audit_line) parse, check, emit, render and introspect the lists alike. A listed column must be a column of the table and appear once, both lists are rejected at the second keyword, and the operation column is rejected in either list, because it is always recorded and one recorded set has one spelling; each is a setting error at the listed column. The schema text writes the setting with the exclude list of the unrecorded columns in column order, or without a list, because the database holds only the recorded columns: the manifest text keeps the written list and an include setting and its exclude equivalent have one schemaHash; introspection reads the recorded columns from the audit_insert insert and restores that form. The comparison adds drop_audit and add_audit when the recorded set of the columns that both tables have differs, so a new column that a setting does not record stays an add_column (docs/spec/plans.md, +ko). The history table check reports each mismatch at the history table name in all four clients; the PHP and Rust checks had reported one combined error, so their own rule cases setting-audit-shape, immutable-and-audit-on-cascade-child and audit-history-shape now expect one error per mismatch, as Go and TypeScript report. Vectors: tests/dbspec/cases.json audit-exclude, audit-include, audit-list-spacing, nine invalid cases (audit-list-unknown-column, audit-list-repeated-column, audit-both-lists, audit-excludes-operation, audit-includes-operation, audit-history-has-excluded-column, audit-history-differs-from-included-columns, audit-list-without-parentheses, audit-list-trailing-word) and the hashes case audit-list-in-schema-text; tests/dbspec/ddl.json audit-columns with the statements of the three dialects and behavior steps (insert, update and soft delete of card, which excludes secret, and insert and update of tag, which includes label, each recorded in a history table without the other columns, and a refused delete); tests/dbspec/plans.json comparison audit-recorded-columns. Physical: contracts/fixtures/audit_columns.dbs and the coverage case audit_selected_columns of the feature audit_triggers (Go TestCoverageAuditSelectedColumns, clients/php/tests/coverage_audit_columns.php, TypeScript and Rust coverage_audit_selected_columns) write through the client with operations 7 and 8 and require the history columns and rows on MySQL, PostgreSQL and SQLite. Red: with the vectors added and the previous code, Go failed 16 subtests of TestSharedVectors, TestRenderVectors and TestCompareSchemas (unexpected 'exclude', expected the end of the line), TypeScript 17 of 197 dbspec tests, PHP stopped in dbspec_test, dbspec_manifest_test, dbspec_render_test and dbspec_plan_test at the first new case, and Rust shared_dbspec_vectors panicked. Green: dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check (the four runners agree twice), dbspec-ddl-check and dbspec-introspect-check with ddl_audit_columns and schema_audit_columns on the three databases, dbspec-introspect-ts-check, dbspec-introspect-php-check, dbspec-introspect-rust-check, node scripts/features/coverage.mjs --feature audit_triggers (four clients, three databases, two runs each), node scripts/features/check.mjs --run --feature audit_triggers, interface-check with the re-recorded symbol inventories, rust-check, ts-check, go-fmt-check and rust-fmt-check pass. Correction: T30 was committed while make interface-check and the feature-check interface_contract case failed, because rustfmt rewrapped the doc comment of the Rust Setting enum after its symbols were recorded; T30 records the formatted symbols, after which interface-check and make feature-check (17 feature contracts) pass. | [o] |
| T31 | Let every transaction entry point take an operation id. Cause: an audited write needs an operation id, but Rust Db::transaction_once (runs once, no retry, keeps the callback error type) took none: operation(id) existed only on the transaction and transaction_send builders, so every audited write through transaction_once failed with CONFIG. | Acceptance: transaction_once is a builder that takes operation(id) and existing .await call sites stay unchanged; every transaction entry point of the four clients is checked to carry an operation id, with owner cases on MySQL, PostgreSQL and SQLite, docs/operations/usage.md, docs/spec/interface-implementation.md, the interface and symbol records. | Evidence: Rust Db::transaction_once(f) returns the builder TransactionOnce with operation(id); awaiting it runs TransactionOnceFuture (clients/rust/orm/src/tx.rs), a state machine that begins the transaction or the savepoint, runs the callback once in the task-local frame, commits, releases or rolls back, and keeps the previous results: TransactionOnceError::Orm, Callback and Rollback, the rollback fault and a continued panic. The callback bound is FnOnce() -> Fut with Fut: Future<Output = Result<T, E>>, which async || closures satisfy, so existing .await call sites compile unchanged; the steps the ORM awaits are boxed Send futures, so the future is Send when the callback, its future, T and E are. A nested transaction_once with an operation id returns TransactionOnceError::Orm with CONFIG. Go Transaction, PHP transaction and TypeScript transaction, also through the Go WithContext and TypeScript withSignal handles, already take the operation id; they are the only entry points of those clients. Red: cargo test --test audit with transaction_once(...).operation(10) in audit_operation_id failed to compile with "no method named operation found for opaque type impl Future<Output = Result<(), TransactionOnceError<orm::Error>>>". Green: audit_operation_id (clients/rust/orm/tests/audit.rs) passes on SQLite, MySQL and PostgreSQL case databases with transaction_send and transaction_once writes recorded under operations 9 and 10, transaction_once without an id returning Callback with CONFIG and a nested one with an id returning CONFIG; the 33 orm library tests pass, among them the send savepoint cases, which now require the transaction_once future with operation(7) to be Send, and the one-shot and savepoint-end cases; the 10 rollback tests pass. The coverage case audit_operation_entry_points of feature audit_triggers (Go TestCoverageAuditOperationEntryPoints, PHP and TypeScript coverage_audit_triggers, Rust coverage_audit_operation_entry_points) writes through every entry point of its client and checks item_history and the nested CONFIG; it passes in the four clients on MySQL, PostgreSQL and SQLite databases of this run. interface-check passes with the recorded Rust symbols, and the audit_triggers feature check, feature docs, clippy with test-faults, rustfmt, gofmt, docs-check, docs-rules-check, version-check and checklist-check pass. docs/operations/usage.md, docs/spec/interface-implementation.md and the Rust README pairs state the builder. Superseded: T35 replaces operation(id) with .audit(values) on the three builders; the builder form of transaction_once stays. | [o] |
| T31.1 | Find the Rust symbol tool where cargo built it. Cause: buildRust of tests/interfaces/check returned tests/interfaces/rust/target/debug/orm-interface-symbols, but the Makefile gives every cargo command CARGO_TARGET_DIR=clients/rust/target, so cargo built the tool there; in a new worktree go run ./tests/interfaces/check --self-test failed with "fork/exec …/tests/interfaces/rust/target/debug/orm-interface-symbols: no such file or directory", and a checkout that kept an older tool ran that stale build (the main checkout held one from 2026-10-02). | — | Evidence: buildRust builds with --message-format json-render-diagnostics and returns the executable that cargo reports for orm-interface-symbols, or an error when it reports none. TestBuildRustReturnsBuiltExecutable (tests/interfaces/check/rust_tool_test.go) sets CARGO_TARGET_DIR to a temporary directory; before the change it failed with "buildRust = …/tests/interfaces/rust/target/debug/orm-interface-symbols, want the executable cargo built in CARGO_TARGET_DIR …", after it passes, and go run ./tests/interfaces/check --self-test passes in the new worktree; go vet and gofmt pass. | [o] |
| T32 | Restore a soft-deleted row explicitly. Cause: a table with soft_delete keeps the unique key values of soft-deleted rows and an audited table rejects a physical DELETE, so re-adding a removed association (a link row with a unique key) failed with DUPLICATE_KEY, and no call read or restored a soft-deleted row because the planner always adds the soft delete filter. | Acceptance: an explicit restore, in the four clients, of a soft-deleted row found by its primary key or one unique key, with new values of its other columns, clears the soft delete column with an UPDATE (an audit records it with its operation like any update) and returns the restored model; a row that is not deleted and a missing row have a defined result stated in the docs; default reads do not change; shared planner and conformance vectors; owner cases on MySQL, PostgreSQL and SQLite including an audited table with an exclude list. | Evidence: the IR kind restore (engine/ir, PHP Validator, TypeScript and Rust validate) takes a where of eq predicates that names every column of the primary key or of one unique key once and a set of new values checked like an update; the planners (Go restoreStep, PHP Planner::restoreStep, TypeScript restoreStep, Rust restore_step) render UPDATE <table> SET <new values>, <soft delete> = NULL[, <operation> = ?] WHERE <key> AND <soft delete> IS NOT NULL and reject a table without soft_delete, another predicate, a partial or other key and an assignment of a primary key, identity, soft delete or operation column with IR_INVALID. The model method restore (Go Core.Restore and generated Restore, PHP Model::restore, TypeScript Model.restore, Rust orm::model::restore and the orm-build template) takes the primary key when every primary key column has a set value, otherwise the first unique key in name order whose columns all have set values (CONFIG without one), writes every other set value with the restore, then reads the row by the key: a row that is not deleted is returned unchanged with nothing written, a missing row is NO_ROWS, and an audited restore without an operation id is CONFIG. restore is a reserved column name in the four generators and docs/spec/dsl.md. Red: Go TestRestorePlansGuardedUpdate failed with "IR_INVALID: unknown kind "restore"", the Go owner test failed to compile with "Restore undefined (type *orm.Core has no field or method Restore)" and TestColumnNameRules with "restore: got <nil>, want "reserved""; PHP engine_test and TypeScript engine.mjs failed with "IR_INVALID: unknown kind "restore"", the PHP and TypeScript generator tests accepted a column named restore, and the PHP and TypeScript database cases failed with "Restore is not a column of membership"; Rust failed with "IR_INVALID: unknown kind "restore"", "cannot find function restore in module orm::model" and "reserved_method_names_are_not_columns: restore: ()". The change to new values failed first in the four clients with "IR_INVALID: restore takes no set or optimistic" and "restore of membership names every column of its primary key or of one unique key once with an eq value". Green: the shared scenario on contracts/fixtures/restore.dbs (membership with the unique key (team_id, member_id) and an audit that excludes note, label with the unique key (name) and no audit: the re-insert is DUPLICATE_KEY, the default read is NO_ROWS, the restore writes the new note under operation 4 and the history holds insert 1, update 2 deleted and update 4 live, a second restore leaves the row, a missing key is NO_ROWS, a restore without key values is CONFIG) passes as Go TestRestoreSoftDeletedRow, PHP runtime_db_test, TypeScript dbspec_runtime_db and Rust restore_soft_deleted_row on SQLite, MySQL and PostgreSQL case databases, and as the coverage case soft_delete_restore of audit_triggers in the four clients on the three databases twice; the planner coverage cases planner_restore and planner_restore_rejects_non_key (contracts/fixtures/planner.json) pass in the four clients; the conformance vector restore (soft_record by seq with a new name, in a transaction that rolls back) is recorded after go run ./tests/conformance/check run reported 26 vectors × 4 languages identical on MySQL, PostgreSQL and SQLite of a bench database of this run, and soft_record is a declared counter table. interface-check passes with the rule Model.restore, the recorded symbols and the restore sequence; --results on the three outputs passes after the delete_recursive sequence expectation was corrected from false to the recorded NO_ROWS. Feature checks, go-model-check, ts-model-check, go vet, gofmt, rustfmt, the Rust clippy variants, docs-check, docs-rules-check and version-check pass. docs/operations/usage.md, docs/spec/protocol.md, docs/spec/dbspec.md, docs/spec/interfaces.md, docs/spec/interface-implementation.md and docs/spec/dsl.md (+ko) state the call. | [o] |
| T33 | Set the operation id inside a running transaction. Cause: a unit of work whose operation id is the key of a row it inserts first (an unaudited operation row) has the id only after its transaction began, but the four clients took the id only when the transaction began, so its audited writes failed with CONFIG. | Acceptance: in the four clients a call on the running transaction sets the id before the first audited write; an audited write before it is CONFIG; setting a different id, also from a nested transaction, is a defined error stated in the docs; owner cases on MySQL, PostgreSQL and SQLite. | Evidence: Go db.Utils().SetOperation(id) error, PHP $db->utils()->setOperation(int|string $id): void, TypeScript db.utils().setOperation(id: OperationId): void and Rust db.utils().set_operation(id)? set the operation id of the active transaction of the connection. Outside one they fail with CONFIG; an id of another type (Go: not int64 or string, TypeScript: not a safe integer or non-empty string) is CONFIG; the id the transaction already has, from the option or an earlier call, changes nothing, and another id is CONFIG the transaction already has the operation id <old>; it cannot change to <new>, also inside a nested transaction. The id belongs to the transaction: a nested transaction uses it, and a savepoint that rolls back (callback error, Go runtime.Goexit, Go and Rust panic, Rust transaction_once) restores the id it began with, because the rows written under it roll back too; a released savepoint keeps it. Rust keeps the id in a mutex of the transaction and clones it before each statement. Red: Go go vet failed with "db.Utils().SetOperation undefined (type *orm.Utils has no field or method SetOperation)", PHP with "Call to undefined method Orm\Utils::setOperation()", TypeScript with "setOperation outside a transaction" and "TypeError: db.utils(...).setOperation is not a function", Rust with "no method named set_operation found for struct Utils<'a>". Green: the shared scenario (outside a transaction CONFIG; an audited insert before the call CONFIG; id 7 set, the same id again unchanged, id 8 and a nested id 9 CONFIG; a nested transaction that sets 10 and fails leaves the next audited update CONFIG; id 11 then writes; a transaction with the option 12 accepts 12 and rejects 13; the history holds 7, 7, 11 and 12) passes as Go TestSetOperationInTransaction, PHP runtime_db_test, TypeScript dbspec_runtime_db and Rust set_operation_in_transaction on SQLite, MySQL and PostgreSQL case databases, and as the coverage case audit_operation_set_in_transaction of audit_triggers in the four clients on the three databases twice; the Rust case also rolls back a nested transaction_once. The orm library tests with the Send check of transaction_once, the rollback tests, interface-check with setOperation in the Utils methods and the recorded symbols, the feature checks, docs-check, docs-rules-check, gofmt, rustfmt, the Rust clippy variants and version-check pass. docs/operations/usage.md, docs/spec/interfaces.md and docs/spec/dbspec.md (+ko) state the call and its rules. Superseded by T35: an audit record that the transaction inserts from the handle's audit defaults and its values replaces the operation id, so setOperation and the operation id are removed. | [o] |
| T34 | Publish TypeScript declarations without driver types. Cause: @polyspec/orm-typescript 0.0.2 published dist/dbspec/apply.d.ts and introspect.d.ts that imported types from pg (whose types are only a devDependency), mysql2/promise and node:sqlite, so a type check of code that imports the package without skipLibCheck failed (TS7016 with pg installed and no @types/pg, TS2307 without the driver), also with only the MySQL or SQLite driver installed. | Acceptance: the public declarations reference no driver module; the needed connection types are the client's own; a test type-checks code that imports the packed package with skipLibCheck off and no pg types (Red first); mysql2 and node:sqlite are checked the same way. | Evidence: clients/typescript/src/dbspec/connections.ts declares DbspecMySqlConnection, DbspecPostgresConnection and DbspecSqliteConnection (with DbspecMySqlQueryResult and DbspecSqliteStatement) with only the methods that apply and introspection call; the apply connection types are aliases of them, apply runs its MySQL and PostgreSQL statements through the object forms of query, and the runtime pool checks keep their driver imports, which stay out of the declarations. tests/typecheck.ts requires a mysql2 connection, pool connection and pool, a pg client, pool client and pool, and a node:sqlite DatabaseSync to fit the types. clients/typescript/tests/package-declarations.mjs (in npm run typescript:test) rejects a declaration file that imports pg, mysql2 or node:sqlite and packs the package with npm pack, installs it with ordered-json and @types/node but without pg, @types/pg and mysql2, and runs tsc with skipLibCheck: false. Red: "declaration files that import a driver module: dist/dbspec/apply.d.ts, dist/dbspec/introspect.d.ts" and "error TS2307: Cannot find module 'mysql2/promise' …" and "Cannot find module 'pg' …" in apply.d.ts and introspect.d.ts. Green: both cases pass; npm run typescript:check, dbspec-introspect.mjs (63), dbspec-apply-physical.mjs (122) and dbspec-apply-cleanup.mjs (3) pass; package-check, interface-check with the recorded TypeScript symbols, the feature checks, docs-check and docs-rules-check pass. docs/spec/plans.md (+ko) states the types. | [o] |
| T35 | Record an audited unit of work as a row of a declared audit table (replaces the operation id of T31 and T33). Cause: an audited write carried a bare operation id that no table defined, so nothing guaranteed that the id named a recorded unit of work, and the "operation" vocabulary differed from the audit record table that the setting names. | Acceptance: the setting names the audit column and the audit record table, the column is a real restrict foreign key to the record's primary key on the three databases and is restored by introspection; a connection handle carries the audit defaults and a transaction takes the values of its change, and the client inserts one record and writes its key into every audited row it inserts, updates, soft deletes or restores; a nested audit and an audited write without an audit are CONFIG; "operation" leaves dbspec, the docs and the four client APIs; vectors, fixtures and coverage cases in four clients on three databases. | Evidence: audit into <history> column <col> references <table> action <col> previous <col> [exclude (...) | include (...)] replaces the operation form in the four parsers, emitters, renderers, comparisons and trigger introspection (Go engine/dbspec, PHP Dbspec, TypeScript src/dbspec, Rust orm-schema); the validators require a non-null audit column, a record table of the set that is not the audited or history table and is not audited itself, a one-column primary key of the column's type and the declared foreign key (<col>) references <table> (<key>) on delete restrict on update restrict, and the previous column of the audit column's type; introspection takes the record table from the one foreign key of the audit column. tests/dbspec/cases.json, ddl.json and plans.json use the new form with an audit table, the foreign key and its index (behavior steps insert audit rows first and a raw insert with audit_seq 99 fails), and add the invalid vectors audit-record-table-unknown, audit-record-table-is-history, audit-record-key-of-two-columns, audit-column-type-differs, audit-column-without-foreign-key, audit-foreign-key-updates-in-cascade and audit-column-nullable; the fixtures audit.dbs, audit_columns.dbs, restore.dbs and add_tables_and_columns/*.dbs follow. The runtime models carry the audit column and the record entity, the planners bind the slot audit with the record entity, and the executors write the transaction's record key or fail with CONFIG ("a write of an audited table needs an audit: run it in a transaction with audit values", or another record entity). API: Go db.Audit(defaults) *DB and orm.Audit(map[string]any), PHP $db->audit(Model $defaults): Db and transaction(..., ?array $audit = null), TypeScript db.audit(defaults): Db and TransactionOptions.audit, Rust Db::audit(defaults) -> Db and .audit(pairs) on transaction, transaction_send and transaction_once; audit(defaults) returns a handle of the same connection and does not change the connection; the transaction validates the values before it begins (no defaults, or a value that is not a column of the record table, is CONFIG), inserts the defaults with its values (a value wins) in every attempt before the callback, and a nested transaction keeps the outer audit (audit there is CONFIG). The operation id, its options, Go SetOperation, PHP and TypeScript setOperation, Rust set_operation and OperationId are removed. Red: the four dbspec suites failed on the new vectors (Go unexpected 'operation', expected 'column' in the rule cases, PHP "expected operation", TypeScript 34 failures in dbspec-ts-check, Rust "'column' is not allowed here"), the Go owner test did not compile (orm.Operation undefined after the API change, then cannot use auditOf(...) as map[string]any), PHP failed with "Unknown named parameter $audit", TypeScript with "db.audit is not a function", Rust with "no method named audit found for struct orm::Transaction". Green: the owner case auditCase (records [[1 create] [2 default]], history audits 1, 1, 2, a raw insert with audit_seq 999 rejected, a failed callback leaves no record, no defaults and an unknown value CONFIG) passes in Go TestAuditRecordTransaction, PHP runtime_db_test, TypeScript dbspec_runtime_db and Rust audit_record_transaction on SQLite, MySQL and PostgreSQL case databases, with the restore and add-tables-and-columns cases; the coverage cases audit_history, audit_transaction_entry_points, audit_selected_columns and soft_delete_restore pass in the four clients on the three databases twice; dbspec-go-check, dbspec-ddl-check (with the foreign key behavior steps), dbspec-compare-check, dbspec-introspect-compare-check and the PHP, TypeScript and Rust dbspec, introspection, plan and apply checks pass; interface-check passes with audit in the Db methods, the new PHP transaction signature and the recorded symbols; feature checks, docs-check, docs-rules-check, go-model-check, ts-model-check, gofmt, rustfmt, the Rust clippy variants and version-check pass. docs/spec/dbspec.md, usage.md (Audited writes), protocol.md, interfaces.md, interface-implementation.md, dialects.md and plans.md (+ko) and the Rust README pair state the design. Changed in part by T35.2: the audit values come from an audit source of the connection config, and audit(defaults) and its handle are removed. | [o] |
| T35.1 | Let a document set use tables of another set without owning them (part of T35). Cause: a set had to hold every document that its documents use, and every document of a set was equal: the manifest, the rendered statements, install, addTablesAndColumns and code generation covered all of them. A module set whose audited tables reference the audit table of the core set therefore either listed the core document, and then owned audit (installing it after the core set failed with CONFIG because only some tables existed, and the generated code repeated the core models), or omitted it and failed with a use diagnostic. | Acceptance: a set is its owned documents plus external documents given separately; external documents are parsed and validated but never rendered, installed, altered, compared or generated; the manifest hash covers the owned documents and the definitions of the used external tables; install, addTablesAndColumns and connect verify the used external tables and fail with CONFIG; the all-or-none rule, comparisons and plans cover owned tables only; generators take the external documents; shared vectors and owner cases in four clients on three databases. | Evidence: dbspec documents carry an external flag (Go Document.External, PHP $external, TypeScript external, Rust external); an external document that no owned document reaches through use is a use error at its header. The manifest text holds the owned documents and the new external text the used tables of each external document with their columns, primary key and unique keys; manifestHash covers both, so a change of an unused table of the external document keeps it; the schema text holds the owned tables and one use line per external document. Render skips external documents; the schema text check needs no external document; a plan whose target has a use line is a plan error (a plan targets a whole database, and its target uses the external document <doc>: install a set that uses external documents with install or addTablesAndColumns), because plan apply treats the database as one schema, and addTablesAndColumns builds its plan without parsing a plan text. ExternalDifferences (PHP externalDifferences, TypeScript externalDifferences, Rust external_differences) names a missing table, a missing column, another type or nullability, another primary key and a missing unique key; install, addTablesAndColumns and the connect with a generated schema value (Go ConnectSchema, PHP Orm::connectSchema, TypeScript Db.connectSchema, Rust Db::connect_schema) introspect the database only when the set has external documents and fail with CONFIG "the tables that the set uses from external documents differ from the database: ..." before any statement. The runtime models load the manifest text with the external text and have no entity for an external table; the generated schema value carries the external text (Go orm.Schema.External and ExternalText, PHP EXTERNAL_TEXT, TypeScript externalText, Rust Schema::with_external and orm_external.dbs), written only when it is not empty, so existing generated code is unchanged. Generators: Go and PHP orm-gen gen --use <file.dbs>, TypeScript orm-gen gen --use <file.dbs>, Rust Builder::uses. Red: Go go vet failed with "document.External undefined (type *Document has no field or method External)" and "undefined: runtimemodel.LoadFileSet"; PHP with "Undefined property: Orm\Dbspec\Manifest::$externalText", "FAIL plan/parse/target-uses-external-document" and "flag provided but not defined: -use"; TypeScript make dbspec-ts-check with "FAIL plan parse target-uses-external-document" and "FAIL sets external-document-unused", the owner case with "SCHEMA_INVALID ... document ext_core is not in the declared document set" and the generator with "Unknown option '--use'"; Rust with "error[E0609]: no field external on type Document", "error[E0599]: no associated function or constant named with_external found for struct Schema" and "no method named uses found for struct orm_build::Builder". Green: tests/dbspec/cases.json adds the sets external-document-used, external-document-unrelated-table-changed (same manifestHash), external-document-used-table-changed (another hash) and external-document-unused with their texts, hashes and created tables, and tests/dbspec/plans.json the parse case target-uses-external-document; dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check and dbspec-compare-check pass. The owner case on contracts/fixtures/external (Go TestExternalDocuments, PHP and TypeScript external_documents in schema_set_test and schema-set, Rust external_documents) passes on SQLite, MySQL and PostgreSQL case databases: before the core set every call is CONFIG naming ext_account and ext_audit and creates nothing; the member install creates only ext_post and ext_post_history, twice; an audited create records the source's actor in the core's ext_audit; member_v2 adds exactly ext_post.summary and ext_post_history.summary; a drifted external definition is CONFIG naming ext_account.nick. The coverage case schema_install_external_documents of schema_install passes in the four clients on the three bench databases, and the generator tests generate only the owned models. interface-check with the external text fields and the recorded symbols, the feature checks, go-model-check, ts-model-check, the TypeScript type check and declarations, gofmt, rustfmt, the Rust clippy variants, docs-check, docs-rules-check and version-check pass. docs/spec/dbspec.md (External documents), plans.md, schema.md and usage.md (+ko) state the design. | [o] |
| T35.2 | Take the audit values from a source of the connection (part of T35). Cause: T35 put the audit defaults on a connection handle (audit(defaults)), so every request had to build a handle and pass it to the code that writes, and the record table came from the handle's model instead of the audit setting that names it. | Acceptance: audit(defaults) and its handle are removed in the four clients; the connection config takes one audit source per process that returns the audit values of the current request, and a transaction takes the values of its change; the record table is the table that references names; a missing source, a value that is not a column of the record table and a record with no value are CONFIG before the transaction begins, and an error of the source fails the transaction unchanged; owner and coverage cases on MySQL, PostgreSQL and SQLite. | Evidence: Go Config.AuditSource func(ctx context.Context) (map[string]any, error), called with the transaction's context; PHP new Config(..., auditSource: \Closure); TypeScript connect({ ..., auditSource: () => Record<string, unknown> }); Rust Config.audit_source: Option<AuditSource> with AuditSource = Arc<dyn Fn() -> Result<Vec<(String, Param)>> + Send + Sync>; the per-call orm.Audit(map), PHP audit:, TypeScript { audit } and Rust .audit(pairs) stay. Before BEGIN the transaction finds the one record table of the audited tables of the registered sets (none, more than one, one outside the registered sets or one without a one-column primary key is CONFIG), calls the source once, merges the source values with the call's values (a call value wins), and checks the columns and that a value exists; in every attempt it inserts the record before the callback. The runtime models and the planner slot audit carry the record table name, and an audited write checks it against the transaction's record. Red: the updated tests against the previous code failed: Go go vet with "unknown field AuditSource in struct literal of type orm.Config", PHP runtime_db_test with "Unknown named parameter $auditSource" in its 12 cases, TypeScript dbspec_runtime_db with "an audit transaction whose source fails returns the source error" and "CONFIG: the transaction has audit values but the connection has no audit defaults: set them with db.audit(defaults)", Rust with "error[E0425]: cannot find type AuditSource in crate orm". Green: the owner case auditCase (no source CONFIG, a source value that is not a column CONFIG, a failing source returns its error, a call value wins over the source, records [(1, create), (2, default)]) passes in Go TestAuditRecordTransaction, PHP runtime_db_test, TypeScript dbspec_runtime_db and Rust audit_record_transaction on SQLite, MySQL and PostgreSQL case databases, with the restore and add-tables-and-columns cases; the coverage cases audit_history, audit_transaction_entry_points, audit_selected_columns and soft_delete_restore pass in the four clients on the three databases; the Go orm tests, the Rust workspace tests with test-faults and live-db, transaction_once and transaction_send futures that stay Send, interface-check with the Db methods without audit and the recorded symbols, the audit_triggers feature check, dbspec-go-check, dbspec-php-check, dbspec-compare-check, go-model-check, ts-model-check, docs-static-check, docs-rules-check, version-check, gofmt, rustfmt and the Rust clippy variants pass. docs/operations/usage.md (Audited writes), config.md, interfaces.md, interface-implementation.md, protocol.md and dbspec.md (+ko) and the Rust README pair state the source. | [o] |
| T35.3 | Render the audit fixture in the schema definition fixture as T35 declares it. Cause: T35 changed contracts/fixtures/audit.dbs to the audit record table, but the dbspec_render_ddl case of contracts/fixtures/schema_definition.json kept the statements of the old fixture (operation_id, ix_item_operation, no audit table and no foreign key), so the schema_definition coverage of the four clients failed in make feature-check. | — | Evidence: the expected MySQL, PostgreSQL and SQLite statements are the Go dbspec.Render of the current audit.dbs, with the audit table, fk_item_audit and the triggers on audit_seq. Red: make feature-check failed in schema_definition for Go, PHP, Rust and TypeScript; Rust coverage_dbspec_render_ddl with "assertion left == right failed: mysql" whose expected side held operation_id. Green: node scripts/features/coverage.mjs --feature schema_definition runs the coverage of the four clients without an error. | [o] |
| T35.4 | Plan the audit record of a set registered by its schema value in PHP. Cause: PHP built the runtime model of every registered set from its generated classes (Registry::model), so on a connection where a module set was registered by schema value (utils()->schema()->install($schema)) before or without its generated classes, every audit transaction failed with SCHEMA_HASH_MISMATCH: no loaded generated models registered manifest ... in Db::auditRecord; Go, Rust and TypeScript build the runtime model from the manifest text. | Acceptance: in the four clients a set registered by schema value has its runtime model from its manifest text and external text, and the audit record table is resolved from the set that owns it; a Red test of that registration order on MySQL, PostgreSQL and SQLite. | Evidence: PHP Registry::schema(Schema) records the texts of a set registered by value (Db::registerSet calls it); Registry::model uses the generated classes once the set's bootstrap ran and otherwise builds the model once from RuntimeModel::loadSet; Registry::generated drops a model built before. External tables are not entities, so the record table comes only from the owning set. Go (Schema.Model with runtimemodel.LoadSet) and Rust (Schema::manifest with Manifest::load_set, audit_record in tx.rs) already build every registered set from its texts: their external owner cases now also install the module set contracts/fixtures/external/board.dbs by value and pass unchanged. Red: clients/php/tests/audit_external_sets_test.php (core and board registered by value, member generated after them or registered by value before its bootstrap, then an audited create) failed in its six cases with "SCHEMA_HASH_MISMATCH: no loaded generated models registered manifest sha256:0f94ef9772a2f21531b12cbe7e5563ba5a830d4e7ae91d9f2fd4b9a76fc59ffe". Green: the six cases pass on SQLite, MySQL and PostgreSQL with one ext_audit record of the source's actor and its key in ext_post_history; Go TestExternalDocuments and Rust external_documents pass on the three databases; PHP runtime_db_test, schema_set_test, add_tables_and_columns_test (12 each), model_test (66) and dbspec-php-check (556) pass. make owner-check on the change selected schema_install, audit_triggers and interface_contract and passed their six steps. | [o] |
| T35.5 | Check table existence in the Go external owner case without a failing query. Cause: the Go case TestExternalDocuments tested a table by reading SELECT COUNT(*) from it through the native connection; through PgBouncer in pool_mode = transaction the read of the still missing ext_post left the driver's named prepared statement on a server session, so the read after the member install failed with "FATAL: prepared statement name is already in use (SQLSTATE 08P01)" and the case reported that ext_post did not exist. | — | Evidence: the case asks the catalog (information_schema.tables of current_schema(), MySQL information_schema.TABLES of DATABASE(), SQLite sqlite_master), which never fails for a missing table. Red: make check failed in client-pooler-check with "external_documents_test.go:141: ext_post does not exist after the member install" on postgres. Green: make client-pooler-check passes in the four clients, and make owner-check selected schema_install and passed its verification and coverage. | [o] |
| T36 | Select owner checks from the changed files. Cause: owner checks were chosen by hand from the feature whose behavior changed, so T35 and T35.2 were committed while the schema_definition coverage of the four clients failed: contracts/fixtures/schema_definition.json renders contracts/fixtures/audit.dbs, which T35 changed, and no command connected the two. | Acceptance: one command lists and runs every feature whose declared inputs include a changed file, with fixtures that name the changed file, RUN, PASS or FAIL and elapsed time per step and a deadline per step; a changed audit.dbs selects schema_definition; AGENTS.md states how to run it. | Evidence: scripts/features/owners.mjs (make owner-check, PATHS="<paths>" or the files that differ from HEAD and the untracked files) selects the features whose fixtures or tests contain a changed file and those whose declared fixture files name one, prints the paths that selected each, and runs per feature the verification commands (scripts/features/check.mjs --run --feature <id>) and the coverage (scripts/features/coverage.mjs --feature <id>) as steps with a 10-minute deadline that ends the step's process group; --list only prints. scripts/features/owners.test.mjs runs in make feature-check. AGENTS.md and docs/spec/feature-coverage.md (+ko) state the rule. Red: node --test scripts/features/owners.test.mjs against a selection of declared fixtures and tests only failed with "FAIL a changed fixture selects every feature whose fixtures render it elapsed=3ms: selected audit_triggers". Green: the three cases pass (audit.dbs selects schema_definition, named by schema_definition.json, and audit_triggers; a test file and tests/dbspec/cases.json select their features; README.md and docs/checklist.md select nothing). make owner-check on the T37 change selected model_queries and interface_contract and passed their four steps (23 and 2 verification commands, each coverage executed). docs-check and docs-rules-check pass. | [o] |
| T36.1 | Follow the files that named JSON files name when selecting owner checks. Cause: T36 selected a feature only when one of its declared fixtures named the changed file; interface_contract declares contracts/interfaces.json, which names contracts/symbols/rust.json, and that snapshot names every Rust source file, so a change of clients/rust/orm/src/tx_send_tests.rs (T38) selected nothing and its changed symbols failed interface-check only in make check ("rust: changed clients/rust/orm/src/tx_send_tests.rs::state"). | — | Evidence: selectOwners follows each named JSON file and selects the feature for the files it names, with the reason <path> (named by <json file>); a source file of any client now selects interface_contract. Red: the new case "a file named by a file that a fixture names selects the feature" failed with "Expected values to be strictly deep-equal" (no feature selected). Green: the four cases of scripts/features/owners.test.mjs pass, and make owner-check on the T38 change selected interface_contract and passed its verification and coverage. | [o] |
| T37 | Give the internal transaction helpers of the utilities one name and one place in the four clients. Cause: the helper that runs a utility statement in the active transaction of the connection or in a new one was Go Utils.run, Rust Utils::run, PHP UtilsSql::run in a separate internal class and TypeScript inTx, a module function; the helper that reads through the active transaction or the connection was Go Utils.read, Rust Utils::reader, PHP UtilsSql::scalar and TypeScript a module read. One concept had several names and places since the first commit. | Acceptance: in every client Utils owns active (require the active transaction, CONFIG otherwise), run (the active transaction or a new one without retry) and read (the active transaction or the connection); the schema, privilege and AES utilities reach them through the Utils they hold; no behavior change. | Evidence: Go keeps Utils.active, run and read. PHP removes UtilsSql: Utils has the internal active, run, read (was scalar) and exec, and SchemaUtils, PrivilegeUtils and AesUtils take (Db $db, Utils $u). TypeScript Utils has the internal active, run (was inTx), read (was the module read) and db, and the three utilities take u: Utils. Rust renames Utils::reader to read and makes the external table check a Utils method that introspects through read(). The checks that refuse a transaction (MySQL install, MySQL and SQLite addTablesAndColumns) stay, because they are another concept. Red: none, a rename without behavior; the interface check reported the renamed members (TypeScript inTx and read missing, Utils.run and Utils.read unexpected) until the symbols were recorded. Green: PHP runtime_db_test, model_test, add_tables_and_columns_test and schema_set_test (12 each) on SQLite, MySQL and PostgreSQL, aes_json_test, rollback_test and dbspec-php-check pass; TypeScript typescript:test (361), dbspec-ts-check (275), schema-set and add-tables-and-columns (12 each) and package-declarations pass; Rust orm lib tests (35), add_tables_and_columns, external_documents, aes_json and the integration program on the bench schema (30 cases on three databases with privileges and AES rotation), rustfmt and the three clippy variants pass; interface-check passes with the recorded symbols. | [o] |
| T38 | Give the Rust send savepoint test a table that a transaction pooler keeps apart. Cause: tx::send_tests (clients/rust/orm/src/tx_send_tests.rs, from N9.2.1) wrote to CREATE TEMPORARY TABLE orm_send_savepoint_probe and relied on closing its connection to remove it. Through PgBouncer in pool_mode = transaction a temporary table stays on the server session after the client connection closes, and a later run can get that session, so make check failed in client-pooler-check with postgres_send_savepoint "outer commit must succeed: ... duplicate key value violates unique constraint "orm_send_savepoint_probe_pkey" ... Key (id)=(1) already exists" on schema pg_temp_52; the test also passed only while its statements happened to share one server session. | — | Evidence: the test creates a regular table named orm_send_savepoint_probe_<process id>_<nanoseconds> for the run and drops it at its end, so every server session sees the same table and no run meets another's rows. Red: the failure above in make check. Green: make client-pooler-check passes in the four clients with mysql_send_savepoint, postgres_send_savepoint and sqlite_send_savepoint, the three send tests pass directly, rustfmt and clippy of orm with test-faults pass. The test's changed signatures are recorded in contracts/symbols/rust.json, and interface-check passes; make owner-check (with T36.1) selects interface_contract for the test file and passes its verification and coverage. | [o] |
| T39 | Make the generated models independent of how a scan or output path is written (four clients). Cause: the generated models must depend only on the documents and the scanned sources. Rust orm_build::Builder::try_generate joined a relative out_dir to CARGO_MANIFEST_DIR and wrote the manifest path into include_str! as written, so the same sources and documents gave a different generated source for out, an absolute path, ./out or a path through ... The TypeScript, Go and PHP generators had no such dependence: TypeScript scan reads only file contents and resolves the output directory, Go loads package patterns into absolute file names and resolves --out, and PHP has no scan and writes no path. | — | Evidence: Rust canonicalizes the output directory after creating it, so include_str! names one path for one directory. Each client has a case that generates from one fixture, a source in src and one in tests (PHP, which has no scan: a document), with the scan, output and document paths written relative, absolute, with ./ and with a trailing /, and requires byte-equal output that holds the calls of both directories: Go TestGoGenerationScanPathSpelling (generator/gen_go_scan_test.go), TypeScript generate-scan/scan_path_spelling (clients/typescript/tests/generate-scan.mjs), Rust scan_path_spelling_keeps_the_generated_source (clients/rust/orm-build/tests/generate.rs) and PHP gen writes the same models however the paths are written (clients/php/tests/orm_gen_test.php). The model_generation verification now also runs generate-scan.mjs and orm_gen_test.php. docs/operations/usage.md and its Korean pair state the rule. Red: the new Rust case failed with "the generated source of the absolute paths differs from the relative paths": the include_str! path was <CARGO_MANIFEST_DIR>/../../…/out/orm_manifest.dbs for the relative spelling and <dir>/./out/orm_manifest.dbs for ./; the Go, TypeScript and PHP cases passed before the change. Green: make owner-check on the changed paths selects model_generation and interface_contract and passes owners/model_generation/verification, owners/model_generation/coverage, owners/interface_contract/verification and owners/interface_contract/coverage; the four new cases pass in it. The Rust coverage_model_generation_check compares the built model with canonical temporary and OUT_DIR paths. clippy -D warnings of orm-build, go vet ./generator, gofmt, rustfmt and docs-rules-check pass. | [o] |
| T40 | Register a schema set without database I/O and verify the database at install (four clients). Cause: a connection opened per request must have its schema sets registered, and the only public call that registered a set on an open connection was install(schema), which on every call verified the hash, rendered the DDL, introspected the external tables and queried the existence of each owned table inside a transaction (39% of a PHP request, about 30 extra statements on PostgreSQL); the connect helper (connectSchema) also introspected the database for a set with external documents. And install did not prove that the database matched the set: it checked table names and external tables only, never columns, keys, indexes or foreign keys. | — | Evidence: utils().schema().register(schema) (Go Register, PHP register, TypeScript register, Rust register, synchronous in Rust and TypeScript) checks the manifest hash and registers the set without a statement, and connectSchema registers the same way. install introspects the database once, checks the external tables, applies the rendered statements only when no owned table exists (all or none), introspects again and fails with CONFIG the database differs from the document set: <kind> <table>[.<name>]; … when the owned tables differ from the set; addTablesAndColumns ends with the same comparison. The comparison is the new schema operation InstalledDifferences (Go engine/dbspec, PHP Dbspec::installedDifferences, TypeScript installedDifferences, Rust orm_schema::dbspec::installed_differences), which shares the owned-table comparison of addTablesAndColumnsSteps. contracts/interfaces.json declares SchemaUtils.register with the output Registered, a synchronous result, and contracts/validate.go exempts it from the async rule of operations that execute statements. docs/spec/schema.md (new section 6 "Schema registration" with when to call which), docs/spec/protocol.md, docs/spec/dbspec.md, docs/operations/usage.md, docs/spec/interfaces.md and their Korean pairs state the rule: the database is verified at install and upgrade, not per request. Shared vector: contracts/fixtures/install/changed_database.json (case renamed_column on contracts/fixtures/install/note.dbs, one expected message per database). Red: Go TestInstallVerifiesTheDatabase, PHP and TypeScript install_verifies_the_database and Rust install_verifies_the_database failed on SQLite, MySQL and PostgreSQL with install over a changed column = <nil> (Rust Ok(())); the register tests failed to compile or call (Register undefined, Call to undefined method Orm\SchemaUtils::register(), register is not a function, no method named register), and the Go test without the register calls failed with "connect with the schema: CONFIG: the tables that the set uses from external documents differ from the database: table ext_account does not exist; table ext_audit does not exist". Green: the register tests count the statements the connection sends with a driver spy (Go countingDriver through RegisterDriver, PHP StatementCountingPdo, TypeScript wrapped pool methods, Rust a logger of the sqlx::query records that empty() is shown to produce) and get 0 for register, and connectSchema sends what connect sends; the install tests get the expected messages on the three databases. make owner-check on the changed paths selects schema_install and interface_contract and passes owners/schema_install/verification, owners/schema_install/coverage, owners/interface_contract/verification and owners/interface_contract/coverage. | [o] |
| T41 | Make make owner-check run the make targets whose inputs a change touches. The owner check selected only features whose fixtures or tests are a changed file, so a change to a document ran nothing, and the T40 commit left make docs-check red. contracts/check-inputs.json declares, for every target of CHECK_TARGETS, the repository paths it reads (* within a path segment, ** across segments), and scripts/features/owners.mjs runs make <target> for each target whose inputs match a changed path, before the selected features, each with a 30-minute deadline and its RUN, STEP, PASS or FAIL lines. make repo-check and scripts/features/owners.test.mjs fail when a target of CHECK_TARGETS declares no inputs, when an entry names a target outside it, or when a pattern matches no tracked file. AGENTS.md and its Korean pair state the rule, together with searching the tests and scripts for every changed file name before a commit. | — | Evidence: before the change, make owner-check with the changed checklist pair reported select 0 feature(s) and ran nothing; now docs/checklist.md selects checklist-check, version-check, repo-check, docs-rules-check, docs-check and docs-verify-idempotent; scripts/features/owners.test.mjs passes 7/7, including the pattern cases and the declaration check; make owner-check with the changed paths of this item selects seven make targets and passes them. | [o] |
| T41.1 | Give the make targets that make owner-check selects the server environment, the decimal environment and the Rust target directory that owner-check receives. scripts/features/owners.mjs started each target with a plain make <target>, so in a worktree a target read the worktree's .runtime/servers/env and .runtime/decimal-env, which do not exist, and cargo built into the worktree's own clients/rust/target, even when owner-check was given the values of the main checkout. The owner-check recipe now passes ORM_OWNER_TEST_ENV and ORM_OWNER_CARGO_TARGET_DIR, and makeArguments gives each target TEST_ENV=, DECIMAL_ENV= and CARGO_TARGET_DIR= on the make command line, the only way a make variable overrides the Makefile; a worktree check then uses the servers and the target directory of the main checkout. | — | Evidence: in a new worktree of main with no .runtime, make owner-check PATHS=clients/go/orm/cancel_test.go with TEST_ENV, DECIMAL_ENV and CARGO_TARGET_DIR of the main checkout failed 8 of its 12 selected targets, each with <worktree>/.runtime/servers/env is missing; run make test-servers, and left a 2.0 GB clients/rust/target in the worktree; with the change the same command passes all 12 targets and the transactions feature and creates no clients/rust/target in the worktree. scripts/features/owners.test.mjs passes 8/8, including makeArguments. | [o] |
| T42 | Publish an event for every statement a client sends, in Go, PHP, Rust and TypeScript with the same semantics, in place of the OnQuery hook (docs/operations/usage.md "Statement events"). Subscribers registered on the connection (Go DB.Subscribe, PHP Db::subscribe, Rust Db::subscribe with Subscription::unsubscribe, TypeScript Db.subscribe) receive the statement text, its masked binds, its kind by origin (select, insert, update, delete, begin, commit, rollback, savepoint, release, rollback_to, schema, utility), the tables it names, its elapsed time, the number of its transaction on the connection or none, and its error, synchronously after the statement and before the operation continues; every handle of the connection shares them; a subscriber that fails stops the others and fails the operation with the new code SUBSCRIBER whose cause is its error. Every plan step of the four planners carries tables, recorded where the planner renders a table name (contracts/fixtures/planner.json states them, with the new case planner_tables); the renderers give each install statement its table and an add step names the table of its effect. Every client sends the same transaction control statements as its own SQL (MySQL START TRANSACTION, PostgreSQL one BEGIN with its options, SQLite BEGIN IMMEDIATE, the SQLite row-lock table once per connection before its first transaction, COMMIT, ROLLBACK, savepoints), so the Go client no longer uses database/sql transactions and PHP and TypeScript no longer use the driver's transaction calls; a cancelled MySQL or PostgreSQL transaction closes its session and a cancelled SQLite transaction rolls back; the five table privileges are read with one statement in every client. The conformance runners record every statement as its event, with the transaction renumbered within the vector. | — | Evidence: Red: before each client's implementation its vector test failed every case (db.subscribe is not a function, Call to undefined method Orm\Db::subscribe(), Rust no method named subscribe), and the old TypeScript planner failed the planner cases with tables: undefined. Green: the shared vector tests/events/vectors.json (fixture contracts/fixtures/statement_events.dbs, eight cases: model statements, a committed savepoint, a rolled back transaction, a rolled back savepoint, a statement the database rejects with DUPLICATE_KEY, a failing subscriber, unsubscribing, and the statements of install that name a table) passes 8/8 on MySQL, PostgreSQL and SQLite in each of Go (TestStatementEvents*), PHP (statement_events_test.php), Rust (--test statement_events) and TypeScript (statement-events.mjs); the planner cases pass 6/6 in each client; go run ./tests/conformance/check run gave the same output from the four runners for all 26 vectors on each database, the expectations were recorded from them, and their model statements, binds and results equal the previous expectations; the interface check passes for the four languages. Check: the coverage case statement_events (each client's runner of the vector on the database that feature-check selects) passes twice on each database for Go, PHP, Rust and TypeScript; the Go coverage case first reported its eight subtests as tests of their own and now runs them inside TestCoverageStatementEvents, which passes exactly once per run on MySQL, PostgreSQL and SQLite. The broad checks are not run per item; they run in the final full suite. | [o] |
| T7.17.2.10.2 | Locate the physical Markdown block without interpreting fenced examples (requires T7.17.2.10.1). Priority: first executable document parsing step. Cause: searching for annotation text alone mistakes examples for active schema and cannot preserve surrounding source. | — | Evidence: missing scanner Reds are Green in Go/PHP/Rust/TypeScript. physical-envelope-check executes 21 shared range/diagnostic cases, six source/line/block limit and next-value cases, and native encoding checks twice per owner. All source slices preserve exact surrounding/body UTF-8 and CRLF bytes. PHP accepts the 64 MiB source with unchanged 128M limit; measured peak allocation is 71319552 bytes, not RSS. Go vet, Rust 1.98.1 strict scoped Clippy/formatting, TypeScript compilation, PHP syntax and paired-document/diff checks pass on macOS arm64. This is a lexical scanner, not a Markdown AST, joint diagram validator or import capability. | [o] |
| T7.17.2.10.2.1 | Distinguish owned opening fences from indented list examples. Cause: a 0–3-space opening rule recognizes a list-contained example as active schema. | — | Evidence: list-example fails in all four scanners before correction and passes twice after requiring zero indentation for owned openings; foreign fenced examples remain opaque. | [o] |
| T7.17.2.10.2.2 | Correct the new source-block prose rejected by the writing checker. Cause: the new English heading and task text use a prohibited word. | — | Evidence: the same paired-document check passes without changing technical criteria or checker rules. | [o] |
| T7.17.2.10.3 | Parse physical annotations and the diagram together (requires T7.17.2.10.2). Priority: next physical document step. Cause: source ranges alone cannot validate graph metadata, display escaping or contradictory references. | Acceptance: define the full grammar, preserve every graph field and surrounding Markdown, reject contradictions with safe locations, verify real diagram parsing/rendering and the connected stress case in four clients before enabling the capability. Cause: T8 replaces the Mermaid physical document with dbspec, so the annotated Markdown path is not continued. Retry: if T8.1 decides to keep the physical Markdown document. | Evidence: superseded by dbspec: the annotated Markdown physical-document path this item would parse no longer exists; T8.2.6.4 removed it with the Mermaid source, and dbspec documents carry tables and diagrams together (docs/spec/dbspec.md "Diagrams"). | [o] |
| T7.17.2.10.3.4 | Verify document input resource limits through the full parser. Cause: scanner checks did not prove retained document allocations; PHP duplicated surrounding prose and exhausted unchanged 128M at the accepted 64 MiB bound. | — | Evidence: the tracked Red is Green after retaining the original string/ranges through copy-on-write and removing eager prefix/suffix fields. physical-document-limits-check executes byte/line/block bounds and excess (six cases) twice in all four owners, retaining exact source and root-safe resource errors. Limits remain 64 MiB/200000 lines/4096 blocks. PHP peak allocation is 71319552 bytes, not RSS. Related PHP metadata/diagnostic and connected 2000-table/60000-column/10000-FK document round trips pass twice; scoped vet/Clippy/compilation/syntax and paired records pass. Explicit slice/output materialization requires its own memory budget; this proves parsing/retention, not two full edited copies under 128M. Ownership, remaining grammar/output acceptance and import activation stay pending. | [o] |
| T7.17.2.10.3.1 | Verify active physical-document ownership before enabling authoritative imports. Cause: lexical source ranges alone do not establish an authoritative document. | Acceptance: the declared physical grammar identifies its active block, rejects ambiguous/unsupported ownership safely and preserves fenced examples/prose in all four clients. Existing HTML protections are regression coverage, not a requirement to build a general HTML parser. Do not enable imports using source ranges alone. | Evidence: superseded with T7.17.2.10.3: authoritative imports read the dbspec document set, whose documents are named and declared, so no physical-document ownership remains to verify. | [o] |
| T7.17.2.10.3.1.1 | Shield explicitly terminated HTML blocks in the source scanner. Cause: HTML comments, raw elements, processing instructions, declarations and CDATA can contain schema-shaped fences. | — | Evidence: the shared comment case fails in all four old scanners and passes after opaque HTML handling. physical-envelope-check executes all 29 new HTML preservation/diagnostic cases and two 100000-line comment/raw-element stress cases twice per Go/PHP/Rust/TypeScript owner, plus 21 previous source cases, six limit/next-value cases and native encoding checks. Search each terminator once per block; do not allocate a line/body list. Scoped Go vet, Rust 1.98.1 strict Clippy/formatting, PHP syntax, TypeScript compilation, paired-document/checklist and diff checks pass on macOS arm64. Correct the new Korean wording rejected by the writing checker without weakening its rules. Blank-line-terminated HTML, general containers, graph/diagram validation, imports and native/platform proof remain pending. | [o] |
| T7.17.2.10.3.1.2 | Shield named block HTML until a blank line or EOF. Cause: closing a div/table tag does not end Markdown HTML ownership, so the scanner still activates fenced examples in those blocks. | — | Evidence: named-tag examples fail in all four old scanners and pass after bounded ASCII tag matching and an opaque blank-line state. physical-envelope-check executes 62 names in opening/closing forms (124 cases), 23 additional source/diagnostic cases and a 100000-line block case twice per owner, alongside 29 explicit HTML/21 source cases, two existing long HTML cases, six limit/next-value cases and native encoding checks. Preserve bytes; nested starts and closing tags do not switch the ending rule, while raw HTML still requires its explicit end. Scoped Go vet, Rust 1.98.1 strict Clippy/formatting, PHP syntax, TypeScript compilation, paired-document/checklist and diff checks pass on macOS arm64. Arbitrary complete tags with paragraph-sensitive starts, general containers, joint graph/diagram parsing, imports and native/platform acceptance remain unfinished. | [o] |
| T7.17.2.10.3.2 | State the scope of the source scanner. Cause: docs/spec/schema.md described a tag/paragraph cursor that the scanner does not implement. | — | Evidence: docs/spec/schema.md (+ko) states that the scanner does not classify arbitrary HTML tags or full Markdown containers, and that the document reader establishes its own source ownership and validates graph metadata with the diagram before imports are enabled. Paired-document/checklist and diff checks pass; runtime code is unchanged. The next step is joint physical metadata/diagram parsing. | [o] |
| T7.17.2.10.3.3 | Verify reversible display escapes and joint document retention in four clients. Cause: actual Mermaid rejects backslashes in quoted entity aliases; the first display draft fails real parsing. | — | Evidence: four missing parser Reds become Green. Shared cases retain every graph field, CRLF prose, parallel FKs and empty graphs, reject seven mutations at exact safe lines/paths, and verify display reversibility. physical-document-check runs owners twice and Chromium renders hostile labels and parallel FKs twice. All four owners retain the connected 2000-table/60000-column/10000-FK graph with 2000 indices/keys/CHECKs through document emission/parsing/re-emission. PHP peak allocation is 117030912 bytes under unchanged 128M, not RSS. Scoped compilation/lint and paired records pass on macOS arm64. These experimental parsers do not enable authoritative imports: ownership and full resource/grammar acceptance remain in the parent item. No native frame or platform result is claimed. | [o] |
| T7.17.2.10.1 | Identify physical Markdown projection constraints before implementing its grammar (requires T7.17.2.9). Priority: required for lossless annotated documents. Cause: logical Mermaid fields cannot directly express arbitrary physical strings or prove SQL cardinality. | — | Evidence: inspected the current logical parser, physical interchange and physical-import design; checked the official ER syntax and documented quotation, identity, cardinality, contradiction, prose and bounded-source requirements in both schema pages. Existing logical parsing does not supply this capability. Paired-document/checklist and diff checks pass. This records required criteria, not an implemented grammar, renderer proof or import capability. | [o] |
| T7.17.2.9 | Read and emit strict physical graph JSON (requires T7.17.2.8). Priority: necessary input for annotated Markdown. Cause: decoded-value APIs cannot detect duplicate members discarded by a general JSON decoder. | — | Evidence: missing text API Reds in four clients are Green. physical-json-check passes 30 shared syntax/value cases, six number forms, six native decoder byte/depth/node limit/next-value cases, full-record roundtrips and native encoding checks twice per client. Two additional shared output-size cases pass twice per client. Each owner compares every field through the connected 2000-table/60000-column/10000-FK graph with indices/keys/CHECKs and emits a stable 14123977-byte result; related graph/record regressions pass twice. PHP's same stress case passes its unchanged 128M limit with 93290496 peak allocated bytes (89 MiB), not RSS. Go vet, Rust 1.98.1 strict owner Clippy/scoped formatting, Node 26.10.0 TypeScript compilation, PHP syntax and paired-document/diff checks pass on macOS arm64. Markdown, import, DDL, execution and native/platform acceptance remain unfinished requirements. | [o] |
| T7.17.2.9.1 | Decode PHP text directly into the final value tree. Cause: whole-object decoding and fixed first-admission caches exhaust the unchanged 128M limit in the connected stress case; indices preceding tables fill the cache before repetitive column data arrives. | — | Evidence: reproduced exhaustion is Green with direct tree construction and bounded FIFO short-string/small-object sharing. The same full-field text roundtrip passes twice with the original fixture and memory limit; object/list distinctions, escaped duplicates and copy-on-write detachment remain verified. Controlled 256M diagnostic runs were not acceptance evidence. | [o] |
| T7.17.2.9.2 | Keep TypeScript Unicode validation within its declared language target. Cause: isWellFormed has no type declaration in the current ES2022 compilation. | — | Evidence: the reproduced compile failure is Green with direct code-point validation; raw and escaped unpaired-surrogate rejections and owning compilation pass without suppressions or runtime alternatives. | [o] |
| T7.17.2.9.3 | Correct new documentation terms rejected by the writing checker. Cause: the new paired record uses prohibited words. | — | Evidence: the reproduced docs-rules-check failure is Green without changing technical criteria or checker rules; paired IDs/states also pass. | [o] |
| T7.17.2.8 | Resolve indices, keys and CHECKs in the physical graph (follows T7.17.2.4–7). Priority: required for authoritative physical documents. Cause: independent records do not prove ownership, global identity, shared constraint names or backing-index compatibility. | — | Evidence: four old-shape Reds are Green with mandatory arrays and no fallback. physical-graph-check passes 45 new shared record/limit cases and 28 existing graph/five limit cases twice in Go/PHP/Rust/TypeScript, plus related record regressions and detached/sparse cases. Each client retains 2000 tables/60000 columns/10000 FKs with 2000 indices, 2000 keys and 2000 CHECKs under existing deadlines. PHP peak allocation is 72 MiB with the unchanged 128M limit, not RSS. Rust 1.98.1 strict owner Clippy/scoped formatting, Go vet, Node 26.10.0 TypeScript compilation, PHP syntax, paired-document checks and diff checks pass on macOS arm64. SQL grammar, unsupported objects, import, DDL, execution and native/platform acceptance remain separate requirements. | [o] |
| T7.17.2.7 | Preserve immutable physical primary/unique key records (follows T7.17.2.6). Priority: required before complete physical graph assembly. Cause: indices alone could not retain named constraints, ordered key columns, deferred checking or backing-index identity. | — | Evidence: missing-API Reds in all four clients are Green. physical-key-check executes 35 shared exact-field/preservation/rejection vectors and 14 shared count/UTF-8/aggregate limit cases twice per Go/PHP/Rust/TypeScript client, plus detached/reference, invalid encoding and sparse-array checks. Existing column, CHECK, index and FK record regressions pass twice per client. Preserve temporal overlap metadata without pretending dialect/type validation. Rust 1.98.1 strict owner Clippy/scoped formatting, Node 26.10.0 TypeScript compilation, PHP syntax, paired-document rules and diff checks pass on macOS arm64. Graph resolution, physical import, DDL, execution, DB conformance and native/platform acceptance remain separate requirements. | [o] |
| T7.17.2.6 | Preserve immutable physical index records (follows T7.17.2.5). Priority: prerequisite for lossless import and complete graph assembly. Cause: physical records omitted ordered expression/column index terms, included columns, prefixes, sort/null ordering and partial predicates. | — | Evidence: missing-API Reds in all four clients are Green. physical-index-check passes 46 shared field/preservation/rejection vectors and 20 shared count/UTF-8/aggregate limit vectors twice per Go/PHP/Rust/TypeScript client, plus nested alias, sparse-array, encoding and native-number cases. Mixed term order and repeated columns remain unchanged. Existing 24 CHECK vectors/17 bounds and 25 column vectors also pass twice per client. Rust 1.98.1 strict owner Clippy/scoped formatting, Node 26.10.0 TypeScript compilation, PHP syntax, paired-document rules and diff checks pass on macOS arm64. Keep primary/unique constraints distinct from their backing indices. This does not attach graph references, parse SQL, enable imports/DDL/execution or prove DB/platform acceptance. | [o] |
| T7.17.2.5.1 | Correct the new CHECK documentation wording and use the declared dot-separated sub-item ID. Cause: docs-rules-check rejected the new prohibited terms and checklist-check rejected a hyphenated ID. | — | Evidence: both owning checks reproduced the rejections and now pass without changing record meaning or weakening their rules. | [o] |
| T7.17.2.5 | Preserve immutable physical CHECK records (follows T7.17.2.4). Priority: prerequisite for lossless physical import. Cause: the physical graph cannot represent CHECK expressions or enforcement/validation states. | — | Evidence: missing-API Reds in four clients are Green; physical-check-check executes the same 24 exact-field/rejection and 17 byte/count/missing-field cases twice in Go/PHP/Rust/TypeScript, plus encoding and detached-value cases and 25 column regressions per client. Rust 1.98.1 strict owner Clippy/formatting, TypeScript compilation on Node 26.10.0, PHP syntax and paired-record rules pass on macOS arm64. This unit defines a structural record, not graph attachment, SQL validation, import, DDL, DB conformance or execution. | [o] |
| I1 | Define common structure, ownership, and state transitions in interfaces.md and Mermaid diagrams. | — | — | [o] |
| I2 | Regenerate contracts/interfaces.json, the generated component page, and the symbol checks for the model syntax. | — | Evidence: go run ./tests/interfaces/check --generate --record --self-test and make interface-check regenerated and compared the manifest, both component pages, four symbol snapshots, and source mutations; the generated outputs were current and all four languages passed. | [o] |
| I3 | Rewrite the implementation matrix for the model syntax and the in-process planners. | — | Evidence: the English and Korean matrices now identify each language's planner and generated-model owner, owner test location, shared conformance checks, and SQLite datetime scope; make interface-check and the paired-document checks pass. | [o] |
| I4 | Verify connections, transactions, relations, writes, and time zones on physical databases in all four clients. | — | — | [o] |
| I4.1 | Compare all 20 PHP request record declarations with the shared field and nested-type definitions. Source mutations must fail the checker, and the PHP-only declaration check must pass. | — | — | [o] |
| I4.2 | Check every interface error label and each recorded error result against docs/spec/errors.yaml. Unknown or repeated labels and malformed error results must fail. | — | — | [o] |
| D1 | Build the Markdown pages with VitePress and provide implementation status and local search. | — | — | [o] |
| D2 | Render Mermaid diagrams to SVG and verify the no-JavaScript page content. | — | — | [o] |
| D3 | Check /orm/ links, anchors, direct HTML paths, search, mobile navigation, and repeated builds. | — | — | [o] |
| D4 | Deploy the current pages to https://polyspec.github.io/orm/ and verify them. Cause: the static site builds successfully twice and local static checks pass, and the existing Pages URL responds with HTTP 200, but this repository is local-only and the development rules prohibit pushing or publishing the current commit. | — | Evidence: the docs-pages run of G5.24 built, checked and deployed the site; its Pages artifact (github-pages/artifact.tar) holds 179 files, and every one of them, fetched from https://polyspec.github.io/orm/ at the same path, including index.html, dbspec.html, checklist.html and ko/checklist.html, is byte-identical to the artifact (179/179, HTTP 200). The coordinates of the Mermaid diagrams depend on the fonts of the platform that builds them, so builds are compared on one platform (make docs-verify-idempotent) and not across platforms. | [o] |
| M1 | Implement model creation and connect, chains, connectors, groups, operators, value shapes, column comparisons, tuples, and subqueries in the four clients. | — | — | [o] |
| M2 | Implement relations, joins, column selection, order, group, limit, finders, aggregates, and pages. | — | — | [o] |
| M3 | Implement writes: set, setRaw, new<Name>, plus, minus, create, creates, duplication, update, update(true), save, and delete. | — | — | [o] |
| M4 | Implement flow-scoped transactions, savepoints, retry, options, row locks, and connection.utils(). | — | — | [o] |
| M5 | Reject reserved column names and names that collide with generated methods. | — | — | [o] |
| N1 | Port validation, planning, dialects, and DDL to PHP, Rust, and TypeScript; the Go client calls the engine packages directly. | — | — | [o] |
| N2 | Provide one generator per language: ormgen gen --lang go, vendor/bin/orm-gen, the orm-gen npm bin, and the orm-build crate. | — | — | [o] |
| N2.1 | Distinguish Rust Result handling from model calls after fallible styled setters. Priority: required for generated-model controller compilation. | Acceptance: tracked expect/unwrap source cases fail before correction, generation retains the model for later calls without generating Result methods, and unknown model methods still fail. Rust source analysis is client-specific; runtime and other clients remain unchanged. | Evidence: the owner Result-handling case first failed on an invalid expect column method; all ten generation cases and targeted Clippy now pass. A generated controller compiles its styled-setter expect calls and its configuration cases pass within 68 CLI cases. Checklist checks pass; existing document rule findings remain. | [o] |
| N2.1.1 | Distinguish Result transformations after fallible Rust model setters. Priority: before controller compilation. | Acceptance: tracked RED reproduces Result methods being read as columns; preserve model calls after error mapping and extraction, distinguish success mapping from the original model, and reject unknown model calls and Result methods on infallible setters. Owner generation and generated controller cases must pass. Scope: Rust source analysis. Depends on: N2.1. | Evidence: intended generation RED rejects map_err and map as columns; fourteen generation cases and one generated-model case pass, the strict Clippy checks of both parts pass, and paired documentation checks pass. | [o] |
| N3 | Implement utils().schema().install() on MySQL, PostgreSQL, and SQLite in the four clients. | — | — | [o] |
| N3.1 | Load the generated PHP models of several schemas in one process and plan each model request with the engine of its own schema on the connection. Priority: required to use a core schema and module schemas on one connection. Cause: the PHP registry keeps one schema hash for the process, a connection holds one engine, and install() creates tables without registering the manifest. | Acceptance: each generated PHP model class carries its schema hash; a connection starts with the engine of its connect schema; utils().schema().install() verifies the manifest hash against its content and adds its engine to the connection; a request of a schema the connection has not registered fails with SCHEMA_HASH_MISMATCH; one-schema use is unchanged. Owner cases execute the several-schema connection in Go, PHP, Rust, and TypeScript on MySQL, PostgreSQL, and SQLite. | Evidence: the PHP owner case first failed while loading the second generated bootstrap with SCHEMA_HASH_MISMATCH: models of schema ... are loaded together. After the change, several_schemas, unregistered_schema, and edited_manifest pass twice in the PHP and TypeScript owner tests on MySQL, PostgreSQL, and SQLite; Go TestSchemaSet* and Rust schema_set_* pass twice on the three databases with the core and decimal schemas on one connection. PHP unit, model, schema database, SQLite concurrency, grouped-row, and unselected-column checks pass; the PHP interface symbols, feature pages, documentation rules, and static documentation checks pass for the changed symbols and pages. | [o] |
| N3.2 | Make the four clients follow the several-schema rule of docs/spec/protocol.md: a connection plans only the schema sets registered on it. Cause: after the T8.8.1 integration every client planned a request with any set the process loaded: Go engineFor built an engine for the schema of any model on any connection, TypeScript and PHP planned every set whose generated code was imported or bootstrapped, and Rust planned with the manifest the model embeds, so a connection to a database that lacks a set, or holds another version of it, planned the request and failed only at execution or ran it against the wrong tables; Go DB.plan also read its plan cache before it checked the schema, so a request of edited generated code ran from the cached plan of the same hash and shape. Criteria: in the four clients a request of a schema that the connection has not registered fails with SCHEMA_HASH_MISMATCH before execution, also with a cached plan; the generated connect helper and install() add the set to that connection and a raw connection adds none; an edited manifest fails with CONFIG; the cases several_schemas, unregistered_schema and edited_manifest pass in all four clients on MySQL, PostgreSQL and SQLite. Depends on: N3.1. | — | Evidence: rulings and reasons: (1) a raw connection (Go orm.Connect, PHP Orm::connect, Rust Db::connect, TypeScript Db.connect) starts with no set, because nothing has told it what the database holds; the generated code of a set has a connect helper (Go model.Connect, PHP Polyspec\Orm\Tests\Model\connect, Rust model::connect, TypeScript connect) that opens the connection and registers its own set through connectSchema (Go orm.ConnectSchema, PHP Orm::connectSchema, Rust Db::connect_schema, TypeScript Db.connectSchema), one call with nothing to forget and the schema taken from the code that holds it; code with models moves to the helper and raw connect stays for tools; no schema parameter is added to the raw connects. (2) install() and the helper take the generated schema value, the manifest text with its declared manifestHash (Go *orm.Schema, PHP Orm\Schema, Rust &orm::Schema, TypeScript { manifestText, manifestHash }), because the dbspec manifest text carries no hash and only a declared hash lets an edited text fail; a mismatch is CONFIG before any statement, and install registers the set it installs. (3) the schema check runs before the plan cache in Go (DB.plan calls engineFor first, which checks the text of the request's schema and its registration), so an edited manifest never plans from the cache; Rust Db::plan already checked the manifest before its cache and now also checks the registration there, and TypeScript and PHP check the registration before their caches. Red: at T24 the Go cases TestSchemaSetUnregistered* (a model of a set no package registered) and TestSchemaSetEditedManifest* (an edited manifest after the same request was planned) failed 3/3 each on MySQL, PostgreSQL and SQLite with <nil>: the requests planned and ran. With the registration check disabled, the new unregistered_schema cases failed in Rust (DRIVER instead of SCHEMA_HASH_MISMATCH on a raw connection, MySQL, PostgreSQL and SQLite), PHP and TypeScript (bench read on a raw connection, decimal read and write on the bench connection). Green on MySQL, PostgreSQL and SQLite: Go TestSchemaSet*, TestSchemaSetUnregistered*, TestSchemaSetEditedManifest* 9/9; Rust schema_set_* 9/9; PHP clients/php/tests/schema_set_test.php and TypeScript clients/typescript/tests/schema-set.mjs 3 cases × 3 databases each. installed_elsewhere is folded into unregistered_schema: a connection does not use a set that another connection installed. Owner checks of the changed code and callers, all passing: go test -tags ormtest ./clients/go/orm ./clients/go/decimalmodel (112 tests), go test ./clients/go/model ./generator (41 tests), Go featurecoverage TestCoverage* 26 per database on the bench databases; TypeScript tsc --noEmit, tests/engine.mjs, generate.mjs, generate-scan.mjs, dsn.mjs, models-scan.mjs (2), model.mjs, clock.mjs, driver-error.mjs, dbspec_runtime_db.mjs, sqlite-concurrency.mjs and rollback.mjs (--conditions=orm-test) on 3 databases, and the coverage files composite_keys, constraints_and_relations, model_queries, model_writes, parameter_chunking, transactions (2), schema_install (2), audit_triggers, dsn and interface_contract on 3 databases; PHP schema_set, engine, relation_keys, dsn, hostcodec, runtime_model, orm_gen, model (11 × 3), aes_json, clock, driver_error, runtime_db, sqlite_concurrency and rollback tests, the 17 coverage files on 3 databases and make php-without-mysql-check; Rust cargo test -p orm --features test-faults (91 tests), orm-build (18), the integration binary (30 database runs), its 10 coverage cases and the orm coverage_dsn, coverage_audit_triggers, coverage_authenticated_encryption and coverage_interface_contract cases on 3 databases, clippy -D warnings of the workspace and with test-faults and live-db; make example-check (complex and thin-slice), make group-rows-physical-check (12 runs), make unselected-column-physical-check (6 runs), make decimal-physical-check (24 runs), the PHP, TypeScript and Rust conformance runners on the relations vector on 3 databases, go test ./contracts/..., interface-check with the recorded symbols and hashes (Db.connectSchema added, SchemaUtils.install takes GeneratedSchema), docs-rules-check, checklist-check, git-check, go-fmt-check, go-model-check and rust-fmt-check. docs/spec/protocol.md, dbspec.md, schema.md, usage.md, interfaces.md, interface-implementation.md, config.md, the generated component diagram and the PHP and Rust READMEs, with their Korean pairs, state the rule. | [o] |
| N3.3 | Provide utils().schema().register(manifestJson) in the four clients. Priority: required to install a schema once and register it on every later connection. Cause: the only public call that adds a manifest to a connection is install(), which also creates objects; registering a set otherwise needs the internal registerEngine. | Acceptance: register verifies the manifest hash against its content and fails with CONFIG when they differ, executes no statement and creates nothing, and adds the engine of the manifest to the connection exactly as install() does after its statements; install() uses the same registration. The cases register_schema and register_edited_manifest pass in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite. Depends on: N3.1. | Evidence: the PHP and TypeScript cases failed before the change because register did not exist, and the Go and Rust cases did not compile. After it register_schema and register_edited_manifest pass twice in clients/php/tests/schema_set_test.php, clients/typescript/tests/schema-set.mjs, Go TestRegister* and Rust register_* on MySQL, PostgreSQL and SQLite, together with the earlier schema set cases. Before installation a registered connection reads the missing module table with DRIVER, so register creates nothing. PHP and TypeScript reject a module request before register with SCHEMA_HASH_MISMATCH; Go and Rust do not yet keep the schemas of a connection, which N3.2 records, and the Rust register verifies the manifest only. Rust install now also returns CONFIG for an edited manifest. contracts/interfaces.json holds SchemaUtils.register, its native signatures pass contracts/validate.go, and the snapshot hashes match the recorded symbols. | [o] |
| N3.3.1 | Record that this branch provides no utils().schema().register(manifestJson) (follows N3.3). Cause: N3.3 is complete on main, where register adds an installed manifest to a connection, but the T8.8.1 integration replaced it, and this branch has no such call in any client; a record that names it as provided misleads, and a second registration call would give two ways to do one thing. Decision: a set reaches a connection in two ways only. The generated code of a set provides a connect helper that opens the connection and registers its own set on it, and utils().schema().install(schema) creates the tables of the generated schema value and registers that set on the same connection; N3.2 implements both. Reason: using a set on every later connection after one install is covered by the connect helper, which registers the set on each connection it opens from the code that holds the set; a public register() would add an API without a behaviour that orm lacks. | — | Evidence: git grep -n "schema().register|register(manifest|registerEngine" finds the call only in the N3.3 records of docs/checklist.md, docs/checklist.ko.md, CHANGELOG.md and CHANGELOG.ko.md, which describe main, and in the T8.8.1 records; no document under docs/ other than the checklists, no file of contracts/ (interfaces.json and the four symbol records) and no test names it. Checks: docs-rules-check (24 English documents, 24 Korean translations), checklist-check and git-check pass. | [o] |
| N4 | Apply connection time zones on the three databases: PostgreSQL offset zones, instant reads, SQLite clock defaults, and the MySQL named-zone error. | — | — | [o] |
| N5 | Run the conformance checker against the in-process runners and record the vectors again. Record expectations only from four agreeing outputs, preserve the file on rejection, and retain exact TypeScript SQLite integer results. The three database expectation files now use fixed-scale decimal text for write_cycle.price. | — | Evidence: incomplete, conflicting, missing, and undeclared output cases fail; the recording and TypeScript integer cases pass; make conformance-check passes with 25 vectors × four clients on each of MySQL, PostgreSQL, and SQLite, two equal physical executions per client, and unchanged rows and counters. | [o] |
| N5.1 | Run all four clients on MySQL, PostgreSQL, and SQLite, compare every vector with recorded expectations, and confirm repeated execution leaves the observed database state unchanged. | — | Evidence: make conformance-check passes; each database compares 25 vectors across four clients, and the physical result test executes each client twice with equal output and unchanged rows and counters. | [o] |
| N5.1.1 | Require all four outputs, reject stale and changed repeated results, bound runner execution, verify all table rows and declared counters, restore only declared test counters, and test counter observation and cleanup on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N5.1.1.1 | Compare and record JSON numbers without float64 rounding; distinguish integers beyond 2^53 while treating equivalent decimal forms as equal. | — | — | [o] |
| N5.1.1.2 | Reject duplicate keys in nested JSON evidence and expectations, and fail when a database expectation file is missing. | — | — | [o] |
| N5.1.1.3 | Redact database connection strings in runner command logs for both -dsn and --dsn flags. | — | — | [o] |
| N5.1.1.4 | Read SQLite state when no table uses AUTOINCREMENT and sqlite_sequence does not exist. Treat the verified absence as zero counters, still detect row changes and sequence changes when the table exists, and reject other query errors. | — | — | [o] |
| N5.1.1.4.1 | State the SQLite counter rule in both checklist languages: zero counters require a verified missing sqlite_sequence table, and other query errors fail. Verify the documentation wording rule and matching item IDs and states. | — | — | [o] |
| N5.1.2 | Make all four conformance runners propagate unexpected errors, clean write state on failure, and enforce the common aggregate scalar rule on all three databases. | — | Evidence: N5.1.2.9–N5.1.2.11 verify the runner error and write paths, N5.1.2.12 runs all four runners twice on each database with equal output and unchanged rows and counters, and N5.1.2.13 verifies the Rust boolean aggregate result on all three databases. make conformance-result-check passes; the feature inventory includes the central Go runner tests without treating them as client ownership evidence. | [o] |
| N5.1.2.1 | Make Go value conversion, host encoding, and generated model assignment report malformed, null, overflowing, and unsupported values. Verify valid values with executable tests; propagate assignment failures through row assembly and validate insert fields before writing. | — | — | [o] |
| N5.1.2.2 | Reject invalid and nonfinite PHP and TypeScript aggregate scalars and verify nearest binary64 conversion with the shared nine-case fixture. | — | — | [o] |
| N5.1.2.3 | Preserve exact decimal column values in generated models in all four clients. Generated decimal fields accept and return fixed-scale text, including 48.0450, without binary64 conversion; declarations outside precision 1–18 or scale 0–precision fail. SQLite stores scaled signed integers, while MySQL and PostgreSQL store decimal values. Scalar aggregates retain their separate finite binary64 rule. | — | Evidence: shared decimal cases pass in four clients; make decimal-physical-check runs each generated model on MySQL, PostgreSQL, and SQLite twice and verifies all 24 owner results, rollback, and unchanged database rows and counters. Feature inventory names the decimal owner tests; the full feature coverage audit remains tracked under G1. | [o] |
| N5.1.2.3.1 | Remove the redundant as_deref() from the Rust decimal physical assertion without changing its exact-value criterion. Cause: cargo clippy -p orm-tests --all-targets -- -D warnings failed at clients/rust/tests/src/decimal_physical.rs:22 because get_large_value() already returns a borrowed optional string. | — | Evidence: the assertion now compares the borrowed optional string directly; targeted Clippy passed for orm and orm-tests, and the changed Rust physical test passed on MySQL, PostgreSQL, and SQLite with the exact value 9007199254740993 and rollback intact. | [o] |
| N5.1.2.4 | Require an auto column to be a non-null signed i64 primary key in the Go, PHP, Rust, and TypeScript schema builders. Each language executes the same accepted and rejected Mermaid cases and reports the source line for a rejected declaration. | — | — | [o] |
| N5.1.2.4.1 | Read a live SQLite INTEGER PRIMARY KEY AUTOINCREMENT column as a signed i64 automatic key in each schema tool, while preserving a non-automatic INTEGER column as i32. Verify physical SQLite import and schema build in the clients. | — | — | [o] |
| N5.1.2.5 | Replace Go scalar and collection key string fallback with an exact typed key representation. Unsupported values fail, distinct values cannot collide, and relation, split-query, and collection callers propagate errors. | — | Evidence: Go owner RED/GREEN cases cover typed scalar and composite keys, invalid values, byte scan types, relation keys, split-query deduplication, model key callbacks, and collection lookups. The Go client suite and runner unit cases pass; model conditions, joins, relations, columns, and subqueries pass on MySQL, PostgreSQL, and SQLite. Go vet, documentation, rules, and checklist checks pass. | [o] |
| N5.1.2.6 | Distinguish SQL NULL, an encoded null value, and an unselected styled value column in the four clients on all three databases. The completed Go, PHP, TypeScript, and Rust owner cases use the shared 13-case fixture and physical MySQL, PostgreSQL, and SQLite cases to check storage text, getters, row arrays, model JSON output, and errors. The TypeScript output validation case also passes twice on each database. | — | — | [o] |
| N5.1.2.6.1 | Make Go styled values explicit in codecs and generated model setters and getters; verify the shared states, row arrays, model JSON, and errors on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N5.1.2.6.2 | Use StyledValue in PHP styled-column setters, getters, codecs, and model output. Verify the shared state cases and physical model behavior on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N5.1.2.6.3 | Use StyledValue in TypeScript styled-column setters, getters, codecs, and model output. Verify the shared state and codec cases and physical model behavior on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N5.1.2.6.4 | Pass explicit styled values to the Go conformance write runner and return each setter error before writing. Verify model generation, runner compilation, and Go model and runtime tests on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N5.1.2.6.5 | Use StyledValue in Rust styled-column codecs and generated setters and getters. Reject SqlNull on a non-null column at the setter, report an unselected field, and preserve SQL NULL and encoded null through storage, row arrays, and model JSON. | — | Evidence: the shared 13-case fixture and Rust library and generator tests pass (23/23); a generated non-null styled setter rejects SqlNull and accepts encoded JSON null (1/1); orm-tests integration --case json_values passes on MySQL, PostgreSQL and SQLite twice and again after the setter test; workspace Clippy, formatting, English/Korean documentation and checklist checks pass. | [o] |
| N5.1.2.6.5.1 | Update the Rust model JSON output assertion for the explicit StyledValue value wrapper while preserving the ordered JSON member and number text. Cause: the completed styled-column implementation emits {"kind":"value","value":...}, but the existing ordered-text test still expected the former unwrapped document. | — | Evidence: cargo test --offline --locked -p orm --test model_output -- --exact json_output_keeps_the_ordered_json_text passes with both ordered fixtures. | [o] |
| N5.1.2.6.6 | Reject undefined and other omitted members in TypeScript styled-value JSON output before model mutation and when encoding or reading an assigned value. The owner RED cases cover StyledValue.value({missing: undefined}).toJSON(), nested objects, array gaps, hidden and symbol members, unsupported objects, and mutation after assignment. The owner case and TypeScript build pass; model.mjs --case styledStates --dialect passes twice for each of MySQL, PostgreSQL, and SQLite; English/Korean documentation and checklist checks pass. | — | — | [o] |
| N5.1.2.6.6.1 | Check the CODEC_ENCODE code when a styled value contains an undefined member in a TypeScript conformance result. The tracked central result test fails on its former diagnostic wording and passes after checking the code (5/5); the TypeScript styled-value owner case and English/Korean documentation and checklist checks pass. | — | — | [o] |
| N5.1.2.7 | Keep generated Rust model fields private and reject access to a field that was neither selected nor assigned with COLUMN_UNSELECTED. Preserve SQL projection and return grouping values with checked row counts through GroupRows instead of partial models. | — | Evidence: generated model and grouping cases execute on MySQL, PostgreSQL and SQLite, malformed group counts fail, and Rust code compiles against the result types. | [o] |
| N5.1.2.8 | Check all table rows and declared counters after every conformance runner execution, including a failed execution. Report the runner error and any remaining database state change together. SQLite fault cases and MySQL, PostgreSQL, and SQLite physical cases verify failure reporting and cleanup. | — | — | [o] |
| N5.1.2.9 | Make PHP and TypeScript conformance runners reject invalid derived integers and invalid result values, preserve exact ordered JSON numbers, propagate unexpected vector errors, and execute write vectors in transactions. Verify result cases and two identical executions with unchanged rows and counters on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N5.1.2.10 | Make the Go conformance runner reject invalid query binds and missing selected fields, preserve unexpected errors, verify the aggregate binary64 value, and execute write vectors in transactions. Verify its error cases and two identical executions with unchanged rows and counters on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N5.1.2.11 | Make the Rust conformance runner reject malformed bind and derived integer values, reject an aggregate average with different binary64 bits, propagate unexpected vector errors, and roll back a failed write vector. Verify the malformed encrypted prefix with RED and GREEN cases and the failed write state on MySQL, PostgreSQL, and SQLite. N5.1.2 still requires successful four-client comparison. | — | — | [o] |
| N5.1.2.12 | Require the physical result test to execute the exact four-client list twice on MySQL, PostgreSQL, and SQLite, compare the two outputs, and verify all rows and counters return to their prior state. A missing Rust runner must fail before execution. Register and run central Go runner tests in the feature inventory without using them as client owner evidence. | — | Evidence: the missing-runner case fails with three of four clients; after correction, all 12 client/database cases and their repeats pass, and the central Go runner tests pass. | [o] |
| N5.1.2.13 | Decode selected Rust boolean group columns according to their declared type before returning GroupRows. A RED owner case must reproduce integer 0/1 in a boolean group result and reject an invalid boolean, then turn GREEN without changing non-boolean columns. | — | Evidence: Rust library 16/16 and Clippy pass; the aggregates vector matches expectations twice on each of MySQL, PostgreSQL, and SQLite with unchanged rows and counters. | [o] |
| N6 | Remove the compiler service, its message definitions, the WASM and FFI entry points, and the deployment units; update the Makefile and CI. | — | — | [o] |
| N7 | Compare SQLite datetime text given as a string in the stored six-digit form. | — | Evidence: the Go TestConnectionTimeZone, PHP model suite, and TypeScript model suite compare string datetime values with missing, zero, and fractional seconds on SQLite; each also rejects a date-only datetime, and the generated value is read in the six-digit form. The same cases pass on MySQL and PostgreSQL where the clients support string datetime input. | [o] |
| N8 | Run the 150-table Rust compile check with orm-build. | — | Evidence: make rust-150-check generated 150 entities and compiled a Rust crate that called a getter, setter, and chain for every generated model. | [o] |
| N9 | Preserve the callback's error type in a Rust transaction executed once. Verify rollback, commit, nested savepoint behavior, and distinct callback and rollback failures on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| N9.1 | Apply statement_timeout_ms to SQLite lock waits through busy_timeout. A positive connection timeout must return CANCELED at its declared bound while preserving the connection for later work. | — | Evidence: the SQLite owner case and the generated-model case both pass with a 200 ms bound; Rust formatting and Clippy pass. | [o] |
| N9.2 | Provide Db::transaction_send for generated model writes that must cross a Send service callback. Require the callback and its future to be Send, preserve transaction options and retry behavior, and verify the returned future's Send requirement with an owner test. | — | Evidence: the ORM compiles and the requirement test passes. | [o] |
| N10 | Write the client clock with microseconds in the four clients. Cause: the TypeScript client reads its clock from Date, which has millisecond resolution, so a now bind slot stores .mmm000 in a datetime(6) column while the Go, PHP and Rust clients store six real fraction digits. | Acceptance: docs/spec/protocol.md states one rule for the now bind slot: the wall clock with microsecond resolution, truncated to six fraction digits, read once per statement; the TypeScript client reads microseconds from the wall clock and the monotonic clock; the case clock_microseconds passes in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite. Depends on: none. | Evidence: the TypeScript case clock_microseconds failed on SQLite before the change, with every stored value ending in 000; after it the case passes twice in clients/typescript/tests/clock.mjs, clients/php/tests/clock_test.php, Go TestClockMicroseconds* and Rust clock_microseconds_* on MySQL, PostgreSQL and SQLite. The TypeScript model and engine suites, Rust Clippy and formatting, PHP syntax, the feature manifest and the documentation rules pass. | [o] |
| N11 | Report every driver error as an ORM error with a catalog code in the four clients. Cause: the PHP client returns a driver error that the catalog does not list as a raw PDOException, so a write that an orm:audit or orm:immutable trigger refuses has no ORM code; the Go client returns such an error unchanged, the Rust client reports SQLX, a code the catalog does not hold, and the Go, PHP and Rust clients report the SQLite trigger refusal 1811 as FOREIGN_KEY; the PHP, Rust and TypeScript clients do not map a CHECK violation to CONSTRAINT. | Acceptance: docs/spec/errors.yaml holds DRIVER for every driver error that it does not list, with the driver message and the driver error kept as the cause; SQLite 1811 is not FOREIGN_KEY; the cases trigger_refused (DRIVER) and check_refused (CONSTRAINT) pass in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite. Depends on: none. | Evidence: before the change the PHP cases raised PDOException on all three databases, Go reported no code on MySQL and PostgreSQL and FOREIGN_KEY on SQLite, Rust reported SQLX and FOREIGN_KEY, and TypeScript reported DRIVER for the CHECK violation; after it clients/php/tests/driver_error_test.php, clients/typescript/tests/driver-error.mjs, Go Test(Trigger|Check)Refused* and Rust refusal pass twice on MySQL, PostgreSQL and SQLite. The PHP model, schema set and clock suites, the TypeScript model, schema set, SQLite concurrency, engine and DSN suites, the Go client, engine and generator packages and the Rust workspace pass except the cases that need the decimal and coverage environments; Rust Clippy, the symbol records of the changed declarations and the documentation rules pass. | [o] |
| N12 | Type only the method calls of model chains in the TypeScript generator scan. Cause: --scan collects the name of every method call of a source file, so a call such as this.enabled() of a class that is not a model adds the method enabled to every model with a column enabled, and removing that call removes it. | Acceptance: docs/operations/usage.md states which receivers the scan resolves to a model; a call whose receiver does not resolve to a model adds no method; the generator owner cases show that a non-model call adds nothing and that model calls through new, bindings, typed parameters, callbacks, rows and collections add their methods to their own model. Depends on: none. | Evidence: clients/typescript/tests/generate-scan.mjs failed before the change with 14 failures: calls on other receivers declared name, isClose, readCount, aliasGhost and getGhost on models, and ltSeq and neName were declared on every model; after it both cases pass twice. The repository build had also declared newPage, a call of the browser test, on every model. The generator, TypeScript type check, model, schema set, SQLite concurrency, clock, driver error and styled value suites pass; a project with 18 models type-checks with the regenerated models, which only lose declarations of calls on other receivers. The symbol records of the scanner and the generated models, the feature manifest and the documentation rules pass. | [o] |
| N13 | Report both errors when a transaction or savepoint callback fails and its rollback fails too, in the four clients. Cause: the PHP client replaces the callback error with the rollback error, the TypeScript client drops the rollback error, the Go client discards the transaction rollback error and returns only the savepoint rollback error, and the Rust client reports both only as CONFIG text and returns only the savepoint rollback error. | Acceptance: docs/spec/errors.yaml holds ROLLBACK; the error of such a failure has the code ROLLBACK, names both errors in its message, and keeps the callback error and the rollback error; the ORM tests end a transaction on SQLite with a trigger that raises ROLLBACK while the callback runs only model calls, and on MySQL and PostgreSQL by ending the transaction's server session from a test connection; the cases rollback_failed and savepoint_rollback_failed pass in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite. Depends on: N11. | Evidence: before the change PHP raised the raw rollback PDOException on MySQL and PostgreSQL and no ROLLBACK code on SQLite, TypeScript reported the callback error alone and ended the process with an unhandled PostgreSQL error event, Go reported the callback error alone in all six runs, and Rust had no Error::Rollback. After it clients/php/tests/rollback_test.php, clients/typescript/tests/rollback.mjs, Go Test(Savepoint)?RollbackFailed* and Rust rollback pass twice on MySQL, PostgreSQL and SQLite. The Go cancellation case inside a transaction now expects ROLLBACK that keeps the cancellation on MySQL and PostgreSQL. The PHP, TypeScript and Go client suites and the Rust workspace pass except the cases that need the decimal and coverage environments; Rust Clippy, Go vet, the symbol records and hashes, the feature manifest and the documentation rules pass. | [o] |
| N14 | Write the database clock with microseconds on MySQL and keep the fraction of the bound clock in SQLite relative value functions. Cause: the MySQL dialect renders soft deletion as CURRENT_TIMESTAMP and the now value function and its relative forms with NOW(), so a soft deletion stores whole seconds in a datetime(6) column and a condition created_ts <= now() misses a row created earlier in the same second; the SQLite relative forms render datetime(clock, modifier), which drops the fraction of the bound clock. PostgreSQL stores and compares microseconds. | Acceptance: docs/spec/protocol.md states one rule: an ORM write of the database clock to a column uses the declared fraction digits of the column (CURRENT_TIMESTAMP(p) on MySQL) for the update time and soft deletion, the MySQL now value function and its relative forms use NOW(6), and the SQLite relative forms keep the six fraction digits of the bound clock; the cases clock_soft_delete_microseconds and clock_now_condition pass in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite; the conformance expectations hold the new SQL. Depends on: none. | Evidence: before the change clock_soft_delete_microseconds failed on MySQL in all four clients with every stored value ending in .000000, and clock_now_condition failed on MySQL for now() and secondsLater(0) and on SQLite for secondsLater(0); PostgreSQL passed both. After it Go TestClock*, clients/php/tests/clock_test.php, clients/typescript/tests/clock.mjs and Rust clock pass twice on MySQL, PostgreSQL and SQLite, together with clock_microseconds. The Go engine and planner tests, including a soft deletion case for datetime(6) and datetime columns, the PHP and TypeScript engine and model suites, the Rust orm library tests and Clippy, and the documentation rules and build pass. functions is recorded with NOW(6) and the SQLite relative form from four agreeing outputs; MySQL and PostgreSQL ran on separate databases seeded from bench/sql, because the shared orm_bench databases differ from bench/sql on main as well. | [o] |
| N15 | Store the times of the migration ledger with microseconds on every database. Cause: the ledger orm_schema_migrations declares timestamp on MySQL and TEXT written with CURRENT_TIMESTAMP on SQLite, so started_at and finished_at keep whole seconds there, while PostgreSQL keeps microseconds. | Acceptance: docs/spec/protocol.md states one rule: MySQL declares timestamp(6) written with CURRENT_TIMESTAMP(6), PostgreSQL timestamptz written with CURRENT_TIMESTAMP, and SQLite TEXT with a CHECK for six fraction digits that takes the tool clock in UTC; before a command uses an existing ledger, the tool verifies its time columns, and a ledger with other time columns fails with MIGRATION_HISTORY_PRECISION and stays unchanged; docs/operations/usage.md states the statements that convert an earlier MySQL or SQLite ledger; the cases migration_ledger_microseconds, migration_ledger_whole_seconds and migration_ledger_earlier pass in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite. Depends on: none. | Evidence: before the change migration_ledger_microseconds failed on MySQL and SQLite in all four clients with whole-second times and passed on PostgreSQL, and migration_ledger_whole_seconds failed on all three databases in all four clients because the migration ran on the whole-second ledger. After it Go TestMigrationLedger*, clients/php/tests/ledger_test.php, clients/typescript/tests/ledger.mjs and the Rust cli cases migration_ledger_* pass twice on MySQL, PostgreSQL and SQLite, on separate databases. The Go internal/ormgen tests, the full Rust cli tests, the PHP schema database test and the TypeScript schema tool parity test pass. The symbol inventories hold the new symbols of the changed files; the interface check still reports the unrecorded symbols of other work, and Clippy reports one finding in tests/unit/typed_bind_validation.rs, neither of which this item changes. | [o] |
| N16 | Provide a test fault that makes the rollback of a transaction fail on every database, in the four clients. Cause: orm reports a callback error and a failed rollback together, but offered no supported way to make a rollback fail; its own tests end the server session on MySQL and PostgreSQL or install a SQLite trigger written in SQL, which a test without SQL cannot do. | Acceptance: docs/spec/protocol.md §3.1 states the rule: only the test entry point of a client arms the fault, and no DSN, configuration value or environment variable arms it; the Go function FailNextRollback exists only with the build tag ormtest, the Rust module orm::testing only with the feature test-faults, the TypeScript subpath @polyspec/orm-typescript/testing resolves only under the Node condition orm-test, and the PHP class Orm\Testing\Faults is outside the package autoloader. The next rollback of a transaction whose callback failed runs and is then reported with the new catalog code FAULT, so the transaction returns ROLLBACK with the callback error and the FAULT error; a commit does not consume the fault, and the next failing transaction returns its callback error alone; the case rollback_fault passes in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite, and each client checks that its production entry point does not reach the fault. Depends on: N13. | Evidence: before the implementation, with the entry points that only arm the fault, rollback_fault failed in all twelve runs: Go TestRollbackFault* and the PHP, TypeScript and Rust cases reported the callback error alone instead of ROLLBACK on MySQL, PostgreSQL and SQLite. After it Go TestRollbackFault* (build tag ormtest), clients/php/tests/rollback_test.php, clients/typescript/tests/rollback.mjs (condition orm-test) and Rust rollback (feature test-faults, including transaction_send and transaction_once) pass twice on MySQL, PostgreSQL and SQLite, together with rollback_failed and savepoint_rollback_failed; the entry cases TestRollbackFaultEntry, rollback_fault_entry and the PHP and TypeScript entry checks pass. Rust Clippy with test-faults, Go vet with ormtest, the documentation build, idempotence and rules, the checklist, the feature manifest and the interface check of the new symbols pass. The checks that fail on main (feature coverage, Git subjects, the interface inventory of other work, TypeScript codec vectors, schema records, Clippy and formatting of orm-build, the Go decimal and coverage cases, PHP AES JSON and audit service, the TypeScript model suite, conformance results and the performance check) report the same failures with this change. | [o] |
| N17 | Require no PDO driver extension in the PHP client. Cause: clients/php/composer.json required ext-pdo_mysql and only suggested pdo_pgsql and pdo_sqlite, so composer install refused a PHP without pdo_mysql, such as the official PHP image, although the client references Pdo\Mysql only when a mysql:// DSN opens a connection. | Acceptance: composer.json requires none of ext-pdo_mysql, ext-pdo_pgsql and ext-pdo_sqlite and suggests each; the case clients/php/tests/sqlite_without_mysql.php, which scripts/php-without-mysql.sh and make php-without-mysql-check run in the official PHP image php:8.5.10-cli-trixie, checks the manifest and creates and reads a row on SQLite without pdo_mysql. Depends on: none. | Evidence: the case failed with requires ["ext-pdo_mysql"] before the change and passes after it; composer validate, the PHP unit tests and the checklist and document checks pass. | [o] |
| N18 | Connect to MySQL with TLS through one DSN mode in the four clients, and refuse an unknown DSN parameter in the PHP and TypeScript clients. Cause: no client documented a MySQL TLS parameter; the PHP and TypeScript clients ignored every unknown parameter, so ssl-mode=VERIFY_CA&ssl-ca=<file> connected without TLS, and the PHP driver cannot check a server certificate against a CA without also checking the host name, so the one mode that every client can implement is VERIFY_IDENTITY. | Acceptance: docs/spec/config.md states ssl-mode=VERIFY_IDENTITY with an absolute ssl-ca and a host name as the one shared MySQL TLS mode, with TLS that checks the CA and the host name and no RSA password exchange; the Go, PHP and TypeScript clients return CONFIG for another ssl-mode, a missing or relative ssl-ca, ssl-ca without ssl-mode, socket with ssl-mode and an IP address host; the PHP and TypeScript clients return CONFIG for a parameter outside the documented set of each scheme; the Rust client keeps passing its parameters to sqlx and is tested with VERIFY_IDENTITY; the TLS cases of the four clients connect to the TLS server of make test-servers and refuse a certificate of another CA and one that names another host. Depends on: none. | Evidence: scripts/test-servers.sh tls issues a test CA, a certificate of the primary that names localhost, a certificate of the replica that names orm-mismatch.invalid, signed by the same CA, and a second CA, loads them with ALTER INSTANCE RELOAD TLS and writes ORM_TEST_MYSQL_TLS_DSN, ORM_TEST_MYSQL_TLS_OTHER_CA_DSN and ORM_TEST_MYSQL_TLS_MISMATCH_DSN with the host localhost., which resolves over TCP. RED: clients/typescript/tests/mysql_tls.mjs and clients/php/tests/mysql_tls.php failed with every refused DSN accepted, an empty Ssl_version and both wrong servers accepted; clients/go/orm/mysql_tls_test.go failed to compile without the TLS fields of the parsed DSN. GREEN: the three pass, with TLSv1.3 on the primary and refusals of both wrong servers; clients/rust/orm/tests/mysql_tls.rs passed at once (2 of 2), because sqlx implements ssl-mode itself. The PHP DSN parsing case, the TypeScript database suite, the TypeScript DSN, engine, generator and schema tool cases, the PHP schema, ledger, clock, driver error and rollback cases and the DSN coverage cases of Go, PHP and TypeScript on MySQL, PostgreSQL and SQLite pass; the TypeScript codec vector case and the PHP AES JSON, audit service and empty schema cases fail on ORM main in the same way and do not read the DSN parameters. Follow-up: the clients differ in the other parameters that they accept: the Go client passes them to go-sql-driver/mysql and pgx, and the Rust client to sqlx, which also accepts verify_ca; refusing them in Go and Rust as in PHP and TypeScript would also refuse a MySQL DSN that gives ssl-mode=verify_ca to the Rust client. | [o] |
| N19 | Add the missing nullable or defaulted columns of the existing tables of a manifest through the connection, in the four clients. Priority: required when an installation adds a column to a table that a live database already holds. Cause: utils().schema().install(manifestJson) creates only missing tables and keeps existing ones, and the migration tools plan one manifest against the whole live database through their own connection, a ledger and a log directory, so for the manifest of one module they plan the removal of every other table; the import also dropped a foreign key of a table to its own key, so such a table never matched its manifest. | Acceptance: docs/spec/schema.md §8 states utils().schema().addColumns(manifestJson) (Go AddColumns, Rust add_columns): it adds every missing column of an existing table of the manifest that is nullable or has a default, replaces the audit triggers of each changed table so that they record the new columns, returns the added columns as table.column, leaves the missing tables and the tables of other manifests unchanged, and returns the new catalog code SCHEMA_DIFFERS before any change for every other difference (a required missing column, a removed or changed column, a key or an index); a repeated call adds nothing; on MySQL a call inside a transaction returns CONFIG; §6 states that a foreign key of a table to its own key is imported as a relation of the table to itself; the cases add_columns, add_columns_differs, add_columns_transaction and add_columns_edited_manifest pass in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite with the fixtures contracts/fixtures/add_columns_*. Depends on: none. | Evidence: RED: clients/php/tests/add_columns_test.php and clients/typescript/tests/add-columns.mjs failed in all twelve runs because the call did not exist, and clients/go/orm/add_columns_test.go and clients/rust/orm/tests/add_columns.rs did not compile without it. With the call and the old import, the case add_columns, whose addcol_tag has a key to itself, failed with SCHEMA_DIFFERS in the four clients on the three databases: SQLite planned a rebuild of addcol_tag, and MySQL and PostgreSQL planned ADD CONSTRAINT fk_addcol_tag_parent_addcol_tag_seq. The first PHP run also showed that the import reads a MySQL decimal default with the digits of its scale and a PostgreSQL string default with its quotes; the call compares these defaults by value. GREEN: the four cases pass twice in PHP, TypeScript, Go TestAddColumns* and Rust add_columns* on MySQL, PostgreSQL and SQLite: the rows are kept, the new columns read NULL or their default, an audited update records {"note":"later","rank":4}, the log tables are unchanged, the missing table stays absent until install, and each of seven other differences returns SCHEMA_DIFFERS with no column added. The Rust catalog reads and the live import moved from orm-build to orm_schema::catalog and orm_schema::live, so the runtime and the tool read the catalog through one Catalog trait. The PHP schema, schema tool, schema database, ledger and schema set cases, the TypeScript schema tool, schema case, SQLite import, ledger, engine, schema set, generator, DSN and driver error cases, the Go internal/ormgen, engine and client packages, the Rust workspace and orm-build --features cli tests, the TypeScript type check, Rust Clippy of the workspace and of test-faults, and the documentation records pass, except the cases that need the decimal, coverage and send-savepoint environments, the timing bound of physical_graph_vectors, the TypeScript model suite, whose empty-database cases find tables of other tests in the shared databases, and the formatting and cli Clippy findings of orm-build, which fail on ORM main in the same way. | [o] |
| N19.1 | Compare the PostgreSQL column types and the bounded index names of the existing tables by their stored form in addColumns, in the four clients. Cause: PostgreSQL stores char(n) as varchar(n) and every blob type as bytea, and stores an index name longer than 63 bytes cut with a digest, so the import reads another raw type and another index name than the manifest declares; addColumns returned SCHEMA_DIFFERS for installed core and board manifests that hold a longblob, a char(64) and long index names. | Acceptance: docs/spec/schema.md §8 states that a column whose type the dialect stores as the declared type has the declared type and that an index whose physical name is the physical name of a declared index has the declared name; the fixtures contracts/fixtures/add_columns_* hold a longblob column, a char(8) column and an index whose PostgreSQL name is longer than 63 bytes, and the N19 cases pass with them in Go, PHP, Rust and TypeScript on MySQL, PostgreSQL and SQLite. | Evidence: RED: with the new fixtures the four cases failed on PostgreSQL in the four clients with SCHEMA_DIFFERS: addcol_item.payload changed from raw=blob to raw=longblob; with the type comparison and without the index comparison PHP add_columns failed on PostgreSQL with DROP INDEX "addcol_item_ix_addcol_item_enabled_and_created_at__b10618c7774a" and CREATE INDEX "addcol_item_ix_addcol_item_enabled_and_created_at_stored_together". GREEN: the four cases pass twice in PHP, TypeScript, Go TestAddColumns* and Rust add_columns* on MySQL, PostgreSQL and SQLite, and a plan for installed core, member, page and board manifests adds nothing and finds no difference on MySQL, PostgreSQL and SQLite. | [o] |
| T7.17.2.4 | Resolve bounded immutable physical graphs in four clients (requires T7.17.2.1–3). Cause: independently valid records do not prove reference membership or uniqueness. | — | Evidence: missing APIs in four clients, PHP 128 MiB exhaustion and Go native integer version rejection are Red-to-Green. The reusable physical-graph-check passes 28 common graph and five count/byte-limit vectors twice per client, plus all 25 column and 26 FK regressions. Exact shapes, global IDs/names, ordered owner resolution, cycles/self/shared references, located value-free errors and detached snapshots are verified. Each client retains the connected 2000-table/60000-column/10000-FK case. PHP process allocation peak is 62 MiB with the unchanged 128M limit; this is not RSS or another client's memory measurement. Owner Rust 1.98.1 Clippy, TypeScript compilation on declared Node 26.10.0, paired records and diff checks pass on macOS arm64. This does not complete physical import, full physical schemas, DDL or DB conformance. | [o] |
| T7.17.2.3 | Preserve physical foreign keys and share bounded record validation (requires T7.17.2.1/T7.17.2.2). Cause: logical relationships lose independent FK names, paired-column order, update/delete actions and deferral. | — | Evidence: four missing-API Reds are Green. The reusable physical-fk-check passes all 26 FK and 25 existing column vectors twice per Go/PHP/Rust/TypeScript client, plus sparse/alias and 64/65-column checks. Preserve independent names, ordered pairs, self/shared references, distinct actions, unspecified states and exact options; reject contradictions safely. Shared record modules replace column-specific validation, including Rust RecordError without the obsolete ColumnError path. Rust 1.98.1 owner Clippy, declared Node 26.10.0 TypeScript compilation, PHP syntax, paired checklist, documentation rules and diff checks pass on macOS arm64. Each client creates/retains 2000 FK records twice; observed second-run times are Go 7.36 ms, PHP 9.50 ms, Rust debug 22.95 ms and TypeScript 12.27 ms. These are record-retention measurements, not connected graph, archive, memory, frame, database or other-platform acceptance. Graph reference resolution, imports, DDL and execution remain separate requirements. | [o] |
| T7.17.2.2 | Preserve immutable physical column records in four clients (requires T7.17.2.1). Cause: logical columns cannot carry exact native type/default/generation/options without loss. | — | Evidence: missing API Reds in all four clients, TypeScript type-narrowing compile failure and PHP reference-alias runtime Red are Green. The exposed physical-column-check passes all 25 shared exact-field/rejection vectors twice in Go/PHP/Rust/TypeScript, plus aggregate/field/option limits, invalid encodings, sparse options and detached input/output checks. TypeScript compilation using declared Node 26.10.0, owner Rust Clippy, PHP syntax, paired checklist, documentation rules and diff checks pass. Preserve stable IDs, raw SQL, absent/NULL/literal/expression distinctions, identity/computed generation and ordered options. This is structural interchange, not text parsing, SQL grammar validation, physical import, database conformance or execution authority. | [o] |
| T7.17.2.1 | Preserve qualified physical identities without logical normalization. Cause: logical identifier rules cannot represent arbitrary physical names. | — | Evidence: missing API Reds in all four clients and a TypeScript sparse-array rejection Red are Green. The tracked physical-identity-check runs all ten shared vectors twice per Go/PHP/Rust/TypeScript client with identical exact keys/rejections, plus immutable component, invalid encoding and UTF-8 byte-limit checks. TypeScript compilation, owner Rust Clippy, PHP syntax, paired checklist, documentation rules and diff checks pass. Preserve optional catalog/schema/column, required table, exact case/UTF-8 and unambiguous name keys without normalization. This primitive does not enable physical import, annotation parsing, DDL, migrations or database-dependent conformance. | [o] |
| N9.2.1 | Remove the existing send-savepoint borrowed-Box warning. Cause: strict Clippy rejects savepoint_send in orm/src/tx.rs. | — | Evidence: borrowed-Box lint and the tracked directly borrowed callback compile case fail before correction and pass after accepting &SendOperation with as_ref() at the caller. The reusable rust-send-savepoint-check runs Rust 1.98.1 strict library Clippy and all three owning MySQL/PostgreSQL/SQLite tests twice. Verify Send futures, direct callback borrowing, nested commit/rollback, original callback errors, restored transaction frames, outer rollback of released savepoints, exact retained fixture rows and connection reuse. Each test has its own 10-second deadline. Fixtures use connection-local temporary tables, never user tables; the shared Make environment declares the SQLite URI for existing test entry points. Owner library/tests Clippy, scoped formatting, paired records and diff checks pass on macOS arm64. This is a Rust implementation correction, not new transaction semantics or four-client DB conformance. | [o] |
| L1 | Build schema.json from .mmd files in every language. | — | Evidence: make schema-cross-language-check builds the same Mermaid source with Go, PHP, Rust, and TypeScript, compares the complete JSON manifests, and reports four equal outputs with 12 entities and schema hash 16198b563e2e3cae. | [o] |
| L2 | Provide migration and import tools in every language. | — | Evidence: Go SQLite import tests normalize DECIMALINT(13,4) and DECIMALINT(16,0) to logical decimal types and build the manifest; PHP schema tools pass migration and import on SQLite, MySQL and PostgreSQL; Rust CLI import and TypeScript SQLite import tests preserve decimal precision and scale. The four SQLite import paths use the same logical mapping and reject no input silently. | [o] |
| T7.3 | Implement deterministic ormgen diff and destructive-change checks. | — | — | [o] |
| T7.5 | Implement YAML 1.2 and point conversions in Go, PHP, Rust, and TypeScript. Verify point DDL and SQL on MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| T7.9 | Compare Rust mysql_async 0.37.1 with sqlx 0.9 using equal SQL, binds, typed results, connection count, and fixture. Retain sqlx because neither measured workload shows the required 2x improvement. | — | — | [o] |
| T7.10 | Exclude multi_statement from every public API because the supported databases cannot provide the same safe parameterized execution structure. G4 owns the executable exclusion check for all four clients. | — | — | [o] |
| T7.13 | Validate AES version columns, persist the current version on writes, and provide status and transactional rotation APIs in Go, PHP, Rust, and TypeScript. | — | — | [o] |
| T7.14 | Include table and column comments in the manifest, schema hash, import, DDL, diff, and SQLite metadata. | — | — | [o] |
| T7.15 | Add migration execution locking, transaction boundaries, and detailed recovery states for MySQL, PostgreSQL, and SQLite. | — | — | [o] |
| T7.16 | Replace semicolon splitting with a dialect-aware SQL statement parser and preserve statement-level failure locations. | — | — | [o] |
| T7.17 | Accept MMD, manifest JSON, metadata-bearing ORM SQL, and live DB schema sources for DDL, diff, structured plans, verification, recovery, idempotent migration, and verified rollback. | — | — | [o] |
| T7.17.1 | Expose Rust DSN-only catalog connections through the public library and make the CLI use the same implementation. | — | Evidence: missing module compile Red corrected; missing SQLite file creation Red corrected. Four public API owner tests pass, including actual MySQL/PostgreSQL repeated catalog reads and SQLite FK/content preservation; existing SQLite CLI automatic-rowid import regression passes. This extracts existing behavior, not lossless physical import or arbitrary data decoding. | [o] |
| T7.17.1.1 | Reject lossy catalog/tool cell decoding before extending public data access. Cause: unsupported cells become NULL, invalid UTF-8 is replaced and unsigned overflow wraps. Criterion: owning physical Red/Green cases on MySQL/PostgreSQL/SQLite, explicit errors without secret values, and unchanged valid catalog/CLI paths. | — | Evidence: seven lossy cases Red → all three databases Green; four catalog tests and the SQLite CLI automatic-rowid case pass. | [o] |
| T7.17.1.2 | Replace invalid numeric/boolean accessor defaults with checked results and propagate errors through owning catalog/migration paths. Linked to T7.17.1.1. Cause: malformed integer text becomes zero and malformed/NULL booleans become false. Criterion: required conversions return errors without values; only optional integers accept NULL; catalog and migration callers propagate failures, retaining transaction cleanup; owning accessor, real catalog and CLI regressions pass. | — | Evidence: missing Result API compile Red → two accessor tests Green (including all three databases); four catalog, one raw-cell and five CLI regressions pass. Invalid SQLite history is rejected without value exposure or mutation; three-database plan/apply/rollback/recovery remains valid. | [o] |
| T7.17.1.3 | Bound Rust tool query accumulation while streaming instead of fetching all rows before validation. Cause: catalog/tool fetch_all has unbounded retained results. | — | Evidence: missing-budget API compile Red and multiple-statement runtime Red corrected; MySQL prepared unsigned-overflow regression reproduced and corrected. Nine owning query/catalog/accessor tests and eleven CLI tests pass. Actual MySQL/PostgreSQL/SQLite cases verify exact/overflow budgets, invalid ceilings, binds, empty results, secret-free failures and connection reuse. Defaults cap rows at 100,000 and JSON results at 64 MiB; reject rather than truncate and accept one statement. This bounds accumulated output, not driver packet memory/DB execution, and does not complete arbitrary SQL result types. | [o] |
| T7.17.1.4 | Expose ordered bounded query column metadata, including empty results and duplicate aliases. | — | Evidence: missing result API compile Red corrected; actual MySQL/PostgreSQL/SQLite cases verify ordered duplicate aliases, empty-result columns and public catalog queries. Ten owning integration tests, one post-implementation metadata-budget unit regression and eleven CLI tests pass. Prepared metadata/checked rows share one reserved connection; metadata limits are 2,048 columns and 64 KiB of name/type bytes. This is SQL-grid groundwork, not read-only isolation or complete data types. | [o] |
| T7.17.1.5 | Expose bounded read-only catalog queries using dialect AST validation and database-enforced read-only scopes on discard-on-drop connections. | — | Evidence: missing API compile Red corrected; eleven owning integration tests, two post-implementation policy/disposal unit regressions and eleven CLI tests pass. Actual three-database cases verify MySQL function-write rejection, PostgreSQL/SQLite read-only state, unchanged rows and recovery after query errors. Event-driven abort tests verify no scope connection pool reuse; cancellation latency is not measured. Reject mutation/transaction statements, write CTEs, SELECT INTO, locks and executable comments; preserve SQL text and errors and bound parser/visitor depth. Correct the initial MySQL session-variable test assumption without server changes; identified failed test fixtures were cleaned. Not an OS/function sandbox or complete SQL type support. | [o] |
| T7.19 | Add AES blind-index schema declarations, keyed equality predicates, and write synchronization. | — | — | [o] |
| T7.20 | Split oversized root IN predicates, preserve non-IN parameters, merge rows, sum count results, and reject unsafe query shapes. | — | — | [o] |
| T7.21 | Add the soft_delete schema directive, apply active-row predicates to reads and updates, and convert deletes to timestamp updates. | — | — | [o] |
| T7.17.1.6 | Add bounded Rust typed read-only grid results with byte-preserving binary cells separate from text. Cause: tool values reject arbitrary bytes or interpret valid binary UTF-8 as text; SQL grid values need faithful representation. | — | Evidence: missing GridCell/API compile Red is Green; actual MySQL/PostgreSQL/SQLite binary/NULL/empty/text cases pass. Added post-implementation budget, mutation-policy, empty-metadata and bind regressions pass. Twelve owning integration tests, eleven CLI tests and five library tests pass; streaming/metadata budgets and enforced read-only scope/disposal share the existing implementation. Numeric/temporal types and other-client grid APIs remain pending; binary support alone does not complete query types. | [o] |
| T7.17.1.7 | Preserve native Rust grid floating-point bits and unsigned integer range. Cause: strict catalog scalar decoding rejects these query values. | — | Evidence: missing typed variants compile Red is Green; actual three-database float64, PostgreSQL float32/signed-zero/NaN and MySQL u64::MAX cases pass. Numeric NULL and byte-budget checks are added regressions. Thirteen integration, eleven CLI and five library tests pass, retaining strict catalog semantics and read-only disposal. Decimal/temporal types remain pending. | [o] |
| T7.17.1.8 | Preserve finite Rust grid decimal precision and declared scale without float conversion. Cause: current grid values have no decimal tag and fixed-precision model decoding cannot represent arbitrary SQL numeric results. | — | Evidence: missing decimal variant compile Red is Green on actual MySQL 65-digit decimals, PostgreSQL declared trailing-zero scale and SQLite native storage classes. Added PostgreSQL 200-digit/exponent, numeric NULL, byte-budget and nonfinite rejection regressions pass. Fourteen integration, eleven CLI and five library tests pass. Use pinned BigDecimal 0.4.11; restore PostgreSQL binary dscale after public driver decoding and reject any value-changing scale adjustment. Nonfinite temporal values remain pending. | [o] |
| T7.17.1.9 | Expose qualified Rust table metadata with declared primary-key order, native types, nullability and generated columns for explicit table browsing/editing. Cause: logical catalog column flags lose composite-key order and cannot establish reliable row identity. | — | Evidence: missing public API compile Red; actual MySQL/PostgreSQL/SQLite Green for reversed composite keys, generated columns, nullable SQLite keys and rowid aliases. Added views, DESC/WITHOUT ROWID keys, untyped columns and qualified-name/input regressions pass; 12 focused tests pass. This metadata does not authorize edits; mutations and optimistic conflict checks remain pending. | [o] |
| T7.17.1.9.1 | Read qualified MySQL system-view metadata through the Rust catalog API. Cause: actual process inspection of a system catalog fails; the native SYSTEM VIEW kind was absent from the relation classifier. | — | Evidence: actual-MySQL Red confirmed native kind and failed descriptor; Green classifies it as View without reliable row identity. Six focused metadata/catalog tests pass, including ordinary table/view cases on all three databases. The new system-view case changes no database contents; Rust scope does not establish four-client conformance. | [o] |
| T7.17.1.10 | Add explicit qualified Rust table-page reads through the native typed grid API. Cause: arbitrary SQL results do not establish table/column/key provenance for row edits. | — | Evidence: missing public API compile Red is Green on actual MySQL/PostgreSQL/SQLite, preserving exact columns, declared composite-key ordering, quoted identifiers and page continuation. Added empty/view pages, offset/limit bounds, 8 MiB encoded overflow/recovery and changed-descriptor assembly regressions pass. Ten focused library/read-only/metadata/page tests pass. Independent offset pages are not a stable cross-request snapshot or edit authorization; four-client conformance remains separate work. | [o] |
| T7.17.1.11 | Capture checked immutable Rust row-edit baselines from table pages. | — | Evidence: missing API compile Red is Green; three owning baseline tests and all 13 focused library/metadata/page/read-only tests pass. Actual MySQL/PostgreSQL/SQLite owner fixtures detect changed rows and accept restored exact values. Preserve non-null declared key order and bounded 8 MiB value-sensitive revision; reject malformed identity/projection, distinguish schema/missing/changed/ambiguous conflicts and keep Debug/errors free of values. Added validation/type/budget/live cases are regressions. This pure API performs no reads/locks/writes or authorization. Typed binds, locked mutation execution and four-client conformance remain incomplete. | [o] |
| T7.17.1.12 | Extend native Rust typed write binds beyond text/integer. | — | Evidence: missing typed variants/NULL API compile Red is Green. Three new owner validation/physical cases and 40 focused library/native/CLI tests pass, including actual MySQL/PostgreSQL/SQLite typed queries, stored roundtrips, supported typed NULLs, affected rows, rejection recovery, unchanged rejected writes, native constraint errors and rollback. Added use-path/type/budget checks are regressions. Correct SQLite BOOLEAN test assumptions with measured INTEGER storage; finish logs are not premature passes. Reject unsupported kinds/nonfinite cases/malformed decimals and bounded parameter excess before prepare/execution. Separate driver bind modules share query/exec semantics and no SQL value formatting. Test-owned tables/files removed; locked optimistic mutations and four-client conformance remain incomplete. | [o] |
| T7.17.1.13 | Execute caller-authorized native Rust update/delete and explicit-key insertion under owning transactions. Cause: baselines/binds alone did not lock or compare current rows. | — | Evidence: API/verification compile Reds and actual INTEGER-key/commit-rejection Reds are Green; 18 focused library/baseline/page/bind/mutation tests pass on macOS with actual MySQL/PostgreSQL/SQLite. Verify original locks and descriptor/value conflicts, quoted composite keys, key changes, generated/default/NULL values, affected rows, FK/unique preservation, coercion rollback, cancellation/abort cleanup and detached commit ownership. Confirm PostgreSQL constraint rejection with rollback and retain its native cause; owned connection termination reports Indeterminate without retry. MySQL requires InnoDB, verified direct trigger visibility and no triggers/partial revokes. Test-owned resources removed. Added lifecycle/default/budget cases are regressions, not Reds. Automatic identities (T7.17.1.13.2), durable authority/recovery, four-client conformance and other-platform gates remain incomplete. | [o] |
| T7.17.1.13.1 | Correct PostgreSQL bind preparation and commit-error classification. Actual INTEGER-key 22P03 and explicit deferred-constraint rejection Reds are Green. Prepare grid/query metadata with declared codec types; preserve native commit cause, confirm rollback for PostgreSQL constraint/transaction rejection and distinguish owned connection termination. Detached commit remains observable after caller drop. Included in the 18-test owner run and parent work-unit commit. | — | — | [o] |
| T7.17.1.13.1.1 | Require a fallible pre-commit permit for native Rust mutations. Cause: an infallible phase observer cannot prevent commit when durable intent recording fails. Criterion: every insert/update/delete requires an explicit synchronous permit after write verification and before commit ownership; permit failure rolls back with no CommitStarted/Committed event. Verify failed permits and accepted commit ownership on actual MySQL/PostgreSQL/SQLite. No journal implementation or four-client completion is claimed by this primitive. | — | Evidence: required-permit API compile Red is Green. Rejected insert/update/delete permits roll back unchanged rows with no commit-start event on actual MySQL/PostgreSQL/SQLite; invalid assignments do not call the permit and accepted permission commits. All 20 focused owner tests and owning Clippy --no-deps pass; changed-file formatting, paired checklist/rules and fresh documentation build/static checks pass (42 pages, 437 targets, 26 diagrams). Updated existing native callers require explicit permission; no compatibility path. | [o] |
| T7.17.1.13.1.1.1 | Correct final mutation-permit record wording. Cause: final Korean evidence contains a prohibited expression and was committed after its documentation check failed. Criterion: replace only that wording, execute the unchanged owning documentation rules and paired checklist checks, and record the actual correction separately. | — | Evidence: the unchanged documentation rule failed on the final Korean record; replacing the prohibited wording makes all 21 bilingual pairs pass. Paired checklist checks and fresh static documentation checks pass (42 pages, 437 targets, 26 diagrams). Native code and criteria are unchanged. | [o] |
| T7.17.1.13.2 | Support native Rust automatic/default primary-key insertion using authoritative identities. | — | Evidence: generated, literal-default and volatile-expression keys execute on actual MySQL/PostgreSQL/SQLite, including concurrent identities, generated values, constraints/coercion rejection and cancellation rollback. All 21 focused tests and owning Clippy pass. Preserve generation/default descriptors and never reevaluate or guess keys. MySQL ambiguous input locators fail before writing. Durable authority/recovery, other platforms and four-client conformance remain separate incomplete requirements. | [o] |
| T7.17.1.13.2.1 | Return generated/literal keys with generation metadata and bounded server-owned results. Generated-key and MySQL literal-default Reds are Green; 21 focused actual three-database tests pass, including concurrency, default-only values, constraints/coercion rejection and cancellation rollback. Owning Clippy --no-deps passes. Test-owned resources removed; other-platform/four-client completion remain unclaimed. | — | — | [o] |
| T7.17.1.13.2.1.1 | Acquire MySQL target metadata locking before generated-insert descriptor/engine checks. The real LOCK TABLES probe reproduced unheld locking before correction and now receives native error number 1205 at its explicit one-second server deadline. Correct the test's SQLSTATE/error-number distinction and restore its measured session setting. Included in the 21-test owner run and related expression-key commit. | — | — | [o] |
| T7.17.1.13.2.2 | Return MySQL expression-default identities through a bounded transactional input locator. Missing UUID identity Red is Green without default reevaluation, schema changes or new triggers. Require zero existing matches, exact one-row post-write values, then revalidate actual returned keys. Existing matches reject before writing; cancellation/coercion roll back unchanged committed rows. Actual three-database UUID/random-text expressions, NULL selectors and concurrent request-owned identities pass. Concurrency fixture uses disjoint indexed gaps, not a universal deadlock-free claim. Separate validation, RETURNING and locator modules/tests; all 21 focused tests and owner Clippy pass. Test-owned resources removed; no other-platform or four-client completion claim. | — | — | [o] |
| T7.D1 | Keep each English page beside its Korean .ko.md page. | — | — | [o] |
| T7.D2 | Keep headings, code fences, tables, and link targets aligned between paired pages. | — | — | [o] |
| T7.D3 | Provide language links and search for both paths. | — | — | [o] |
| T7.D4 | Remove informal, figurative, personifying, and ambiguous manual wording. | — | — | [o] |
| T7.D7 | Check paired document structure in CI. | — | — | [o] |
| T7.D8 | Check configured writing-style rules in CI. | — | — | [o] |
| T7.D8.2 | Remove prohibited prose without changing interface requirements, performance measurements or checker criteria. | — | Evidence: npm run docs:rules-check reproduced ten file/expression failures before correction and now passes all 21 English/Korean pairs; a fresh npm run docs:build followed by npm run docs:static-check passes 42 pages, 437 internal targets and 26 SVG diagrams, including no-JS reading, search, theme and mobile navigation. make checklist-check passes. | [o] |
| T7.D8.1 | Keep persistent rules in AGENTS.md and task deliverables in this checklist. Remove unnumbered procedure, work-lane, and dated status prose; reject its return with the checklist checker while preserving existing item IDs and states. | — | Evidence: a failing checker case before the cleanup, a passing make checklist-check and paired document checks, and one local commit containing both language versions and the record. | [o] |
| T7.D11 | Regenerate the feature pages from the updated feature manifest. | — | Evidence: make feature-docs generated all 17 feature rows and left the English and Korean feature pages current; the paired-document and static documentation checks pass. | [o] |
| T7.D12 | Keep every tracked Korean document a faithful translation of its English source, with the same rule structure. make docs-rules-check compared only the pairs under docs/ and only their headings, code fences, tables and links; it now also checks every tracked *.ko.md outside docs/ (AGENTS, README, CHANGELOG, CONTRIBUTING, CODE_OF_CONDUCT, SECURITY and the READMEs under clients/rust and tests) and, for every pair, the list items of each heading section and their depth. The translations it found apart are corrected from their English sources: AGENTS.ko.md lacked the rules on the full suite, owner checks and the linux-runner verifications, merged the commit-message and test-run rules and ended one sentence unfinished; CHANGELOG.ko.md lacked the 0.0.1 entries for Tx.Local, NewTransactionConflict, Tx.Driver(), the PostgreSQL advisory locks and ordered DDL installation; README.ko.md lacked the four-language example, the quick-start comments and the document links and ordered its sections differently, and README.md carried a Korean line; docs/codec.ko.md held PHP array, case, vector and generated-code text that the English page does not state. | — | Evidence: with the new check and the previous documents make docs-rules-check failed with nine docs.translation-shape errors, among them AGENTS.ko.md section 1 (# 개발 규칙) has 33 list items, AGENTS.md section 1 (# Development rules) has 36, CHANGELOG.ko.md section 4 (## 0.0.1) has 7 list items, CHANGELOG.md section 4 (## 0.0.1) has 9, README.ko.md code fence declarations differ and three sections of docs/codec.ko.md; it now passes with 34 English documents and 34 Korean translations, and make docs-check passes (48 pages, 544 internal links). The tests and scripts that name the changed files pass: make version-check (25 declarations), make repo-check, scripts/features/owners.test.mjs (4/4); make owner-check with the changed paths selects 0 features. | [o] |
| G0 | Measure client overhead of the in-process clients and record it in perf.md. | — | Evidence: make perf-check passed on 2026-09-27 for Go and PHP both with and without CPU load; all PK and 100-row ratios were within their recorded bounds, and the English and Korean performance records contain the observed values. | [o] |
| G1 | Compare the current conformance output of Go, PHP, Rust, and TypeScript with all 25 recorded vectors on each of MySQL, PostgreSQL, and SQLite. Require two equal physical runs per client with unchanged rows and counters; reject missing, extra, or differing vectors and any changed database state. | — | Evidence: tracked cases first reproduced acceptance of extra dialect expectations and an empty vector set, then passed after exact vector-set validation. An isolated make conformance-check passed all 12 client/database physical cases and compared 25 vectors across four clients on each database; two further SQLite physical runs had equal state digests. | [o] |
| G1.1.2 | Require current-run feature coverage reports for every claimed client and database, exact case IDs, and two equal result and state runs. Mutation tests reject missing implementation claims, language tests, database runs, cases, repeats, and undeclared results. The feature check remains red until every feature provides executable coverage commands. | — | — | [o] |
| G1.1.2.1 | Enforce the AGENTS.md owner and dependent-part test-location rule in scripts/features/coverage.mjs. Mutation tests must reproduce missing owner or dependent-part evidence and central or outside test paths as RED; valid owner and dependent-part execution must be GREEN. make feature-check depends on this checker and remains RED while feature contracts lack current execution evidence. | — | — | [o] |
| G1.1.2.2 | Execute the nine shared aggregate numeric cases from a TypeScript owner test under clients/typescript/tests. A coverage mutation must reject the central test path, the owner case must run twice with identical case IDs and results, and the old central path must be removed. The feature test inventory must detect owner .mjs files, and model_queries verification must invoke the owner test. This is a prerequisite for G1 owner coverage and does not complete the four-language database comparison. | — | — | [o] |
| G1.1.2.3 | Put all remaining TypeScript client behavior tests under clients/typescript/tests and remove their old central paths. A tracked inventory test must fail before the move and pass after it; every test must use the current owner path in commands, feature declarations, and documentation. Execute owner unit cases and the model, schema tool, and SQLite cases on their required databases with bounded tests. Central conformance comparison stays under tests/conformance. | — | — | [o] |
| G1.1.2.4 | Reject a test command that prints an invented JSON success report. Invoke the declared native test file and exact case filter, derive the current result from exit status and observed test events, and read database state with the checker before and after each database run. A tracked fake-report mutation must fail before the change and pass after it; owner and dependent-part tests remain in their own parts. Run bounded checker tests and record remaining feature coverage gaps. | — | — | [o] |
| G1.1.2.5 | Separate shared case IDs from Go and Rust native test symbols in feature coverage commands. Reject missing, extra, duplicate, and nonexistent symbol mappings, and require the declared native pass event before recording the shared ID. Pass the selected database and DSN to each native owner test. Prove the rule with RED mutations and GREEN native execution, then register only the dsn_connection owner and dependent-part cases on MySQL, PostgreSQL, and SQLite with two equal runs and unchanged state. Keep unrelated feature declarations RED until their own evidence exists. | — | Evidence: the native symbol fixture failed before the checker change and all 10 checker tests pass afterward; the isolated dsn_connection run passed 24 owner and 12 generated-model executions, with equal results and unchanged state across both runs on each database. The full feature check remains RED for 16 undeclared coverage kinds, and the pre-existing feature inventory errors reproduce on main. | [o] |
| G1.1.1 | Register the Rust row-value decode test under model_queries so feature verification includes it; N5.1.2.2 depends on this registration. | — | — | [o] |
| G4 | Run generated symbol, schema, and CI checks. Verify that each exported Go, PHP, Rust, and TypeScript client API omits multi_statement and that an attempted call fails interface validation. | — | Evidence: make interface-check inspected 2,827 Go, 1,080 PHP, 2,054 Rust, and 3,111 TypeScript native symbols and rejected a public declaration and attempted call in each language; make schema-check verified 159 schema cases and the current migration plans; the CI command mutation test rejected removal of each generated model, interface, and schema check. A PHP generated model rejected an actual multi_statement(true) call with CONFIG before a connection. Targeted checker, generator, documentation, rule, and checklist checks passed. | [o] |
| G4.1 | Make getsCount return selected grouping values and a checked row_count through a dedicated grouped result in all four clients. Reject malformed counts and do not construct partial models. | — | Evidence: G4.1.1, G4.1.2, and G4.1.4 verify owner behavior on MySQL, PostgreSQL, and SQLite; the common output and four native signatures are checked against current symbols, and an attempted return to model collections fails the logical rule test. make interface-check inspected 2,827 Go, 1,080 PHP, 2,054 Rust, and 3,111 TypeScript symbols and rejected seven source mutations per language. | [o] |
| G4.1.1 | Return a dedicated grouped result from the Go runtime and generated models. Verify selected value types, malformed counts, unselected fields, and physical MySQL, PostgreSQL, and SQLite results. | — | Evidence: Go grouped-result unit cases reject missing, duplicate, malformed, boolean, and negative counts; the generated-model and SQL-keyword owner cases pass on MySQL, PostgreSQL, and SQLite with selected values and counts checked; the Go runner compiles, the generator test passes, and checklist and documentation rule checks pass. | [o] |
| G4.1.2 | Return dedicated GroupRows from PHP and TypeScript getsCount. Each row contains only selected grouping values and a checked nonnegative row_count; absent, duplicate, boolean, lossy, or invalid counts fail. Preserve each selected value's declared type, including boolean and SQL NULL, and never construct a partial model. Generated models expose the result type. | — | Evidence: owner cases first failed on the missing grouped-result type, then PHP unit and TypeScript build, unit, and type cases passed; make group-rows-physical-check passed 12/12 PHP and TypeScript cases across MySQL, PostgreSQL, and SQLite with the same state digest after every case. Documentation, terminology, and checklist checks passed. | [o] |
| G4.1.3 | Reject a PHP read of an unselected non-styled column with COLUMN_UNSELECTED. Preserve selected SQL NULL and assigned values. Cause: readColumn returned null, zero, false, an empty string, or another type-derived value when the column was absent from a loaded row. | — | Evidence: the owner RED case reproduced the default; after correction, the PHP model suite passed 10 cases on SQLite, MySQL, and PostgreSQL, and the generated-model case passed twice on each database with unchanged state digests. | [o] |
| G4.1.4 | Return COLUMN_UNSELECTED when Go or Rust GroupRow.Value requests a name absent from the grouped result. Preserve a selected SQL NULL as present. | — | Evidence: G4.1.4.1 and G4.1.4.2 each reproduced the old return type as RED, passed owner tests, and verified the generated-model path on MySQL, PostgreSQL, and SQLite. | [o] |
| G4.1.4.1 | Make Go GroupRow.Value return an error for an unselected name and keep a selected SQL NULL distinguishable. | — | Evidence: the owner test failed before the signature change and passed afterward; grouped model and SQL-keyword owner cases passed on MySQL, PostgreSQL, and SQLite with COLUMN_UNSELECTED checked. | [o] |
| G4.1.4.2 | Return COLUMN_UNSELECTED from Rust GroupRow::value(name) when the requested name is absent. Preserve selected values, including SQL NULL. | — | Evidence: the owner test failed to compile against the old Option API and passed after the Result<&Val> change (1/1); generated-model columns_and_subqueries passed twice on each of SQLite, MySQL, and PostgreSQL; targeted library and integration Clippy, Rust formatting, English/Korean documentation rules, and checklist checks passed. | [o] |
| G5 | Verify the GitHub Actions build. Cause: the latest recorded ci.yml run started the servers with make test-servers (G5.1, G5.2) and failed in make feature-check because Node 22.16.0 of the workflow printed an ExperimentalWarning for node:sqlite that the coverage checker rejects as test output; G5.3 and G5.4 make the workflow run the declared Node, PHP and Rust, G5.5 makes it run every target of make check, and this local-only checkout cannot publish the current commit for a new run. | — | Evidence: the ci.yml run of G5.24 (job test on ubuntu-26.04-arm) started the servers with make test-servers, ran make check with every target passing, including features/dsn_connection/dsn-php-without-mysql, and succeeded in 36 minutes; the docs-pages run of the same commit succeeded and https://polyspec.github.io/orm/ answers 200. | [o] |
| G5.1 | Make .github/workflows/ci.yml start the servers of the database checks with make test-servers, the definition the local checks use: the MySQL and PostgreSQL primaries and replicas, ProxySQL and PgBouncer, and every DSN of .runtime/servers/env. make repo-check rejects a workflow that does not provide each variable scripts/test-servers.sh writes, defines one of them itself, or runs a check before the servers start. | — | Evidence: before the workflow change make repo-check failed in the new case the CI workflow provides every variable of make test-servers, naming the eight variables the workflow lacked (ORM_TEST_MYSQL_REPLICA_DSN, ORM_TEST_POSTGRES_REPLICA_DSN, ORM_TEST_PROXYSQL_DSN, ORM_TEST_PGBOUNCER_DSN, ORM_TEST_PGBOUNCER_SINGLE_DSN and the three ORM_TEST_MYSQL_TLS_* DSNs), the eight it defined itself and the environment file it wrote. The workflow now installs MySQL 8.4.11, PostgreSQL 17, PgBouncer and ProxySQL 3.0.9, runs make test-servers and adds its 16 variables to $GITHUB_ENV; make repo-check passes 7/7 node cases and the repository case, and TestCIRequiresGeneratedChecks passes. make owner-check with the seven changed paths selects 0 features. Only a GitHub Actions run (G5) can show that the programs install and the servers start on the runner. | [o] |
| G5.2 | Install MySQL 8.4.11 in .github/workflows/ci.yml without a symbolic link: the five packages of the Oracle Ubuntu 24.04 deb bundle through apt, which installs their dependencies such as libaio1t64, so that make test-servers finds mysqld at the package path /usr/sbin/mysqld on PATH. make repo-check rejects a workflow that creates a symbolic link. | — | Evidence: with the new rule make repo-check failed on the G5.1 workflow in the CI workflow provides every variable of make test-servers with ci.yml step "programs of the database servers" creates a symbolic link; the mutation case rejects ln -s, ln -sf, ln --symbolic and symlinkSync. The workflow now installs mysql-common, mysql-community-client-plugins, mysql-community-client-core, mysql-community-client and mysql-community-server-core 8.4.11 from mysql-server_8.4.11-1ubuntu24.04_amd64.deb-bundle.tar, whose control files declare /usr/sbin/mysqld, /usr/bin/mysql_tzinfo_to_sql, /usr/bin/mysqladmin and the dependency libaio1t64, and asserts that mysqld on PATH is /usr/sbin/mysqld at version 8.4.11. make repo-check passes 8/8 node cases and the repository case; make owner-check with the changed paths selects 0 features. Only a GitHub Actions run (G5) can show the installation on the runner. | [o] |
| G5.3 | Run the checks and every GitHub Actions workflow on one Node release, declared once in .node-version (26.8.1, the release of the local checks): ci.yml and docs-pages.yml read it with node-version-file: .node-version instead of 22.16.0 and 24. make repo-check fails when a workflow declares node-version itself, does not read .node-version, or runs Node without actions/setup-node, when .node-version is not one exact x.y.z or is below engines.node, and when the running Node differs from it. engines.node >=22.16.0 remains the lowest release the TypeScript client supports, which make ts-min-check runs; the coverage checker keeps rejecting unexpected output. | — | Evidence: on Node 22.16.0 node --test --test-name-pattern='checker reads physical database state' scripts/features/coverage.test.mjs failed as in that run with sample/owner/typescript/sqlite run 1: unexpected test output: (node:70028) ExperimentalWarning: SQLite is an experimental feature, and on Node 26.8.1 it passes with no warning. With .node-version and the old workflows make repo-check failed in the checks and every workflow run the Node of .node-version, naming node-version in both workflows; it now passes 13/13 node cases and the repository case, and node scripts/repo/check.mjs on Node 22.16.0 fails with Node 22.16.0 runs the checks; .node-version declares 26.8.1. make ts-min-check on Node 22.16.0 passes 335 cases. make owner-check with the twelve changed paths selects 0 features; make docs-check passes. Only a GitHub Actions run (G5) can show the rest of make feature-check and the later steps on the runner. | [o] |
| G5.4 | Run the checks and every workflow on one PHP release and one Rust toolchain, each declared once, as G5.3 does for Node. .php-version declares PHP 8.5, the release of the local checks (8.5.10), and ci.yml reads it with php-version-file instead of 8.4; the declaration is x.y because setup-php and the Homebrew php choose the patch release. rust-toolchain.toml declares channel 1.98.1 with clippy and rustfmt, the toolchain the local make targets ran through PHYSICAL_RUST_TOOLCHAIN ?= 1.98.1 (the rustup default 1.98.0 ran only cargo outside make); the Makefile now reads that channel, and ci.yml installs it with rustup toolchain install instead of dtolnay/rust-toolchain@stable. require.php >=8.4 of clients/php/composer.json remains the lowest release of the PHP client: the new make php-min-check, in CHECK_TARGETS, runs the client-unit-check tests with the PHP that scripts/php/php-min.sh finds (php8.4 on PATH or the Homebrew php@8.4 keg, checked to be 8.4), and ci.yml installs php8.4-cli for it. No Rust minimum is declared (no rust-version); rust-150-check compiles 150 generated models and is not a minimum check. make repo-check fails when a workflow declares php-version itself or runs PHP without setup-php, when .php-version is not one x.y at or above require.php, when the running PHP differs; when a workflow chooses a Rust toolchain (dtolnay/rust-toolchain, rustup default, rustup toolchain install <name>, cargo +<name>, RUSTUP_TOOLCHAIN) or uses cargo without rustup toolchain install, when the Makefile declares a toolchain instead of reading rust-toolchain.toml, when the channel is not one x.y.z with clippy and rustfmt, and when the running rustc differs. | — | Evidence: with the new files and the old workflow node scripts/repo/check.mjs failed with ci.yml declares php-version itself, does not read php-version-file .php-version, chooses a Rust toolchain with dtolnay/rust-toolchain and does not install the toolchain of rust-toolchain.toml with rustup toolchain install, and the rule rejects the previous Makefile with Makefile does not read PHYSICAL_RUST_TOOLCHAIN from rust-toolchain.toml and declares a Rust toolchain itself. make repo-check now passes 18/18 node cases and the repository case (Node 26.8.1, PHP 8.5 and rustc 1.98.1 inspected); make php-min-check passes on PHP 8.4.25 (31 PASS, 0 FAIL); make client-unit-check and make rust-fmt-check pass. make owner-check with the fourteen changed paths selects 0 features. Only a GitHub Actions run (G5) can show that setup-php reads .php-version, that apt installs php8.4-cli and that rustup installs 1.98.1 on the runner. | [o] |
| G5.5 | Make .github/workflows/ci.yml run every target of CHECK_TARGETS once by running make check, the runner of the local checks with its own bench and decimal databases, after make test-servers. The workflow ran 17 of the 43 targets in separate steps and omitted 26, among them ts-min-check, php-min-check, checklist-check, version-check, testcase-check, go-fmt-check, example-check, client-pooler-check, case-database-check, dialect-facts-check, go-test-check and the 15 dbspec-* targets; the separate steps of the 17 are removed. The steps after make check (fuzz, Go, interfaces, state contracts, codec vectors, hot-path regression check) run with if: ${{ !cancelled() }}, so one run reports every result. make repo-check fails when the workflow omits a target of CHECK_TARGETS or, with make check, runs one of them again. | — | Evidence: with the new rule and the previous workflow node scripts/repo/check.mjs failed with 26 errors ci.yml does not run <target> of CHECK_TARGETS, one per omitted target; it now passes, and make repo-check passes 20/20 node cases and the repository case; Ruby YAML.load_file parses both workflows. make owner-check with the changed paths selects 0 features. Only a GitHub Actions run (G5) can show make check on the runner. | [o] |
| G5.6 | Run make php-without-mysql-check without Apple container, on one path: scripts/php-without-mysql.sh runs clients/php/tests/sqlite_without_mysql.php with php -n and -d extension= for each extension the client uses on SQLite (pdo, pdo_sqlite, openssl, zlib, ctype, mbstring) that the PHP does not compile in, and fails with the reason when the PHP compiles mysqlnd, pdo_mysql or mysqli in; the PHP case fails when any of the three is loaded. The check belongs to the Linux runner of .github/runner: in the setup-php PHP 8.4 build for Ubuntu 26.04 arm64, mysqlnd.so, pdo_mysql.so, mysqli.so, pdo.so, pdo_sqlite.so, ctype.so and mbstring.so are shared modules under /usr/lib/php/20240924, while the Homebrew PHP 8.5.10 and 8.4.25 compile mysqlnd, pdo_mysql and mysqli in (php -n -m), and the official image php:8.5.10-cli-trixie of the previous check loads mysqlnd too. Its verification in contracts/features.json declares environment: linux-runner: make check on a machine that is not Linux prints RUNNER features/dsn_connection/dsn-php-without-mysql: … runs on the Linux runner ubuntu-26.04-arm; CI runs it there and does not count it as passed, and make repo-check requires .github/runner to be a Linux runner. Evidence so far: without container on PATH the previous script failed with exec: container: not found (exit 127); on macOS with Homebrew PHP the new script fails with /opt/homebrew/bin/php compiles mysqlnd in, so php -n cannot leave it out; …; make owner-check with the twelve changed paths selects dsn_connection, passes its coverage and verification and prints the RUNNER line; make repo-check passes. On Linux: the ci.yml run of G5.24 on ubuntu-26.04-arm ran php 8.5.11 -n -d extension=pdo -d extension=pdo_sqlite -d extension=ctype -d extension=mbstring and passed php-without-mysql and features/dsn_connection/dsn-php-without-mysql. | — | — | [o] |
| G5.7 | Run each check once in .github/workflows/ci.yml: the steps common interfaces (make interface-check) and hot-path regression gate (make perf-check) are removed, because make check runs their commands, PATH="$HOME/.cargo/bin:$PATH" go run ./tests/interfaces/check --self-test and ./scripts/perf-test.sh, as verification commands of contracts/features.json in feature-check. make repo-check fails when a workflow line equals a verification command that runs from the repository root, or runs a make target outside CHECK_TARGETS whose every recipe command is one (make's $$ and $(NAME) read as the shell's $ and $NAME, without the leading $(WITH_TEST_ENV)). The rule compares whole commands; tests that two different commands both select remain: go test ./... of the Go step and the Go and Rust lines of codec vectors run tests that feature verification commands also run with other filters. | — | Evidence: with the new rule and the G5.5 workflow node scripts/repo/check.mjs failed with ci.yml step "common interfaces" runs make interface-check, whose commands make check runs in feature-check and the same error for hot-path regression gate; it now passes, make repo-check passes 22/22 node cases and the repository case, and Ruby YAML.load_file parses the workflow. make owner-check with the changed paths selects 0 features. Only a GitHub Actions run (G5) can show the workflow on the runner. | [o] |
| G5.8 | Run every workflow job on the runner ubuntu-26.04-arm, declared once in .github/runner, and make each install step valid there. ci.yml: MySQL 8.4.11 from the Ubuntu 26.04 packages mysql-server-core and mysql-client-core (8.4.11-0ubuntu0.26.04.1, which hold /usr/sbin/mysqld, mysql_tzinfo_to_sql and mysqladmin) instead of the Oracle amd64 bundle, checked as Ver 8.4.11; PostgreSQL 17 and PgBouncer from the PostgreSQL apt repository, whose resolute-pgdg holds postgresql-17 17.11 for arm64; ProxySQL 3.0.9 from proxysql_3.0.9-ubuntu24_arm64.deb, checked by its sha256 e12b2721…eb2a0f, because the ProxySQL repository has no resolute: on the runner its dependencies are satisfied and the extracted binary runs (ProxySQL version 3.0.9-618-g7ddb3dc). The build of SQLite 3.46.1 for PHP is removed: the image links PHP to the system SQLite 3.46.1, and the step now only checks that PHP reports 3.46 or later. PHP 8.4 for make php-min-check comes from a second shivammathur/setup-php step, which supports 8.4 on ubuntu-26.04-arm, reading the version that the step php-min writes from scripts/php/php-min.sh --version; the step of .php-version runs last so that PHP 8.5 is php. The static-php-cli common build of PHP 8.4.23 is not used: it links SQLite 3.45.2 on linux-aarch64 and macos-aarch64, below the 3.46 every client requires (dsn.php fails with CAPABILITY_UNSUPPORTED: SQLite 3.45.2 is older than 3.46), and the PHP 8.4 builds that setup-php installs are replaced under the same release URL, so no checksum can pin them. docs-pages.yml runs both jobs on the same runner. make repo-check fails when a job runs on another runner than .github/runner or declares none, when a setup-php step reads steps.php-min.outputs.version without the step php-min that writes it, when it installs the lowest release twice, and when the step of .php-version is not the last setup-php step. | — | Evidence: with .github/runner the rule rejects the previous workflows with ci.yml runs on ubuntu-24.04; .github/runner declares ubuntu-26.04-arm and the same error for both jobs of docs-pages.yml; make repo-check passes 25/25 node cases and the repository case; Ruby YAML.load_file parses both workflows. make owner-check with the changed paths selects 0 features. Only a run on ubuntu-26.04-arm can show the installations, setup-php 8.4 and the servers there. | [o] |
| G5.9 | Make TestCIRequiresGeneratedChecks (tests/interfaces/check/prohibited_test.go) check the workflow of G5.5 and G5.7. The test required the steps run: make go-model-check, run: make ts-model-check and run: make interface-check, which G5.5 and G5.7 replaced with make check; the commits of G5.5 and G5.7 were made without running it, and go test ./tests/interfaces/check failed with CI must run make go-model-check. The test now requires that ci.yml runs make check, that CHECK_TARGETS holds go-model-check and ts-model-check, and that contracts/features.json verifies go run ./tests/interfaces/check --self-test, the command of make interface-check; a mutation removes each of the four and must fail. | — | Evidence: go test ./tests/interfaces/check -count=1 failed before the change and passes after it; make owner-check with the changed paths selects 0 features. | [o] |
| G5.10 | Start every MySQL server of make test-servers with --secure-file-priv=NULL, the value the local checks had, instead of the compiled default of each build. The Ubuntu 26.04 mysqld defaults to /var/lib/mysql-files, a directory that only the mysql-server package creates, while the workflow installs mysql-server-core; the Homebrew build defaults to NULL. | — | Evidence: a run of T40 on an Ubuntu 26.04 ARM64 machine failed in make test-servers with mysqld: Error on realpath() on '/var/lib/mysql-files' (Error 2 - No such file or directory); locally mysqld --secure-file-priv=/var/lib/mysql-files fails to start with the same error and [MY-010095] Failed to access directory for --secure-file-priv, and with --secure-file-priv=NULL after it the server starts and reports @@secure_file_priv NULL. The run of the working tree with G5.10, G5.11 and G5.12 started every server and wrote .runtime/servers/env (test-servers: started). make owner-check with the changed path selects 0 features; make repo-check passes. | [o] |
| G5.11 | Make make test-servers fail before it starts a server when a Unix socket path under .runtime/servers exceeds the platform limit, 107 bytes on Linux and 103 on macOS, naming the path, its length and the limit: test-servers: socket path <path> is <N> bytes; <system> allows at most <limit>. The five socket paths (MySQL, MySQL replica, the two ProxySQL admin interfaces and the ProxySQL PostgreSQL interface) are declared once. A deep checkout, such as a long worktree path, failed with the MySQL log line The socket file path is too long (> 107) after the servers began to start. | — | Evidence: the new case test-servers.sh refuses a socket path longer than the platform limit of scripts/repo/check.test.mjs copies the script under a deep checkout, once where every socket is too long and once where only proxysql-admin-pgsql.sock is; with the previous script it failed with Failed to start mysqld daemon and start failed; logs are in … after starting mysqld, and now it passes in 85 ms with the exact message and no .runtime/servers. make repo-check passes; make owner-check with the changed paths selects 0 features. | [o] |
| G5.12 | Read the output of ProxySQL and PgBouncer in make test-servers line by line as it arrives. start_logged copied the output through awk and waited for the ready line; the awk of Ubuntu, mawk, reads a pipe in large blocks and passes no line until the server exits, so on the Linux runner the start of ProxySQL waited until the run was stopped after 3981.5 s, and PgBouncer the same. A sh loop of read now copies each line to the log and reports the ready line; the read of the FIFO opens it again when the exit of a fast-ending server interrupts the open (Interrupted system call of the macOS /bin/sh). | — | Evidence: on the Ubuntu 26.04 machine (echo ready; sleep 5) | awk '{ print "got " $0; fflush() }' prints got ready only after 5 s, ProxySQL writes 101 log lines within 6 s to a file, and the run of T40 with G5.10 and G5.11 waited in ProxySQL with no .runtime/servers/proxysql.log; the new case start_logged reports a ready line while the server keeps running of scripts/repo/check.test.mjs runs the function of the script with a server that keeps running after its ready line and with one that exits first, and passes in 36 ms six times in a row; the run of the working tree with G5.10, G5.11 and G5.12 started ProxySQL and PgBouncer and wrote .runtime/servers/env. make repo-check passes; make owner-check with the changed paths selects 0 features. | [o] |
| G5.13 | Give every server start and every CI step its own deadline. start_logged in scripts/test-servers.sh takes a declared deadline per server (60 s for ProxySQL and PgBouncer, which log their ready line within seconds); a timer reports through the same FIFO, and a start that passes it fails with test-servers: <name> logged no line with '<line>' within its deadline of <N> s; last lines of <log>: and the last 20 lines of the log, and an exited server reports its last lines too. Every step of .github/workflows/ci.yml and docs-pages.yml declares timeout-minutes, sized from the run of the working tree on ubuntu-26.04-arm (4 vCPU, 16 GB), where make check took 1150.1 s and every other step under 84 s, with room for cold caches and downloads: make check 90, Go and codec vectors 30, the installs 10 to 20, the rest 2 to 15. make repo-check fails on a workflow step without a positive timeout-minutes. | — | Evidence: the run before G5.12 waited 3981.5 s in database servers without a deadline; the case start_logged reports a ready line, an exit or its deadline now fails a silent server after its 2 s deadline with the exact message and passes in 2.1 s, also 24 times in a row with six runs at a time (the timer stops only its sleep, so the shell reports no killed job); it fails against the previous script; with the new rule make repo-check failed with 27 errors <workflow> step "<step>" has no timeout-minutes of its own and now passes; Ruby YAML.load_file parses both workflows. make owner-check with the changed paths selects 0 features. | [o] |
| G5.14 | Keep the documentation build green: the T40 items of docs/checklist.md and its Korean pair wrote <kind> <table>[.<name>] and <nil> outside code, which VitePress compiles as unclosed HTML elements, so make docs-check and make docs-verify-idempotent failed with docs/checklist.md (209:2878): Element is missing end tag locally and on an Ubuntu 26.04 ARM64 machine. The two messages are now inline code, and make docs-rules-check fails on an element outside code in a page under docs/ other than <br>, which the generated feature pages use: docs.element: <file> writes <tag> outside code; put it in inline code. | — | Evidence: with the new rule and the previous checklist pair make docs-rules-check failed with eight docs.element errors (<kind>, <table>, <name> and <nil> in each language); now it passes, make docs-check passes (48 pages, 546 internal links) and make docs-verify-idempotent passes. make owner-check with the changed paths selects 0 features; T41 makes owner checks run the documentation checks for a changed document. | [o] |
| G5.15 | Fetch the crates of clients/rust and bench/rust before any cargo command that runs with --offline. The new target rust-fetch runs cargo fetch --locked for both lockfiles, and the 14 targets that run cargo offline, in their recipe or through scripts/decimal-physical-check.mjs and tests/timing/preempted.test.mjs, name it as their prerequisite; a target no longer depends on an earlier target having filled the cargo cache. | — | Evidence: on a fresh Ubuntu 26.04 ARM64 machine, make rust-driver-check failed in 19 ms with error: no matching package named mysql_async found (location searched: crates.io index, offline); locally cargo test --locked --offline in bench/rust with an empty CARGO_HOME fails with the same error, and make rust-driver-check with an empty CARGO_HOME now passes (PASS rust-fetch elapsed=12.701s, then 0, 1, 4 and 2 passing tests). make repo-check and TestCIRequiresGeneratedChecks pass; make owner-check with the changed paths selects 0 features. | [o] |
| G5.16 | Wait up to 60 s, instead of 10 s, for the MySQL replica to reach the binary log position of the primary in the primary and replica cases of Go, PHP, Rust and TypeScript. The replica applies the writes of every client that make client-db-check runs in parallel, one after another, and on the 4-vCPU Linux runner the PHP case failed with the replica did not reach binlog.000001:358010684 after its 10 s wait; the deadline limits work that another process does, so it is wall-clock time, and the comment beside each wait states the reason. | — | Evidence: the run of the working tree on ubuntu-26.04-arm failed client-db-check with that error; make owner-check with the four changed test files selects nine features (dsn_connection, model_queries, model_writes, transactions, schema_install, composite_keys, authenticated_encryption, parameter_chunking, audit_triggers) and passes 18/18 verification and coverage steps; gofmt, rustfmt, php -l and node --check pass. | [o] |
| G5.17 | Run the PHP of the checks without a debugger or coverage driver, as the local PHP runs. Both shivammathur/setup-php steps of ci.yml set coverage: none, and make repo-check fails on a setup-php step without it (<workflow> sets up PHP without coverage: none, so Xdebug or pcov can load); clients/php/tests/perf_gate.php refuses to measure when Xdebug or pcov is loaded (perf_gate: this PHP loads <extension>; …), through perfGateRefusedExtensions of clients/php/tests/perf_extensions.php, whose case perf_extensions_test.php runs in make client-unit-check. | — | Evidence: the run of the working tree on ubuntu-26.04-arm loaded Xdebug 3.5.3 into PHP 8.5.4 and 8.4.26, and the PHP hot-path regression check measured ratios 1.64 (pk, bound 1.35) and 1.28 (list100, bound 1.25); with the new rule make repo-check failed twice on ci.yml and passes after the change, also for a removed or xdebug value; perf_extensions_test.php passes 2/2 (the local PHP loads neither extension, so the refusal itself was not run here). make owner-check with the changed paths selects performance_gate and passes its verification and coverage; make client-unit-check, make repo-check and TestCIRequiresGeneratedChecks pass. | [o] |
| G5.18 | Define ORM_TEST_MYSQL_SERVER_DSN and ORM_TEST_POSTGRES_SERVER_DSN in the environment file of make test-servers, the one definition of the variables the checks read. Only WITH_TEST_ENV of the Makefile set them, so the CI step go test ./..., which reads the environment file through $GITHUB_ENV, failed TestRollbackFailed{MySQL,Postgres} and TestSavepointRollbackFailed{MySQL,Postgres} with ORM_TEST_MYSQL_SERVER_DSN is required on the Linux runner. write_env writes the file, WITH_TEST_ENV no longer defines the two variables, and start writes the file again for running servers, so an existing file follows the definition. | — | Evidence: with the previous environment file, go test ./clients/go/orm -run TestRollbackFailedMySQL failed with that message; after make test-servers the file defines both variables once (no variable twice) and the four rollback tests pass with only the file loaded. make owner-check with the changed paths selects the feature transactions and 24 make targets; all passed except case-database-check, whose TypeScript case model/postgres/schemaEmpty ended with an unhandled terminating connection due to administrator command from an idle pool connection, and which passed when run again; that intermittent failure is not caused by this change and is recorded for its own item. | [o] |
| G5.19 | Give docs-pages.yml an explicit build step, so its stages are build, test and smoke as GitHub runs them. The job publishes docs/.vitepress/dist, but its only step that wrote it was Build twice and compare every static file (make docs-verify-idempotent), a test step. The workflow does produce an artifact, so it declares no skip: Build the static site runs make docs-build, the idempotence check builds twice more and compares every byte, and Smoke test of the built site in a browser runs make docs-static-check on the output before the upload. | — | Evidence: docs-pages.yml now has the stages build, test and smoke; Ruby YAML.load_file parses the workflow; make owner-check with the changed paths selects checklist-check, version-check, repo-check, docs-rules-check, docs-check and docs-verify-idempotent and passes them. | [o] |
| G5.20 | Run every MySQL test server with lower_case_table_names=1, declared once in scripts/test-servers.sh, and move existing data to it by a repeatable procedure. MySQL picks the value from the file system when it initializes a data directory (2 on macOS, 0 on Linux), and make test-servers did not set it, so dialect-facts-check failed on the Linux runner with got "0"; want "2"; 1 is the only value that both platforms accept. make test-servers passes --lower-case-table-names=1 to every mysqld and writes .runtime/servers/mysql.settings. When the servers already run, scripts/test-servers-mysql.mjs compares the declared value with the marker, or with the running primary when there is none: the same value changes nothing; another value runs the migration steps preflight (names that the value would merge refuse the migration, with the list, before any change), dump (every non-system database with routines, triggers, events, the users and grants, and the per-database counts), stop, keep (the old data directories move to .runtime/mysql-before-<stamp> and are not deleted), start, restore, verify (primary and replica against the recorded counts) and record, each with RUN, PASS or FAIL, its elapsed time and its deadline; progress is recorded, so a run after a failure resumes and rebuilds only the new data directory. The MySQL facts of tests/dialects/probes_mysql.go and docs/spec/dialects.md state 1. The new target test-servers-check (in CHECK_TARGETS, with its inputs) runs scripts/test-servers-mysql.test.mjs: servers initialized with the platform default migrate with equal counts and a second run prints unchanged; colliding names refuse the migration on Linux, and on macOS the server itself rejects such a pair (1050). | — | Evidence: make dialect-facts-check failed with got "2"; want "1" against the local servers; test-servers-check passes 4/4 on macOS; on the local servers make test-servers found 19 databases and no collision, recorded 161 counts (among them a database of 44 tables and 63 triggers, orm_bench 12 tables, dbspec_sub_probe 40 tables, one user), migrated in about 33 s, verified all 161 counts on the primary and the replica, kept the earlier data in .runtime/mysql-before-20261004-121451, and a second make test-servers printed lower_case_table_names=1 unchanged; both servers report lower_case_table_names 1 and secure_file_priv NULL, and make dialect-facts-check passes. | [o] |
| G5.21 | Keep the TypeScript client running when the PostgreSQL server ends a pooled connection that waits in the pool or that the pool is closing. The pg pool reports the server's message on such a connection as an error event of the pool and removes the connection; the client registered no listener, so Node ended the process with Unhandled 'error' event. That ended case-database-check once in model/postgres/schemaEmpty with terminating connection due to administrator command (57P01) from the pool's idleListener: Db.close() resolves when pg-pool 3.14 has started closing its idle connections, before their sockets close, and the case then drops its database WITH (FORCE), which ends a connection that is still closing when the server handles the drop first; the PostgreSQL log shows that termination at 21:32:29.621 followed by the drop's checkpoint. PostgresPoolDriver now listens for the pool's error event; no statement runs on that connection, so no caller receives the error, and the next statement opens a new connection. The MySQL pool already survived the same end. | — | Evidence: the new case clients/typescript/tests/idle-connection-end.mjs (in npm run typescript:test and the dsn_connection tests) runs a child process that keeps one idle pooled connection to a new case database, has the server end it (pg_terminate_backend on PostgreSQL, KILL on MySQL), waits until the server has closed the session (KILL returns before that, so the MySQL branch waits until PROCESSLIST no longer lists it) and for one turn of the event loop, runs a statement again and closes; before the change the PostgreSQL child ended with status 1 and Unhandled 'error' event on every run, and the MySQL child passed; now both pass, 40 of 40 runs on the minimum Node version. Without the wait, the MySQL statement failed in 4 of 30 runs with PROTOCOL_CONNECTION_LOST or ECONNRESET, because it reached the ended connection before the pool removed it. | [o] |
| G5.22 | Run every program that cargo builds from CARGO_TARGET_DIR, which the Makefile declares and exports. The checks named clients/rust/target/... directly: the Makefile targets dbspec-apply-pairs-check, dbspec-introspect-compare-check, dbspec-stress-bench, example-check and timing-check and the stress document paths, the chunking-rust command of contracts/features.json, scripts/case-database-check.mjs, scripts/client-db-test.sh, tests/dbspec/compare/runners.mjs and tests/dbspec/inputs.test.mjs; scripts/check-rust-150.sh and scripts/package-check.sh overrode the declared value with $ROOT/clients/rust/target, and tests/conformance/check fell back to clients/rust/target when the value was unset. With another target directory, such as the one a worktree shares with the main checkout, cargo built into it and the checks looked elsewhere (clients/rust/target/debug/integration: No such file or directory, spawn clients/rust/target/debug/examples/dbspec_compare ENOENT). Every path now comes from $(CARGO_TARGET_DIR) in the Makefile, ${CARGO_TARGET_DIR:?…} in shell, cargoTarget() of tests/cargo-target.mjs in Node and CARGO_TARGET_DIR in Go, and each fails when the value is unset instead of guessing a directory. make repo-check rejects a run command in the Makefile, contracts/features.json, a workflow, scripts/ or tests/ that names clients/rust/target (also as "clients", "rust", "target"), except comments and the declaration itself; documents and example comments may describe the default location. | — | Evidence: with the shared target directory the worktree owner check of T41.1 failed chunking-rust and schema-compare with those errors; with the new rule make repo-check failed with 17 lines naming clients/rust/target, and on the previous tests/conformance/check/main.go with its fallback line; now it passes, and make owner-check with the changed paths passes the selected feature and make targets. | [o] |
| G5.22-1 | Read the package directory of a Rust test when the test runs, not when cargo compiles it. Cause: 31 lines in 24 Rust files under clients/rust (integration tests and the unit tests of orm/src/codec.rs and orm/src/engine/mod.rs) located fixtures and the repository through env!("CARGO_MANIFEST_DIR"), which writes the absolute path of the checkout that compiled the test into its binary. A worktree builds into the main checkout's CARGO_TARGET_DIR (G5.22), and cargo records workspace sources by relative path and does not rebuild a binary when only the checkout differs, so the main checkout ran test binaries that the removed worktree orm-test-design-G5.29 had built. The final make check failed client-db-check and client-pooler-check (add_tables_and_columns: orm-test-design-G5.29/clients/rust/orm/../../../contracts/fixtures/add_tables_and_columns/log.dbs: No such file or directory) and feature-check (the Rust coverage of schema_definition, authenticated_encryption and interface_contract), with 56 lines naming that worktree. Now orm_testcase::manifest_dir() reads the CARGO_MANIFEST_DIR that cargo test sets for the run and panics when it is unset, and every such line uses it. scripts/features/coverage.mjs, which runs Rust test binaries directly, gives each the package directory of its crate as CARGO_MANIFEST_DIR, as cargo test does. make repo-check (manifestDirErrors of scripts/repo/target.mjs) fails a Rust line outside comments that reads env!("CARGO_MANIFEST_DIR"); CARGO_BIN_EXE_* and OUT_DIR stay, since they name the shared target directory itself. | — | Evidence: the new repository case failed before manifestDirErrors existed, and node scripts/repo/check.mjs then reported the 31 lines; the Rust probe of the coverage runner test, which asserts the runtime CARGO_MANIFEST_DIR, failed with left: Err(NotPresent) and the owner check failed 7 Rust coverage parts with CARGO_MANIFEST_DIR is unset before the runner gave the value. Now both repository runs pass, the coverage runner test passes, make rust-fmt-check rust-check passes with the shared target directory, and make owner-check with the 34 changed paths passes its 22 steps. | [o] |
| G5.23 | Cancel the blocked read of TestWithContextCancelsInsideTransaction (clients/go/orm/cancel_test.go) once the server reports it waiting for the lock, not after a fixed 300 ms. The test expects ROLLBACK because the MySQL and PostgreSQL drivers close the connection of a cancelled statement, but under load the timer fired before the statement reached the server: database/sql then returns the cancellation without touching the connection, the rollback succeeds, and the result is CANCELED. That failed client-db-check once in an owner check with blocked read: CANCELED: context canceled (code "CANCELED"), want ROLLBACK for both PostgreSQL cases, while creating a case database took 15 s. The test now polls a server session of the case database (performance_schema.data_locks with LOCK_STATUS = 'WAITING' on MySQL, pg_stat_activity with wait_event_type = 'Lock' on PostgreSQL) and cancels when a session waits; no wait within 10 s fails the test. | — | Evidence: with a 1 s delay before the statement, the old test failed all four cases with that message and the new test passes them; without the delay go test -run '^TestWithContextCancel' -count=3 passes. | [o] |
| G5.24 | Keep the first report of start_logged (scripts/test-servers.sh) when a signal interrupts its read. The function waited on a FIFO that it opened anew for every read; the end of a background job delivers SIGCHLD, which can interrupt that read (EINTR), and the report written after the interrupted read closed the FIFO had no reader and was lost. The deadline's timeout was then read in place of the server's exited, or, when both writers were lost, the function waited forever. That failed make repo-check once on its own with fake logged no line with 'accepting connections' within its deadline of 2 s where the case expects fake exited before it logged 'accepting connections'. The function now opens the FIFO once for reading and writing and keeps it open while it reads, so a report stays in the FIFO's buffer until a later read takes it; the timer always writes at the deadline, so the loop ends. | — | Evidence: 72 parallel runs (3 rounds of 24) of the start_logged case of scripts/repo/check.test.mjs failed 32 times before the change, 28 with the deadline message in place of the exit and 4 with the shell still waiting at the 20 s spawn limit; with the change all 72 pass. | [o] |
| G5.25 | Restore a PostgreSQL sequence of the conformance check by the table that owns it, read from pg_depend. restoreCounters in tests/conformance/check/counters.go took the table from the sequence name, <table>_seq_seq, but PostgreSQL truncates a sequence name to 63 bytes, so a long table name gave another name and its sequence was refused as undeclared. readCounters now reads, with each sequence, the table that owns it through pg_depend (deptype a for a serial column, i for an identity column), and restoreCounters checks that table against the declared write tables; a changed sequence that no table owns is refused with owned by no table. | — | Evidence: the new case TestPhysicalPostgresCounterOwner (in conformance-counter-check) creates, in a case database, a 63-byte table whose serial sequence PostgreSQL names conformance_counter_owner_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_seq_seq, an identity table task, an undeclared table and a free sequence; before the change it failed with undeclared postgres counter changed: "public"."conformance_counter_owner_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_seq_seq"; now both declared counters are restored to their earlier values, and the undeclared table and the free sequence are refused. | [o] |
| G5.26 | Show {{ in a documentation page as written, and fail make docs-build when the server render of a page fails. Cause: VitePress compiles each page as a Vue template and marks only fenced code v-pre, so the inline code if: ${{ !cancelled() }} of G5.5 ran as an interpolation; the server render of the checklist page logged TypeError: _ctx.cancelled is not a function and, as a production Vue build only logs an unhandled error, wrote checklist.html and ko/checklist.html without their content while the build passed. The markdown.config of docs/.vitepress/config.mts now marks inline code and indented code v-pre, and wraps Markdown text that holds {{ in a v-pre span; the theme's enhanceApp (docs/.vitepress/theme/index.ts) sets app.config.throwUnhandledErrorInProduction for the server render, so a render error ends vitepress build with exit 1. Red: before the change make docs-build printed the TypeError twice and exited 0, and the built checklist.html (21733 bytes) held no G5.5. Green: make docs-build exits 0 without an error, and checklist.html and ko/checklist.html hold <code>if: ${{ !cancelled() }}</code>; with only the theme change, the unchanged G5.5 made make docs-build fail with build error: TypeError: _ctx.cancelled is not a function and exit 2; in a scratch copy, an HTML block <div class="scratch">{{ missing() }}</div> made it fail with TypeError: _ctx.missing is not a function, and the Markdown text {{ missing() }} rendered as written. | — | — | [o] |
| G5.27 | Let make owner-check run only the changed features' checks and the per-file targets, and let scripts/features/coverage.mjs --feature <id> name the valid ids. Cause: contracts/check-inputs.json gave every make target of CHECK_TARGETS the same standing, so a change across the four clients selected whole-suite targets such as client-db-check, client-pooler-check, case-database-check, conformance-check and feature-check (all features' coverage, about 6 minutes) under a 30-minute deadline per target; the T42 change ran about 30 minutes, the full suite once per item. feature-check also matched contracts/**, so a changed fixture ran every feature's coverage next to the selected feature's own. An unknown --feature id of coverage.mjs failed with unknown feature <id> and named no valid id. Now every target in contracts/check-inputs.json declares scope: owner (per-file checks: format, checklist, documents, checker unit tests, with inputs) or suite (only make check). scripts/features/owners.mjs selects owner targets only and fails before running anything when a target of CHECK_TARGETS lacks a scope, an owner target lacks inputs or a suite target declares inputs; make repo-check reports the same errors. A change to contracts/features.json selects the features whose entry differs from HEAD. The make targets and feature steps run as groups without an outer deadline; each case inside keeps its own. The unit tests and the feature-document check moved from feature-check to the new owner target feature-unit-check, so make check still runs each once. AGENTS.md and its Korean pair state the rule: per item only the Red/Green tests of what changed and make owner-check; make check once when every active item is done, and on push in CI. Red: on main, node --test scripts/features/owners.test.mjs failed 5 of 11 cases: the T42-shaped set of nine paths across the four clients selected 30 targets including client-db-check and feature-check where the case expects repo-check, go-fmt-check, rust-fmt-check; a target without a scope was selected instead of failing; manifestChanges did not exist; and the two cases on the declaration shape failed. In scripts/features/coverage.test.mjs the unknown-id case failed with unknown feature missing; the single-feature case (only the statement_events owner specs on three databases) passed on main, since --feature already selected one feature. Green: owners.test.mjs passes 11 of 11 and the three selection cases of coverage.test.mjs pass. owners.mjs --list over the 97 non-document paths of T42 selects 15 features and 8 owner targets instead of 38 targets. make owner-check PATHS= four statement_events test files (one per client) ran repo-check, go-fmt-check, rust-fmt-check, the verification of statement_events (11.6 s) and its coverage (44.8 s) in 65 s against about 6 minutes for the coverage of every feature. | — | — | [o] |
| G5.28 | Make the Rust transaction option timeout_ms follow its definition in docs/spec/interfaces.md and docs/operations/usage.md: PostgreSQL applies it as SET LOCAL statement_timeout, and MySQL and SQLite return CAPABILITY_UNSUPPORTED. Cause: the Rust client ran the callback of a transaction with timeout_ms under a client timer (tokio::time::timeout) on every database and, on expiry, returned CANCELED transaction callback timed out after it closed the session or, on SQLite, rolled back. A client timer does not stop a statement on the server, so the option claimed a bound on MySQL and SQLite that it did not have, and on PostgreSQL the timer raced the server's statement_timeout. Go, PHP and TypeScript already follow that definition with the same check before BEGIN. begin in clients/rust/orm/src/tx.rs now returns CAPABILITY_UNSUPPORTED transaction timeoutMs is supported only by postgres for a positive timeout_ms on MySQL and SQLite before it takes a connection, as Go's begin does; the timer and its cancelled path are removed from Transaction and SendTransaction, and the Rust README describes the option as the PostgreSQL statement bound. No shared vector covers transaction options: tests/events/vectors.json runs transactions without options. Red: the case tx::tests::transaction_timeout_is_the_postgres_statement_timeout, which replaces callback_timeout_cancels_a_statement_and_rolls_back, failed on the earlier code with sqlite: got "CANCELED: transaction callback timed out" with callback run true and the same for MySQL, where CAPABILITY_UNSUPPORTED: transaction timeoutMs is supported only by postgres without running the callback is expected; in one of two runs PostgreSQL returned the timer's CANCELED: transaction callback timed out in place of the server's canceling statement due to statement timeout (SQLSTATE 57014). The transactions case of the Rust integration program, which now expects CAPABILITY_UNSUPPORTED for timeout_ms(500) on MySQL and SQLite, failed on SQLite with left: "ok". Green: the lib case passes on the three databases in four runs, PostgreSQL with the server's 57014 error and the callback run, MySQL and SQLite with CAPABILITY_UNSUPPORTED and the callback not run, and timeout_ms(0) commits on all three; integration schema/bench.dbs --case transactions passes on SQLite, MySQL and PostgreSQL. | — | — | [o] |
| G5.28-1 | Remove clients/rust/orm/src/tx.rs::cancelled from the Rust symbol snapshot. Cause: G5.28 removed the client timer and its function cancelled from clients/rust/orm/src/tx.rs but left the function in contracts/symbols/rust.json, so go run ./tests/interfaces/check failed with rust: missing clients/rust/orm/src/tx.rs::cancelled. The final make check did not show it: the Rust coverage test coverage_interface_symbols failed before it ran the check, because its binary looked for the repository in a removed worktree (G5.22-1), and that failure stopped feature-check before its verification command interface-self-test. Now the snapshot has no cancelled entry and the rust hash in contracts/interfaces.json is that of the new snapshot. | — | Evidence: before, go run ./tests/interfaces/check in the main checkout failed interfaces/rust with that line and passed Go, PHP and TypeScript; go run ./tests/interfaces/check -language rust -record removed only that line; now go run ./tests/interfaces/check --self-test passes all six cases. | [o] |
| G5.29 | Make every check follow the test rules of AGENTS.md: each case reports its run, steps, result and elapsed time under its own deadline; no CI step or whole suite has a deadline of its own; the full suite runs once per push; and make owner-check runs only the commands and coverage that a changed file owns. The sub-items G5.29-1 to G5.29-13 each correct one defect. | — | Evidence: make owner-check with the 43 paths that the branch changed selected 11 owner make targets, 3 helper checks and 18 verification commands of 4 features, and passed all 18 steps (the longest testcase-check 1 m 9 s and the perf-gates verification 52 s). | [o] |
| G5.29-4 | State the timeout rule of AGENTS.md without a contradiction. Cause: the testing rule said that every test has its own timeout and, in the next sentence, that a long operation gets detailed step logs instead of a timeout, which allows a long operation without a deadline. Now a long operation gets detailed step logs in addition to its own timeout, in AGENTS.md and AGENTS.ko.md. Red: grep -n 'instead of a timeout' AGENTS.md printed line 53 and grep -n '타임아웃 대신' AGENTS.ko.md printed line 44. Green: both print nothing, and the new sentence is in both files. | — | — | [o] |
| G5.29-6 | Declare the cases of scripts/version/check.test.mjs with caseTest and reject a JavaScript test that declares its tests with node:test directly. Cause: the version check test imported test from node:test, so its three cases had no deadline of their own and printed no RUN, PASS or FAIL line with an elapsed time, unlike the other *.test.mjs files, which use caseTest of tests/testcase.mjs; no check found such a file. The three cases now run through caseTest with the COMPUTE deadline (1 minute). nodeTestErrors in the new scripts/repo/testcases.mjs fails every tracked JavaScript file outside docs/ that imports test, it, describe, suite or the default export of node:test; hooks such as after stay allowed, and tests/testcase.mjs, which defines caseTest, is not checked. make repo-check runs it. Red: the new case every JavaScript test declares its cases with caseTest of scripts/repo/check.test.mjs failed on main with scripts/version/check.test.mjs declares tests with node:test directly. Green: that case and a JavaScript test that declares tests with node:test directly fails pass, node --test scripts/version/check.test.mjs prints RUN and PASS with the elapsed time for its 3 cases, and node scripts/repo/check.mjs passes. The check itself, node scripts/version/check.mjs of make version-check, printed only version: 25 declarations agree on 0.0.2 with no RUN line or deadline; it now runs as the case version with the COMPUTE deadline and prints RUN version deadline=1m0s and PASS version with the elapsed time. | — | — | [o] |
| G5.29-5 | Report the MySQL TLS tests of the four clients as cases with their own deadlines. Cause: clients/go/orm/mysql_tls_test.go never called testcase.Start, so TestParseDSNMySQLTLS and TestMySQLTLSConnection printed no RUN line and had no deadline, and the connection test read Ssl_version with QueryRow without a context; the Rust mysql_tls.rs tests started no orm_testcase case; clients/php/tests/mysql_tls.php and clients/typescript/tests/mysql_tls.mjs printed their own START and PASS lines with one 60 s alarm for the whole file instead of the shared RUN, STEP, PASS or FAIL lines. No check found a Go or Rust test without a case: every other Go and Rust test starts one. Now the Go tests call testcase.Start (Compute for the DSN parse, Database for the connections) and read through QueryRowContext with the case context; the Rust tests start orm_testcase::case!(orm_testcase::DATABASE); the PHP and TypeScript tests run the cases mysql_tls/parameters (COMPUTE) and mysql_tls/connections (DATABASE) through tests/testcase.php and tests/testcase.mjs. goTestCaseErrors and rustTestCaseErrors in scripts/repo/testcases.mjs, run by make repo-check, fail a Go Test or Fuzz function that does not call testcase.Start, Group, Run or Of (Go source inside a raw string literal, as the generator tests write, is skipped) and a Rust #[test] or #[tokio::test] function that starts no case!. Red: with the files of main, the new case every Go and Rust test starts its case with the shared testcase package of scripts/repo/check.test.mjs failed with clients/go/orm/mysql_tls_test.go: TestParseDSNMySQLTLS does not start its case and the same for TestMySQLTLSConnection, and rustTestCaseErrors on main's mysql_tls.rs returned both Rust tests; the PHP and TypeScript files of main printed only START php mysql tls: parameters, START php mysql tls: connections and PASS php mysql tls 19.979ms (TypeScript the same). Green: both repository cases pass; go test -v -timeout 0 ./clients/go/orm -run 'MySQLTLS' -count=1 prints RUN TestParseDSNMySQLTLS deadline=1m0s and RUN TestMySQLTLSConnection deadline=2m0s with their PASS lines; cargo test --test mysql_tls prints RUN and PASS for its 2 tests after a run-case build; the PHP and TypeScript files print RUN mysql_tls/parameters deadline=1m0s and RUN mysql_tls/connections deadline=2m0s with PASS and the elapsed time, all against the TLS server of make test-servers. | — | — | [o] |
| G5.29-7 | Run the decimal database tests of PHP and TypeScript as cases with their own deadlines. Cause: the six verification commands decimal-php-mysql, -postgres, -sqlite and decimal-typescript-mysql, -postgres, -sqlite of contracts/features.json (features model_queries and model_writes) ran clients/php/tests/decimal_model_db.php and clients/typescript/tests/decimal_model_db.mjs, which printed only CASE decimal_<dialect> PASS at the end: no RUN line, no step, no elapsed time and no deadline, so a stuck connection waited without end. No check found a PHP or TypeScript test that a check runs without the shared case report. Now the PHP test runs the cases decimal_db/<dialect>/generate and setters (COMPUTE) and round_trip (DATABASE) through tests/testcase.php, and the TypeScript test runs setters and round_trip through cases() of tests/testcase.mjs; scripts/decimal-physical-check.mjs, which runs both files too, requires a PASS line for each of these cases and no other result line instead of the old CASE line. reportingScriptErrors in scripts/repo/testcases.mjs, run by make repo-check, fails a file under clients/<language>/tests/ that php or node runs in a Makefile recipe, a verification command, a script under scripts/ or a root package.json script, when neither the file nor a file it requires or imports reaches tests/testcase.php or tests/testcase.mjs. Red: with the files of main, the new case every PHP and TypeScript test that a check runs reports its cases of scripts/repo/check.test.mjs failed with contracts/features.json runs clients/php/tests/decimal_model_db.php, which reports no case and the same for decimal_model_db.mjs. Green: that case and a PHP or TypeScript test without the shared case report fails pass; against the decimal databases of .runtime/decimal-env both files print RUN with the deadline and PASS with the elapsed time for each case on MySQL, PostgreSQL and SQLite (the PostgreSQL PHP round trip 1.084 s, every other case below 200 ms), and the observed function of decimal-physical-check.mjs accepts their output and rejects the TypeScript output for PHP. | — | — | [o] |
| G5.29-8 | Run the build and lint tools of the verification commands under tests/run-case.mjs. Cause: five verification commands of contracts/features.json ran a tool that reports no case, with no deadline and no RUN line: decimal-typescript-types of model_queries and of model_writes (tsc --noEmit), generation-go (go generate and git diff), chunking-rust (cargo build of the integration program) and conformance-build (go vet); the Makefile already wraps the same tools with RUN_CASE. Now they run as node tests/run-case.mjs typescript-types 5m, go-generate/model 5m --cwd clients/go/model, rust-build/integration 8m and go-vet/conformance 5m, the deadlines of TOOL_DEADLINE and BUILD_DEADLINE. unwrappedToolErrors in scripts/repo/testcases.mjs, run by make repo-check over the verification commands, fails a command segment (split at &&, ||, ;, | and parentheses outside quotes) that runs tsc, a TypeScript build, go generate, go vet, go build, cargo build, cargo check, cargo clippy or cargo test --no-run without run-case.mjs, $(RUN_CASE) or $(TSC_BUILD) before the tool. Red: the new case every build tool of a verification command runs under run-case of scripts/repo/check.test.mjs failed on main with these five commands. Green: that case and a build tool outside run-case fails pass; the four wrapped tools print RUN with the deadline and PASS (typescript-types 1.6 s, go-generate/model 15.7 s, go-vet/conformance 0.6 s, rust-build/integration 9.8 s). | — | — | [o] |
| G5.29-9 | Build the test binaries of every cargo test run as a case with a deadline before the run. Cause: cargo test compiles before it runs a test, and that compile reports no case and has no deadline; the Makefile ran it so in testcase-check, dbspec-introspect-rust-check, dbspec-apply-rust-check, dbspec-plan-rust-check, dbspec-rust-check, rust-send-savepoint-check and rust-driver-check, the verification commands dsn-rust, dsn-mysql-tls-rust, queries-rust, decimal-rust-physical, statement-events-rust, generation-rust, install-add-tables-and-columns-rust, install-register-rust, encryption-rust and catalog-rust ran it too, and so did the Rust lane of scripts/client-db-test.sh. Each run now follows a cargo test --no-run with the same arguments under $(RUN_CASE) rust-build/<target> $(BUILD_DEADLINE) or node tests/run-case.mjs rust-build/<id> 8m; catalog-rust names the workspace with --manifest-path instead of cd clients/rust. unbuiltCargoTestErrors in scripts/repo/testcases.mjs, run by make repo-check over the Makefile recipes (makeRecipes), the verification commands and the shell scripts that they run outside run-case (reachedScripts), fails a cargo test run whose arguments before -- (without --no-run and the toolchain) were not built by an earlier cargo test --no-run of the same unit; segments keeps $(...) whole. Red: the new case every cargo test run follows a run-case build of the same test binaries of scripts/repo/check.test.mjs failed on main with 26 runs (11 Makefile recipe lines, 14 verification command segments and scripts/client-db-test.sh). Green: that case and a cargo test run without its build fails pass; dsn-rust prints RUN rust-build/dsn-rust deadline=8m0s and its PASS (3.6 s) before the 13 test cases, make rust-driver-check prints RUN rust-build/rust-driver-check deadline=8m0s and PASS (29.1 s), and make -n dbspec-rust-check shows its two --no-run builds. | — | — | [o] |
| G5.29-10 | Run every TypeScript build of the checks under tests/run-case.mjs. Cause: scripts/typescript/sqlite-test.sh (the TypeScript lane of client-db-check and the end of npm run typescript:test) ran npm run typescript:build >/dev/null, a build with no deadline whose output was discarded; the root package.json script typescript:test (run by ts-check and ts-min-check) started with an unwrapped npm run typescript:build; the Makefile target conformance-result-check ran npm --prefix clients/typescript run build without RUN_CASE. The other TypeScript builds of ts-check, ts-min-check and the dbspec targets already ran through RUN_CASE or TSC_BUILD. Now the three run as typescript-build with the 5-minute TOOL_DEADLINE, and the build output appears as STEP lines. make repo-check applies unwrappedToolErrors also to the Makefile recipes, to the root package.json scripts of more than one command (a script of one command defines a tool, and its callers are checked) and to the shell scripts that the checks run outside run-case. Red: the new case every build tool of the Makefile, package.json and the scripts runs under run-case of scripts/repo/check.test.mjs failed on main with these three builds. Green: that case passes, node tests/run-case.mjs typescript-build 5m -- npm run typescript:build prints RUN typescript-build deadline=5m0s and PASS (8.5 s), and make -n conformance-result-check starts with the wrapped build. | — | — | [o] |
| G5.29-1 | Give no CI step that runs a case-reporting suite a deadline of its own. Cause: .github/workflows/ci.yml ran make check under one timeout-minutes: 90, the Go and codec steps under 30 minutes each, make test-servers under 15 and make fuzz-check under 10, and docs-pages.yml ran make docs-build, make docs-verify-idempotent and make docs-static-check under 10, 20 and 15 minutes: each is one deadline for a whole suite whose cases already carry their own. stepTimeoutErrors of scripts/repo/ci.mjs required timeout-minutes on every step and so enforced those suite deadlines. Now the rule is inverted: a step whose run text has a command segment that starts with make, go test or cargo test (reportsCases) has no timeout-minutes, and every other step (actions, installs, one tool) keeps its own. The eight suite steps lost their timeout-minutes, and the header of ci.yml states the rule. Red: the new case a suite step has no timeout-minutes and every other step has its own of scripts/repo/check.test.mjs failed on main with those eight steps. Green: that case passes, including a minimal workflow whose make check, go test and (cd clients/rust && cargo test ...) steps with timeout-minutes fail and whose tool step without one fails, and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.29-2 | Run each test once per push: remove the CI steps that ran tests again after make check, and move what make check lacked into targets of CHECK_TARGETS. Cause: after make check, ci.yml ran the step Go (go vet ./... && go test -v -timeout 0 ./...), whose go test repeats what go-test-check, client-db-check and the feature commands run, and the step codec vectors, whose go test ./clients/go/orm -run TestCodec and cargo test -p orm codec client-db-check also runs; only go vet ./... and php tests/codec/check.php ran nowhere else. make repo-check compared step lines with the exact text of feature commands and Makefile recipes, so these re-runs passed. tests/codec/check.php also passed with 0 other-language outputs when no other codec test had written its output. Now go-vet-check (scope owner, inputs **/*.go, go.mod, go.sum; go vet ./... under $(RUN_CASE) go-vet $(BUILD_DEADLINE)) and codec-check (scope suite, php tests/codec/check.php, after ts-check and client-db-check in CHECK_TARGETS) run in make check; check.php fails when tests/codec/out/go.json, rust.json or typescript.json is missing; the two CI steps are removed. ciRerunErrors in scripts/repo/ci.mjs fails a workflow that runs make check and also a test runner, judged by runnerIdentity (go test except fuzzing with -fuzz, go vet, cargo test, php <file>, node --test or node of a test file, after environment assignments), in a step or in the recipe of a make target outside CHECK_TARGETS with its variables expanded. Red: the new case the CI workflow runs no test runner outside make check of scripts/repo/check.test.mjs failed on main with go vet and go test of the step Go and go test, cargo test and php tests/codec/check.php of the step codec vectors; the check.php of main, without tests/codec/out, printed 0 other-language outputs of 80 vectors and PASS. Green: that case and a workflow that runs a test runner after make check fails by identity pass; make codec-check without the outputs fails with missing tests/codec/out/go.json (and rust.json, typescript.json), and after TestCodecVectors, the Rust codec lib tests and codec-vector.mjs it passes with 3 other-language outputs of 80 vectors; make go-vet-check passes. | — | — | [o] |
| G5.29-3 | Let make owner-check select only the verification commands and coverage parts that a changed file is an input of, and give shared helpers their own checks. Cause: scripts/features/owners.mjs selected a whole feature, every verification command and its whole coverage, when any of its fixtures or tests changed; the shared coverage runners clients/php/tests/coverage_cases.php and clients/typescript/tests/coverage_case.mjs were listed by 17 and 15 of the 18 features, so a change to either ran almost every feature, and some commands were whole suites themselves: planner-go ran go test ./engine/... (every engine package, engine/dbspec included, which schema-go also ran), and schema_definition ran every make dbspec-*-check for any of its files. Now every verification command declares inputs (the test and fixture files that it runs or reads) and every coverage part (owner/<language> or dependent/<id>) is selected by its tests and optional inputs; a declared JSON or dbspec input also selects the files that it names, one level deep (so contracts/symbols/rust.json selects the Rust coverage part for a Rust source, and source comments select nothing). The new top-level helpers of contracts/features.json declares five helpers with their own checks, and no command or part may declare a helper path: coverage-runner-php (clients/php/tests/coverage_cases_test.php, new), coverage-runner-typescript (clients/typescript/tests/coverage-case-unit.mjs, new), php-test-autoload (clients/php/tests/autoload_test.php, new), go-test-helpers (go vet -tags featurecoverage,ormtest of the two Go packages under run-case) and rust-test-helpers (cargo test --no-run of the workspace under run-case); make check runs them in feature-check. planner-go is split into planner-go-engine, -planner, -ir, -plan, -dialect and -runtimemodel, and dbspec-go-check (schema-go) runs the whole engine/dbspec package, so each engine package runs once. scripts/features/check.mjs takes --command <id> after --feature and --helper <id>, fails on a verification command without inputs, an input that matches no tracked file and a helper declared as an input (manifestInputErrors), and scripts/features/coverage.mjs takes --part; owners.mjs runs only the selected commands, parts and helpers and prints every changed path that selects none of them. AGENTS.md and docs/spec/feature-coverage.md, with their Korean pairs, state the rule. Red: with the selection code and manifest of main, 8 of the 13 cases of the rewritten scripts/features/owners.test.mjs failed (for example clients/php/tests/coverage_cases.php selected 17 features, clients/typescript/tests/coverage_case.mjs 15, and clients/go/orm/mysql_tls_test.go all of dsn_connection), and the new case selecting coverage parts requires only those parts of scripts/features/coverage.test.mjs failed. Green: owners.test.mjs passes 13 of 13 and coverage.test.mjs 15 of 15; mysql_tls_test.go selects dsn-go and dsn-mysql-tls-go only, engine/ir/operators_test.go selects planner-go-ir only, each helper path selects its helper only; check.mjs --run with the five helpers passes (the Rust test build 1 m 20 s, the others below 9 s), make dbspec-go-check and --command planner-go-ir pass, and node scripts/features/check.mjs reports 18 feature contracts passed. | — | — | [o] |
| G5.29-11 | Compare the ids and states of indented checklist sub-items between docs/checklist.md and its Korean pair. Cause: scripts/checklist/check.mjs parsed only top-level - [state] ID lines and treated every indented line after an item as continuation text, so a complete sub-item such as T27.1 or G5.29-4 was never compared: a missing or extra sub-item, a different state or an invalid state in one language passed make checklist-check. Its id pattern also rejected the -n ids of sub-items. Now an indented - [state] ID line is an item with the same id, state, duplicate and bypass checks as a top-level one, an id may contain -, and other indented lines stay continuation text. Red: the new case compares the ids and states of indented sub-items too of scripts/checklist/check.test.mjs failed: a Korean sub-item A3-2 marked complete against the English A3-2 marked in progress produced no error. Green: the case passes (state, id, invalid state and duplicate id of a sub-item are reported), the other 5 cases pass, and node scripts/checklist/check.mjs passes comparing the 48 indented sub-items of each file. | — | — | [o] |
| G5.29-12 | Run the generation check of clients/go/model once in make check. Cause: make check ran go generate ./ and git diff --exit-code in clients/go/model twice: in the target go-model-check of CHECK_TARGETS and in the verification command generation-go of model_generation, which feature-check ran first and alone because it was marked exclusive. Now generation-go runs only go test -v -timeout 0 ./generator -count=1, go-model-check stays the one generation check (the rule schema.generated of contracts/rules.json and TestCIRequiresGeneratedChecks name it), and the exclusive field and its ordering are removed from scripts/features/check.mjs, which now fails a verification field other than id, command, inputs, cwd and environment. repeatedGenerateErrors in scripts/repo/testcases.mjs, run by make repo-check over the recipes of CHECK_TARGETS and the verification commands, fails when the same directory's go generate runs more than once; generateRuns reads sh -c bodies, cd and --cwd. Red: the new case make check runs each go generate once of scripts/repo/check.test.mjs failed on the previous tree with go generate of clients/go/model runs 2 times in make check: Makefile go-model-check, contracts/features.json model_generation/generation-go. Green: that case and a go generate that make check runs twice fails pass, check.mjs --run --feature model_generation --command generation-go passes in 1 m 30 s, and node scripts/features/check.mjs passes. | — | — | [o] |
| G5.29-13 | Build the Go test binaries of every go test run as a case with a deadline before the run. Cause: go test compiles the packages and the test binary before it runs a test, and that compile reports no case and has no deadline; it ran so in 21 Makefile recipe lines (GO_TEST was go test -v -timeout 0, including the two fuzzing commands of fuzz-check), 29 verification command segments of contracts/features.json, the Go lane of scripts/client-db-test.sh and scripts/perf-test.sh. The new tests/go-test.mjs runs go test with the same packages, build tags and build flags but -run '^$' -count=1 as the case go-build/<packages> under the 8-minute BUILD_DEADLINE, with the compiler output as STEP lines and the process group ended at the deadline, and then runs go test with the given arguments; GO_TEST is now node tests/go-test.mjs -v -timeout 0, the verification commands and the two scripts call it, and fuzz-check, whose instrumented build belongs to the fuzzing run, runs each command under $(RUN_CASE) fuzz/<package> $(BUILD_DEADLINE). rawGoTestErrors in scripts/repo/testcases.mjs, run by make repo-check over the Makefile recipes with their variables expanded, the verification commands and the scripts they run outside run-case, fails a go test that runs outside tests/go-test.mjs or run-case; runnerIdentity of G5.29-2 reads through tests/run-case.mjs and tests/go-test.mjs. tests/go-test.test.mjs (in testcase-check) checks the build arguments. Red: the new case every go test runs through go-test.mjs or run-case of scripts/repo/check.test.mjs failed on the previous tree with 54 go test runs. Green: that case, a go test without its build case fails and tests/go-test.test.mjs pass; make dbspec-go-check prints RUN go-build/engine/dbspec deadline=8m0s and PASS (1.7 s) before the tests, the verification command planner-go-ir prints go-build/engine/ir, and make fuzz-check prints RUN fuzz/engine-ir deadline=8m0s and RUN fuzz/clients-go-orm deadline=8m0s with PASS (12.7 s and 42.8 s). | — | — | [o] |
| G5.31 | Send every PHP statement on PostgreSQL in one round trip, and set the PHP session time zone as a startup parameter. Cause: clients/php/src/Db.php enabled PDO::ATTR_EMULATE_PREPARES only for MySQL, so on PostgreSQL pdo_pgsql prepared each new statement text as a named statement in a round trip of its own (Parse and Sync) before Bind and Execute, and PostgreSQL runs that Parse in an implicit transaction of its own. A PHP connection lives for one request, so every statement text of every request paid that round trip and transaction; Orm::connect also sent SET TIME ZONE 'UTC' on every PostgreSQL connection, a statement without an event. Red: the new server_transactions of tests/events/vectors.json runs on a PostgreSQL case database in Go (TestStatementEventsServerTransactions), PHP and TypeScript (case server_transactions of the statement events tests) and Rust (statement_events_server_transactions). It requires the connection's TimeZone to be UTC from a startup parameter (pg_settings source client), reads the backend's local transaction number from pg_locks before and after two runs of the steps of model_statements (8 statement events, 5 statement texts) on one connection, subtracts the measured cost of the probe, and compares the transactions of a run with its events. Before the fix PHP failed: TimeZone source session, first run 13 transactions for 8 events, second run 8. TypeScript spent 8 and 8, Go 13 and 8, Rust 15 and 8. Fix: on PostgreSQL the PHP client sets Pdo\Pgsql::ATTR_DISABLE_PREPARES, so pdo_pgsql sends the statement and its binds as one unnamed statement (PQexecParams) in one round trip, and Orm::connect puts -c TimeZone=UTC in the startup options beside statement_timeout, as Go (pgx runtime parameter timezone), Rust and TypeScript (options) already do. Emulated prepares were rejected: they write the binds into the SQL text as literals, which changes the server's type inference (a quoted literal is unknown, a bare number integer or numeric) and sends bytea as escaped text. With prepares disabled the binds stay server-side parameters: pdo_pgsql gives PQexecParams the same parameter types and formats that it gave PQprepare, so the server infers the same types. One statement with bytea (with a NUL byte, PARAM_LOB), boolean (PARAM_BOOL), numeric text, null, bigint and jsonb binds returned identical values with prepares enabled and disabled, and a bytea bind without a cast failed to infer its type in both modes. Go and Rust keep their first-use prepare: pgx and sqlx describe a new statement text in a round trip of its own to encode their binds in binary by the described parameter types, and sqlx also reads the origin of new result columns from the catalog (Rust 15 = 8 events + 5 prepares + 2 catalog reads); their pool keeps the connection and its statements for later pages, so server_transactions lists only PHP and TypeScript in first_run_clients and reports the first run of Go and Rust without comparing it. docs/operations/perf.md (F4) and docs/spec/config.md (+ko) state the PostgreSQL behavior. Green: server_transactions passes in the four clients with TimeZone UTC/client and 8 transactions for 8 events in both PHP runs; php clients/php/tests/statement_events_test.php passes 25 cases, php clients/php/tests/model_test.php 66, php clients/php/tests/dsn.php 13; go run ./tests/conformance/check run -driver postgres reports 26 vectors × 4 languages identical against the recorded PostgreSQL expectations; php tests/codec/check.php passes with the Go, Rust and TypeScript codec outputs of 80 vectors; a PHP connection through PgBouncer reads TimeZone UTC and statement_timeout 5s. make owner-check with the 19 changed paths selected 10 owner make targets and the verification commands and coverage of statement_events and interface_contract, and passed all 14 steps. | — | — | [o] |
| G5.30 | Give every long operation detailed step logs and no deadline, and keep a deadline only on a test case. A long operation is a build, an install, tsc, go build, go generate, go vet, cargo build, cargo test --no-run, a whole suite, a server or a tool run; its success or failure comes from its observed result and errors. The sub-items G5.30-1 to G5.30-16 undo the deadlines that G5.29 and earlier items put on long operations, server starts and the long work inside test cases included. | — | Evidence: make owner-check over the 44 paths between the branch and the main of that run (the 30 paths of the branch and the 14 of G5.31, which main gained during the item) selected 75 verification commands and 43 coverage parts of 11 features, 2 helper checks and 12 make targets and passed all 36 steps (the longest owners/make/test-servers-check 1 m 41 s and owners/model_generation/verification 1 m 37 s); after the rebase onto G5.31, node scripts/repo/check.mjs, checklist/check.mjs, docs/rules.mjs, features/check.mjs and git/check.mjs pass, and check.mjs --run --feature statement_events and coverage.mjs --feature statement_events, the one feature that both items changed, pass. For G5.30-11 and G5.30-12, rebased onto G5.32-1, make owner-check over their 8 paths selected 7 make targets and passed all 7 (test-servers-check 52.9 s, docs-check 24.0 s with the new page cases, docs-verify-idempotent 21.9 s); package-declarations.mjs, which only ts-check of the full suite runs, passed when run directly. G5.30-14 removes the deadlines of the MySQL migration; its evidence is in G5.33. G5.30-15 removes the build deadlines of the Go checkers; its evidence is in G5.33. For G5.30-16, make owner-check over its 14 paths selected 1 verification command, 4 coverage parts of 1 feature and 7 make targets with the databases of its run, whose creation now builds through tests/go-run.mjs (go-build/bench-install and go-build/bench-seedaes), and passed all 11 steps. | [o] |
| G5.30-1 | State the timeout rule of AGENTS.md: a test case has its own timeout, and a long operation gets detailed step logs instead of a timeout. Cause: G5.29-4 took the two sentences "every test has its own timeout" and "a long operation gets detailed step logs instead of a timeout" for a contradiction, although they speak of two different things, a test case and a long operation, and rewrote the second to "detailed step logs in addition to its own timeout". That wording is wrong, and G5.29-8, -9, -10 and -13 then put deadlines on builds and tools, so a slow but healthy build fails by the clock. Now AGENTS.md and AGENTS.ko.md say that every test case, a short verification unit, has its own timeout, and that a long operation (a build, an install, tsc, go build, go generate, go vet, cargo build, cargo test --no-run, a whole suite, a server or a tool run) gets detailed step logs instead of a timeout, with no deadline, a no-output deadline included, and is judged by its observed result and errors. Red: on main grep -c 'instead of a timeout' AGENTS.md and grep -c '타임아웃 대신' AGENTS.ko.md printed 0. Green: both print 1, and neither file contains in addition to its own timeout or 자기 타임아웃에 더해. | — | — | [o] |
| G5.30-2 | Run a long operation through a step runner without a deadline. Cause: the only runner for a command that reports no case of its own was tests/run-case.mjs, which requires a deadline argument and ends the command at that deadline, so every build, install and tool run that the checks wrap had to carry a deadline, and a slow but healthy build failed by the clock. Now runLong of tests/testcase.mjs runs a long operation with no timer and prints RUN <name> no-deadline, its step lines, and PASS or FAIL with the elapsed time; runGroup shares its code. tests/run-long.mjs <name> [--cwd <dir>] -- <program> [args...] runs one command through it: each output line of the command as a STEP line as it comes, exit 0 as a STEP line before PASS, and FAIL with the exit code or signal; it accepts no deadline argument, and the command stays in the process group of the caller, so an interrupt reaches it. The Makefile declares it as RUN_LONG, and make repo-check accepts it where it accepts run-case. The new case testcase/run-long of tests/testcase.test.mjs (in testcase-check) runs a slow but healthy build that pauses 1.5 s between its output lines, a failing build and a deadline argument. Red: the same slow but healthy build under the runner of main, node tests/run-case.mjs fixture/slow-build 1s -- node -e ..., printed STEP fixture/slow-build elapsed=37ms: compiling one and FAIL fixture/slow-build elapsed=1.011s: deadline 1s exceeded and exited 1. Green: node tests/run-long.mjs fixture/slow-build -- node -e ... prints RUN fixture/slow-build no-deadline, the three output lines, STEP fixture/slow-build elapsed=1.599s: exit 0 and PASS fixture/slow-build elapsed=1.6s and exits 0; node --test tests/testcase.test.mjs passes its 7 cases (testcase/run-long 1.8 s, with the failing build reported as FAIL fixture/broken-build ...: ... exited with 3 after its error line, and 5m rejected with the usage line), and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.30-3 | Run the build and lint tools of the verification commands without a deadline. Cause: G5.29-8 wrapped the tools of the verification commands of contracts/features.json in tests/run-case.mjs with the deadlines of TOOL_DEADLINE and BUILD_DEADLINE, because the rule of G5.29-4 asked for a timeout on a long operation: decimal-typescript-types of model_queries and of model_writes (typescript-types 5m, tsc --noEmit), chunking-rust (rust-build/integration 8m, cargo build) and conformance-build (go-vet/conformance 5m); G5.29-3 gave the helper go-test-helpers the same form (go-vet/test-helpers 8m). A tool that takes longer than such a number on a cold cache or a busy machine fails although it works. Now these five commands run as node tests/run-long.mjs <name> -- ... with no deadline and show their output and exit code. Red: the real tools under the form of main with the deadline lowered to 1 s, node tests/run-case.mjs typescript-types 1s -- node clients/typescript/node_modules/typescript/bin/tsc -p clients/typescript/tsconfig.json --noEmit and node tests/run-case.mjs go-vet/conformance 1s -- go vet ./tests/conformance/check ./tests/conformance/runner_go, printed FAIL typescript-types elapsed=1.017s: deadline 1s exceeded and FAIL go-vet/conformance elapsed=1.018s: deadline 1s exceeded and exited 1. Green: the same commands under tests/run-long.mjs print RUN ... no-deadline, STEP typescript-types elapsed=1.459s: exit 0 and PASS typescript-types elapsed=1.46s, and STEP go-vet/conformance elapsed=3.168s: exit 0 and PASS go-vet/conformance elapsed=3.168s, and exit 0; node scripts/features/build.mjs --check passes. | — | — | [o] |
| G5.30-4 | Build the test binaries of every cargo test run without a deadline. Cause: G5.29-9 ran the cargo test --no-run build before each cargo test run under $(RUN_CASE) rust-build/<target> $(BUILD_DEADLINE) or node tests/run-case.mjs rust-build/<id> 8m, following the wrong timeout rule of G5.29-4: in the Makefile targets testcase-check, dbspec-introspect-rust-check, dbspec-apply-rust-check, dbspec-plan-rust-check, dbspec-rust-check (two builds), rust-send-savepoint-check and rust-driver-check, in 11 verification commands of contracts/features.json (14 builds), in the helper rust-test-helpers and in the Rust lane of scripts/client-db-test.sh; the timing target built orm-schema-tests the same way. A Rust build that runs longer than 8 minutes, as a clean build of every dependency can on a busy machine, failed although it compiled. Now each of these builds runs as $(RUN_LONG) rust-build/<target> or node tests/run-long.mjs rust-build/<id> with the compiler output as step lines and no deadline; scripts/client-db-test.sh runs its cargo build of the integration program the same way. The order stays: the build, then the cargo test run whose cases have their own deadlines. Red: a slow but healthy build (a cargo on PATH that prints compiler lines over 1.5 s and exits 0) under the form of main with the deadline lowered to 1 s, node tests/run-case.mjs rust-build/dsn-rust 1s -- cargo test --no-run ..., printed FAIL rust-build/dsn-rust elapsed=1.003s: deadline 1s exceeded after its first compiler line and exited 1. Green: node tests/run-long.mjs rust-build/dsn-rust -- cargo test --no-run ... with the same fixture prints the three compiler lines, STEP rust-build/dsn-rust elapsed=1.514s: exit 0 and PASS rust-build/dsn-rust elapsed=1.514s and exits 0; make -n testcase-check shows node tests/run-long.mjs rust-build/testcase-check --cwd clients/rust -- cargo +1.98.1 test --no-run ..., and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.30-5 | Run every TypeScript build and type check of the checks without a deadline. Cause: G5.29-10 wrapped three TypeScript builds in tests/run-case.mjs with the 5-minute TOOL_DEADLINE, following the wrong timeout rule of G5.29-4: scripts/typescript/sqlite-test.sh, the root package.json script typescript:test and the Makefile target conformance-result-check. The other TypeScript builds of the Makefile already ran under RUN_CASE with a deadline: TSC_BUILD (the dbspec targets), the builds of feature-check, case-database-check (BUILD_DEADLINE) and timing-check, the target typescript-build, and the type check of ts-check (typescript-check). A build slower than its number failed although it compiled. Now all of them run as $(RUN_LONG) typescript-build or typescript-check, or node tests/run-long.mjs typescript-build, with the build output and exit code as step lines and no deadline. Red: a slow but healthy build (an npm on PATH that prints the npm and tsc lines and finishes after 1.5 s with exit 0) under the form of main with the deadline lowered to 1 s, node tests/run-case.mjs typescript-build 1s -- npm run typescript:build, printed its two lines and FAIL typescript-build elapsed=1.006s: deadline 1s exceeded and exited 1. Green: node tests/run-long.mjs typescript-build -- npm run typescript:build with the same fixture prints build finished, STEP typescript-build elapsed=1.533s: exit 0 and PASS typescript-build elapsed=1.534s and exits 0; make -n conformance-result-check starts with node tests/run-long.mjs typescript-build -- npm run typescript:build, and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.30-6 | Build the Go test binaries of every go test run without a deadline. Cause: G5.29-13 made tests/go-test.mjs run the build before each go test as a case go-build/<packages> under an 8-minute BUILD_DEADLINE that ended the build's process group, and ran each command of fuzz-check under $(RUN_CASE) fuzz/<package> $(BUILD_DEADLINE), following the wrong timeout rule of G5.29-4; timing-check built its Go test binary the same way (go-build/dbspec-test). A Go build slower than 8 minutes, as a build with an empty cache on a busy machine can be, failed although it compiled. Now the build of tests/go-test.mjs runs through runLong as RUN go-build/<packages> no-deadline with the compiler output and exit 0 as step lines; a failing build reports its error lines and exit code, and the run of the tests, whose cases keep their own deadlines, starts only after a passing build. The commands of fuzz-check, whose length -fuzztime=1s sets, and the build of timing-check run under $(RUN_LONG). The new case a slow but healthy go test build runs to its end without a deadline of tests/go-test.test.mjs (in testcase-check) puts a go on PATH whose build step pauses 1.5 s between its lines. Red: with that go, the tests/go-test.mjs of main with its BUILD_DEADLINE lowered to 1 s printed STEP go-build/engine elapsed=342ms: # orm/engine and FAIL go-build/engine elapsed=1.005s: deadline 1s exceeded and exited 1 without running the tests. Green: node tests/go-test.mjs -v -timeout 0 ./engine -count=1 with the same go prints RUN go-build/engine no-deadline, both build lines, STEP go-build/engine elapsed=1.517s: exit 0, PASS go-build/engine elapsed=1.517s and then the test lines, and exits 0; node --test tests/go-test.test.mjs passes its 2 cases (the new one 2.5 s, with a failing build reported as FAIL go-build/engine ...: go test build exited with 2 after its error line and no test run), make -n fuzz-check shows both commands under tests/run-long.mjs, and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.30-7 | Remove RUN_CASE, BUILD_DEADLINE and TOOL_DEADLINE from the Makefile, and fail every deadline on a long operation in make repo-check. Cause: the Makefile defined RUN_CASE (tests/run-case.mjs) with BUILD_DEADLINE = 8m and TOOL_DEADLINE = 5m and ran 21 more recipe lines under them: the Rust example, release and stress builds of the dbspec targets, example-check and timing-check, the integration build of case-database-check, cargo check and the four cargo clippy runs of rust-check and rust-send-savepoint-check, cargo fmt, cargo fetch (rust-fetch), gofmt (go-fmt-check), go vet ./... (go-vet-check), the generation of go-model-check, package-check, rust-150-check and php-without-mysql-check. Each is a build, an install or a tool run, which a slow but healthy run fails at the deadline. scripts/package-check.sh ran go test ./... with the default 10-minute deadline of the test binary. The repository rules enforced the deadlines: unwrappedToolErrors, unbuiltCargoTestErrors and rawGoTestErrors of scripts/repo/testcases.mjs required run-case, a runner with a deadline. Now the 21 lines run under $(RUN_LONG), RUN_CASE, BUILD_DEADLINE, TOOL_DEADLINE and tests/run-case.mjs (with its case in tests/testcase.test.mjs) are removed, and package-check.sh runs go test -timeout 0 ./.... The rules are inverted: unwrappedToolErrors fails a build or lint tool that runs outside tests/run-long.mjs or under a deadline form; unbuiltCargoTestErrors fails a cargo test --no-run build outside run-long or under a deadline, and a cargo test run without that build, now also in the root package.json scripts; rawGoTestErrors fails a go test outside tests/go-test.mjs or run-long, or under a deadline; and the new longDeadlineErrors, run over the Makefile recipes with their variables expanded, the verification commands, the root package.json scripts and every script that they reach, run-long included, fails each segment that puts a deadline on a long operation: tests/run-case.mjs, $(RUN_CASE), $(BUILD_DEADLINE), $(TOOL_DEADLINE), timeout <n>, and a go test run with a -timeout other than 0 or none (a -c build is not a run). runnerIdentity reads through run-long only. Red: the new rules over the tree before this change failed node scripts/repo/check.mjs with 40 errors, the 21 Makefile lines and scripts/package-check.sh; copied into a checkout of main, they failed with 130 errors in 73 segments that put a deadline on a long operation, and the new repository cases every build tool of a verification command runs under run-long without a deadline, every cargo test run follows a run-long build of the same test binaries, every build tool of the Makefile, package.json and the scripts runs under run-long, every go test runs through go-test.mjs or run-long and no check puts a deadline on a long operation failed there. Green: those cases and a build tool outside run-long or under a deadline fails, a cargo test run without its build, or a build under a deadline, fails, a go test without its build step, or under a deadline, fails and a deadline on a long operation fails pass; node --test scripts/repo/check.test.mjs passes its 50 cases, node scripts/repo/check.mjs passes, node --test tests/testcase.test.mjs passes its 6 cases, and make go-fmt-check prints RUN go-fmt no-deadline, STEP go-fmt elapsed=266ms: exit 0 and PASS go-fmt elapsed=266ms. | — | — | [o] |
| G5.30-8 | Give no CI step or job a deadline. Cause: G5.29-1 removed timeout-minutes from the steps that run make or a test runner, but made it mandatory for every other step: stepTimeoutErrors of scripts/repo/ci.mjs failed a step without its own timeout-minutes, so ci.yml kept 14 step deadlines (the checkout and setup actions, the Node, Rust, PHP and database server installs, the PHP and environment steps and the interface check) and docs-pages.yml 6 (checkout, setup-node, npm ci, the Playwright install, the upload and the deploy). Each of these steps is a long operation: an install, a server program setup or a tool run, which a slow mirror or a busy runner makes fail at the deadline although it would succeed. Now no step or job of either workflow has timeout-minutes; the runner logs each step line by line and judges it by its exit code. stepTimeoutErrors is inverted: it fails every step with timeout-minutes and a workflow with timeout-minutes outside its steps (a job deadline); reportsCases, which only served the old split, is removed, and the header of ci.yml states the rule. Red: the new rule over the workflows of the previous tree failed node scripts/repo/check.mjs with 20 errors (14 steps of ci.yml, 6 of docs-pages.yml), and the new case no workflow step or job has timeout-minutes of scripts/repo/check.test.mjs failed. Green: that case passes, including a minimal workflow whose checkout, install and make check steps with timeout-minutes fail and whose job deadline fails; grep -c timeout-minutes finds only the two comment lines of ci.yml and none in docs-pages.yml; node --test scripts/repo/check.test.mjs passes its 50 cases and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.30-9 | Run the builds of the feature coverage checker without a deadline. Cause: buildNative of scripts/features/coverage.mjs ran each build before the coverage runs (the state reader, test2json, go test -c per Go package and cargo test --no-run per Rust workspace, with cargo locate-project) as a case under COVERAGE_BUILD_DEADLINE, 8 minutes, the number of the Makefile's BUILD_DEADLINE, and its run ended the build process at that time with timeout after <n> ms. A build slower than that failed every coverage run that used it, although it compiled. Now each build runs through runLong as RUN features/coverage/build/<name> no-deadline with the compiler output and exit 0 as step lines, run sets no timer when its timeoutMs is null, and executeCoverage takes no build deadline; COVERAGE_BUILD_DEADLINE is removed. The coverage runs keep their own deadline per process (DATABASE). The new case a slow but healthy coverage build runs to its end without a deadline of scripts/features/coverage.test.mjs (in feature-unit-check) puts a go on PATH that waits 1.5 s before it runs the real go. Red: with that go and the build deadline of the code before this change lowered to 1 s, executeCoverage printed FAIL features/coverage/build/go/test2json elapsed=1.103s: timeout after 1000 ms and FAIL features/coverage/build/go/clients/go/sample elapsed=1.001s: timeout after 1000 ms and returned go test2json build failed: timeout after 1000 ms and exactly two executions required. Green: the same probe prints RUN features/coverage/build/go/test2json no-deadline, STEP ... elapsed=2.605s: exit 0, PASS features/coverage/build/go/test2json elapsed=2.605s, the same for the sample package (2.33 s), and both runs PASS with no error; the new case passes in 5.1 s and every Go execution runs the test instead of reading the test cache passes. | — | — | [o] |
| G5.30-10 | Build the documentation without a deadline. Cause: scripts/docs/build.mjs (make docs-build) ran the Mermaid rendering of prepare.mjs and the VitePress build as a case under the 5-minute PROCESS deadline and aborted both processes at that time, and scripts/docs/idempotent.mjs (make docs-verify-idempotent) ran its two builds under 10 minutes. Both are builds, which a slow but healthy run fails at the deadline. Now both run through runLong as RUN docs-build no-deadline and RUN docs-idempotent no-deadline, with the output of each process and its exit 0 as step lines, and judge the result by the exit codes and the byte comparison. Red: scripts/docs/build.mjs and scripts/docs/idempotent.mjs of main with the deadline lowered to 1 s printed FAIL docs-build elapsed=1.002s: The operation was aborted and FAIL docs-idempotent elapsed=1.004s: The operation was aborted and exited 1. Green: node scripts/docs/build.mjs prints RUN docs-build no-deadline, the build lines, exit 0 for both processes and PASS docs-build elapsed=22.327s and exits 0, and node scripts/docs/idempotent.mjs prints RUN docs-idempotent no-deadline, two builds produced identical bytes in 179 files and PASS docs-idempotent elapsed=24.818s and exits 0. | — | — | [o] |
| G5.30-11 | Start the test servers without a deadline and judge readiness by the server's ready line. Cause: start_logged of scripts/test-servers.sh, which starts ProxySQL and PgBouncer, took a deadline per server (60 s, from G5.13) and a timer failed the start when no ready line came within it; PostgreSQL and its replica started with pg_ctl start -w and stopped with pg_ctl stop -w, which wait at most 60 s (PGCTLTIMEOUT) and offer no setting without a limit. A server that starts slowly but correctly, on a busy machine or with a large data directory, failed the whole make test-servers. Now start_logged <name> <line> <command>... has no deadline: it runs the server in the foreground, copies each output line to $DIR/<name>.log, shows each line up to the ready line on stderr as test-servers: <name>: <line>, returns when the server logs its ready line, and fails with the last lines of the log when the server exits first; the wait ends on one of these two events. PostgreSQL and its replica run through start_logged as postgres -D ... with the ready lines database system is ready to accept connections and database system is ready to accept read-only connections, and stop through stop_pid with INT, the fast shutdown, which waits for the process to exit; postgres joins the checked tools. The running test servers were not restarted. The repository case start_logged waits for a ready line or an exit, with no deadline of scripts/repo/check.test.mjs (in test-servers-check and repo-check) replaces the deadline case: it rejects a timer in start_logged and pg_ctl ... -w outside comments, and runs a fixture server that is ready at once, one that logs its ready line only after 2.5 s, and one that exits first. Red: that case failed on the script of main with start_logged has a timer, and the start_logged of main with the deadline 1 s failed the fixture server that logs now accepting connections after 1.5 s with test-servers: fake logged no line with 'accepting connections' within its deadline of 1 s and exit 1. Green: the case passes (2.8 s); the new start_logged starts the same fixture, prints test-servers: fake: starting and test-servers: fake: now accepting connections, and returns 0; a throwaway PostgreSQL 17 on ports 55432 and 55433 started through the new start_logged with the arguments of the script printed its log lines up to database system is ready to accept connections, its pg_basebackup replica up to database system is ready to accept read-only connections, served a row written on the primary from the replica, and both stopped through stop_pid ... INT. | — | — | [o] |
| G5.30-12 | Take the long work out of two test cases. Cause: scripts/docs/check.mjs (make docs-static-check, and the end of make docs-check) ran the whole smoke test, the start of a local server, the launch of Chromium, every page, the links, search, theme and mobile navigation, as one case docs-static under the 5-minute PROCESS deadline, so a slow server or browser start counted against the same clock as the page checks, and a stuck page was reported only as the whole case. clients/typescript/tests/package-declarations.mjs ran npm pack, the install into a temporary project and tsc inside the case package-declarations/type-check-without-driver-types under 120 s; those calls were synchronous, so the deadline could not stop them (the case passed after 2.41 s under a deadline lowered to 1 s), and the file ignored the result of runCase, so a failed case still exited 0. Now docs-static/server and docs-static/browser run through runLong with a step line each and no deadline, and each page is its own case docs-static/page/<path> (30 s), followed by docs-static/links (1 minute), docs-static/search and docs-static/theme-and-mobile (2 minutes each); each case also fails on the browser errors raised during it. In package-declarations.mjs, package-declarations/install (npm pack, tar and the copies) and package-declarations/tsc run through runLong with their output and exit codes as step lines, the case package-declarations/type-check-without-driver-types (1 minute) checks the exit code and output of tsc, and a failed case sets exit code 1. Red: scripts/docs/check.mjs of main with its deadline lowered to 1 s printed FAIL docs-static elapsed=6.001s: deadline 1s exceeded; package-declarations.mjs of main printed PASS package-declarations/type-check-without-driver-types elapsed=2.41s under a 1 s deadline, and with its tsc assertion inverted it printed FAIL for that case and exited 0. Green: the new scripts/docs/check.mjs over the built site prints RUN docs-static/server no-deadline and RUN docs-static/browser no-deadline with their step lines, passes 48 page cases (the longest 268 ms) and the links, search (579 ms) and theme and mobile (1.211 s) cases, and exits 0; the new package-declarations.mjs prints RUN package-declarations/install no-deadline and RUN package-declarations/tsc no-deadline with exit 0 step lines, passes both cases and exits 0, and with its tsc assertion inverted it prints FAIL and 1 case(s) failed and exits 1; node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.30-13 | Wait for a test server to stop by the exit event of its process, not by polling. Cause: stop_pid of scripts/test-servers.sh, which stops PgBouncer, ProxySQL and, since G5.30-11, both PostgreSQL servers, sent the signal and then checked kill -0 every 0.1 s until the process was gone; that loop polls although the operating system reports the exit of a process as an event. Now stop_pid runs the new scripts/stop-process.py <signal> <pid>, which registers a watch on the process first (kqueue EVFILT_PROC NOTE_EXIT on macOS, a pidfd from os.pidfd_open with poll on Linux), then sends the signal, and returns when the exit event arrives; a process that is already gone at registration is reported and counts as stopped. The wait has no deadline and no loop. python3 joins the checked tools of make test-servers. The running test servers were not restarted. The new repository case stop_pid waits for the exit event of the server, with no polling of scripts/repo/check.test.mjs rejects sleep, while, until and kill -0 in stop_pid, and stops a fixture server that exits at once and one that takes 1.5 s after TERM, each started with its TERM trap in place and announced through a FIFO. Red: on the script of main the case failed with stop_pid polls for the exit. Green: the case passes (1.6 s): both fixtures are gone when stop_pid returns, the slow one not before 1.5 s; node --test scripts/repo/check.test.mjs passes its 51 cases and node scripts/repo/check.mjs passes; a throwaway PostgreSQL 17 on port 55432, started through start_logged, stopped through stop_pid ... INT with database system is shut down as the last line of its log and its process gone. The Linux path (pidfd) has not run on the development machine; it runs in make test-servers and repo-check on the Linux runner. | — | — | [o] |
| G5.30-13-1 | Write the process exit watcher of the test servers in Go. Cause: G5.30-13 wrote scripts/stop-process.py in Python, a language outside the repository's tooling languages Go, PHP, Rust and TypeScript, so every machine and runner that stops the test servers depended on a Python runtime, and make test-servers checked for python3; no check rejected a tool in another language, and the helper was an input of no owner check, so a change to it selected only repo-check through its **. Now tests/stop-process is a Go command on golang.org/x/sys/unix, already a direct dependency in go.mod: main.go registers the watch, sends the signal and waits, watch_darwin.go uses kqueue EVFILT_PROC NOTE_EXIT and watch_linux.go unix.PidfdOpen with unix.Poll, both without a timeout. The Makefile builds it as $(RUN_LONG) go-build/stop-process into .runtime/bin/stop-process before test-servers, test-servers-stop and test-servers-check, and passes the path as STOP_PROCESS, which stop_pid runs and requires; python3 left the checked tools and scripts/stop-process.py is removed. The stop_pid case moved to the new scripts/test-servers.test.mjs, which test-servers-check runs, and it also requires that stop_pid runs STOP_PROCESS; tests/stop-process/** and scripts/test-servers.test.mjs are inputs of test-servers-check in contracts/check-inputs.json. toolingLanguageErrors of scripts/repo/scripts.mjs, run by make repo-check, fails a tracked program in Python, Ruby, Perl, Lua, Java, Kotlin, C#, Swift, Scala or R. Red: on the tree of main, toolingLanguageErrors returned scripts/stop-process.py is a program in a language outside Go, PHP, Rust and TypeScript, and the stop_pid case over the script of main failed with stop_pid does not run the Go program STOP_PROCESS of tests/stop-process. Green: make test-servers-check prints RUN go-build/stop-process no-deadline, STEP go-build/stop-process elapsed=713ms: exit 0 and its PASS, and passes its 6 cases, the stop_pid case with the fast and the 1.5 s fixture servers in 2.1 s; GOOS=linux GOARCH=arm64 go build ./tests/stop-process under run-long passes, and GOOS=linux go vet and gofmt -l report nothing; node --test scripts/repo/check.test.mjs passes its 51 cases and node scripts/repo/check.mjs passes; a throwaway PostgreSQL 17 on port 55432 stopped through stop_pid ... INT with the Go program, its log ending in database system is shut down and its process gone. The Linux watcher runs for real in make test-servers and test-servers-check of GitHub CI on push. | — | — | [o] |
| G5.30-14 | Run the MySQL migration of the test servers without deadlines. Cause: scripts/test-servers-mysql.mjs ran each migration step as a case under a deadline (preflight 1 minute, dump 30 minutes, stop 5, keep 1, start 5, restore 30, verify 10, record 1), and verify waited for the replica with SOURCE_POS_WAIT(file, position, 600), so a migration of a large data directory on a busy machine failed by the clock in the middle of moving the data. Now each step runs through runLong as RUN mysql-migration/<step> no-deadline with its step lines and is judged by the exit codes of its commands and the counts it compares, and SOURCE_POS_WAIT has no timeout; it still fails when the function returns NULL or a negative value. Red: the migration case a MySQL setting other than the declared one migrates the data with equal counts, then nothing of scripts/test-servers-mysql.test.mjs, which now requires a RUN mysql-migration/<step> no-deadline line for each step, failed on the script of the previous commit, whose output had RUN mysql-migration/dump deadline=30m0s and RUN mysql-migration/verify deadline=10m0s. Green: the same case passes with this script (47.4 s on a throwaway MySQL primary and replica on ports 39301 and 39302). | — | — | [o] |
| G5.30-15 | Build the runners of the Go conformance and interface checkers without deadlines. Cause: tests/conformance/check built its Rust runner, the TypeScript client and its Go runner as one case conformance/build under the sum of rustBuildDeadline (8 minutes), typescriptBuildDeadline (2 minutes) and goBuildDeadline (3 minutes), each command under its own timeout, and tests/interfaces/check built its Rust symbol extractor as the case interfaces/rust-build under rustBuildDeadline (testcase.Process, 5 minutes) with cargo build --quiet, so its progress was not logged; a slow but healthy build failed by the clock. Now internal/testcase has RunLong, which reports RUN <name> no-deadline, its steps and PASS or FAIL with the elapsed time and sets no timer; conformance/build and interfaces/rust-build run through it, runCommand sets no timeout for the builds (timeout 0), the build deadlines are removed, and the extractor build shows the cargo output as step lines. The runner and language cases keep their own deadlines. The report fixture of internal/testcase (in testcase-check) adds a 1.5 s long operation and a failing one. Red: go run ./tests/interfaces/check --self-test of main printed RUN interfaces/rust-build deadline=5m0s. Green: the same command here printed RUN interfaces/rust-build no-deadline, the run cargo build ..., Compiling orm-interface-symbols and Finished step lines and PASS interfaces/rust-build elapsed=1.645s (its PHP and TypeScript cases failed in this worktree, which has no PHP vendor directory or TypeScript build); go test -run TestReportForm ./internal/testcase passes with RUN runner/long no-deadline and PASS runner/long elapsed=1.5...s. | — | — | [o] |
| G5.30-16 | Log the build of every go run. Cause: go run compiles a program before it runs it and prints nothing while it compiles, so 14 go run calls built without a step log: conformance-check and interface-check of the Makefile, the verification command interface-self-test, the six calls of scripts/bench-db.sh (which every run's database creation runs), the two of scripts/decimal-bench-sqlite.sh, and the three of the CI step common state and execution contracts. Now tests/go-run.mjs <name> <package> [args...] builds the package with go build -o <temporary file> as the long operation go-build/<name> (runLong: no deadline, compiler output and exit 0 as step lines), runs the program with the arguments and its exit code, and removes the file; all 14 calls use it, and runnerIdentity does not count it as a test runner. goRunErrors of make repo-check fails a go run in the Makefile recipes, the verification commands, the root package.json scripts, every script they reach and the workflow steps. Red: over the files of the previous commit goRunErrors reported the 14 calls. Green: the new case a go run, whose build has no step log, fails and the updated case for feature commands of scripts/repo/check.test.mjs pass (58 cases), node scripts/repo/check.mjs passes, and make interface-check prints RUN go-build/interfaces-check no-deadline, STEP go-build/interfaces-check elapsed=659ms: exit 0 and PASS go-build/interfaces-check elapsed=659ms before its cases, which all pass. | — | — | [o] |
| G5.30-16-1 | Make the interface guard test require the go-run.mjs form. Cause: G5.30-16 changed the verification command interface-self-test to node tests/go-run.mjs interfaces-check ./tests/interfaces/check --self-test, but TestCIRequiresGeneratedChecks of tests/interfaces/check/prohibited_test.go still required the text go run ./tests/interfaces/check --self-test in contracts/features.json; the test belongs to go-test-check, a suite target that the scoped make owner-check of G5.30-16 did not select, and the full run of G5.32-9 failed it. Now interfaceCommand is the new command, so the guard and its mutation require the form that builds with step logs. Red: go test -run TestCIRequiresGeneratedChecks ./tests/interfaces/check failed with prohibited_test.go:89: contracts/features.json must verify go run ./tests/interfaces/check --self-test. Green: the same test passes. make owner-check over its 5 paths passed 1 verification command, 4 coverage parts of interface_contract and 7 make targets. | — | — | [o] |
| G5.32 | Let every client send a PostgreSQL statement with typed binds in one round trip, with no describe or separate prepare. Cause: the planners keep the dbspec type of a bind slot only for date, time, datetime and decimal (bindType of engine/planner/planner.go and its counterparts) and drop it for every other slot, so a plan does not say what it binds: Rust asks the server for the parameter types (pg_describe through sqlx Executor::prepare, which also reads the origin of new result columns from pg_attribute) and Go prepared each statement text in a round trip of its own. In server_transactions of tests/events/vectors.json (G5.31) the first run of model_statements spent 15 transactions for 8 events in Rust and 13 in Go, against 8 in PHP and TypeScript. The sub-items G5.32-1 to G5.32-8 correct it: every bind slot of the four planners has its dbspec type, Rust binds with those types and asks the server nothing, a connection sends no probe statement, and the raw SQL API whose slots had no type is removed. Go keeps the pgx statement cache (G5.32-5), and Rust keeps the sqlx prepare round trip recorded in docs/operations/perf.md. | — | — | [o] |
| G5.32-1 | Send Go PostgreSQL statements unprepared in the pgx query mode exec. Cause: DB.stmt and txConn.stmt of clients/go/orm prepared every new statement text with database/sql PrepareContext, and pgx sends that prepare (Parse, Describe, Sync) in a round trip and implicit transaction of its own before the execution. Change: the PostgreSQL DSN that the client builds sets default_query_exec_mode=exec, and on PostgreSQL both return an unprepared statement that runs QueryContext, QueryRowContext or ExecContext of the pool or transaction connection with the text; pgx sends the statement with its binds in text format as one unnamed statement without parameter types, so the server infers them as for PHP and TypeScript. MySQL and SQLite keep the prepared statement cache. server_transactions lists go in first_run_clients. docs/operations/perf.md (F1) and docs/spec/config.md (+ko, max_prepared_statements) state it. Red: with the vector change and the previous client, TestStatementEventsServerTransactions failed with 13 transactions for 8 events in the first run (second run 8). Green: 8 and 8; TestParseDSN expects default_query_exec_mode=exec in the two PostgreSQL native DSNs; node tests/go-test.mjs ./clients/go/orm ./clients/go/orm/pg passes every test on MySQL, PostgreSQL and SQLite; go run ./tests/conformance/check run -driver postgres reports 26 vectors × 4 languages identical against the recorded expectations, so the text result format of the exec mode decodes to the same values; php tests/codec/check.php passes with the Go, Rust and TypeScript codec outputs of 80 vectors; go run ./tests/interfaces/check --record --language go recorded the private symbols modelStatement and unprepared and the changed DB.stmt and executor in contracts/symbols/go.json and its hash in contracts/interfaces.json, and go run ./tests/interfaces/check --self-test passes for the four languages; make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-2 | Give every bind slot of the four planners its dbspec type. Cause: bindType of engine/planner/planner.go (and bind_type, bindType of the Rust, TypeScript and PHP planners) kept the type of the compared or assigned column only for date, time, datetime and decimal, and the other slots carried no type, so a plan did not say what it binds. Change: col_type of every slot is the dbspec type of the value the placeholder takes: the column's type for a value compared with or assigned to a column, including contains, plus and minus assignments and the optimistic check (a host-styled value is the stored value); the input type of a SQL-side style function (HEX bytes, INET6_ATON text); the result type of a compared column function; i32, or f64 for seconds, for the interval of a relative time function; the blind index column's type for its plaintext; text for a secret; the AES version column's type for config; the audit column's type for audit; and datetime for now. A parent slot carries key_types, the types of the compared child key columns. A slot without a type fails with IR_INVALID; only the placeholders of raw fragments stay untyped (rawParam, raw_param), until G5.32-3 removes raw fragments. The plan fields are BindSlot.ColType and the new KeyTypes (Go), col_type and key_types (Rust, TypeScript, PHP). The planner coverage of contracts/fixtures/planner.json now compares every slot's source, param number and type instead of the param numbers, and four new cases cover the slot kinds: planner_bind_types_select (contains, between, decimal, blind index, days_ago, year, an ip column whose MySQL slot is text and PostgreSQL and SQLite slot bytes), planner_bind_types_update (plus, AES with blind index and version, optimistic), planner_bind_types_insert (AES, version and the SQLite now slots) and planner_parent_key_types (a composite key relation). docs/spec/dialects.md (+ko) states the rule. Red: with the previous Go planner and the new fixture, -tags featurecoverage TestCoveragePlanner* failed 8 of the 10 cases (every case that compiles; the two error cases passed), since the slots carried no type. Green: the ten planner cases pass in Go (-tags featurecoverage TestCoveragePlanner*), PHP (coverage_planner.php), TypeScript (coverage_planner.mjs) and Rust (engine::coverage_planner, 10 passed) with the same slots on MySQL, PostgreSQL and SQLite; go test ./engine/... passes; conformance on MySQL, PostgreSQL and SQLite reports 26 vectors × 4 languages identical; the client tests pass with the typed slots: Go ./clients/go/orm ./clients/go/orm/pg ./clients/go/model, PHP model_test.php 66, TypeScript model.mjs 57 and the Rust workspace 216; the symbol snapshots of the four languages and their hashes are recorded and go run ./tests/interfaces/check --self-test passes. make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-3 | Bind every Rust PostgreSQL value with the type its plan slot declares and stop asking the server for types. Cause: pg_describe of clients/rust/orm/src/driver.rs prepared every new statement text with sqlx Executor::prepare to learn its parameter types, cached per connection by pg_types_pool and pg_types_conn of db.rs; that prepare is a round trip and transaction of its own, and sqlx prepare_with also reads the origin of every new result column from pg_catalog.pg_attribute, so the first model_statements run of server_transactions spent 15 server transactions for 8 events (8 + 5 prepares + 2 catalog reads). Change: pg_describe, pg_types_pool, pg_types_conn and the pg_types cache are removed; arg_types gives each bound value the dbspec type of its slot (a parent slot repeats its key_types over the expanded values) and bind_pg binds it as that type's Rust value, whose PostgreSQL type sqlx declares in the Parse (i16 int2 … datetime timestamp, time time, uuid through the new 16-byte PgUuid, a NULL without a slot type as an unspecified parameter); the INET, JSON/JSONB and TIMESTAMPTZ branches, which no dbspec type reaches, are removed. The statements of Utils declare their bind types: text for keys, names and values, and for the AES rotation the AES, version and key column types of the entity schema. A raw fragment placeholder takes the type of its value until raw fragments are removed. sqlx still prepares a new statement text in a round trip of its own before its first execution on a connection, so server_transactions lists rust in the new first_run_prepare_clients, whose first run must spend one transaction per event plus one per statement text. docs/spec/dialects.md (+ko) states the binding. Red: with the previous executor and the new expectation, statement_events_server_transactions failed with 15 transactions for 8 events of 5 statement texts, want 13. Green: 13, second run 8; cargo test --workspace passes 216 tests; the Rust parts of every feature coverage (node scripts/features/coverage.mjs --feature <id> --part owner/rust, 18 features on MySQL, PostgreSQL and SQLite) pass after the Rust symbol snapshot and its hash are recorded; conformance on PostgreSQL reports 26 vectors × 4 languages identical. make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-4 | Connect without a probe statement: read the SQLite version from the driver and drop the TypeScript SELECT 1. Cause: a connection asked the server what the client could know without a statement. Every client checked the SQLite minimum 3.46 with SELECT sqlite_version() on each new SQLite connection (Go clients/go/orm/db.go:341, PHP clients/php/src/Orm.php:75, Rust clients/rust/orm/src/db.rs:416, TypeScript clients/typescript/src/driver.ts:653), and the TypeScript client sent SELECT 1 on every connect to check the connection (clients/typescript/src/database.ts:406). No other statement on the runtime paths of the four clients (dbspec introspection and apply excluded) reads metadata: no model run publishes one; the Utils state APIs (exists, installed, empty, inspectTable) and Go BackendWaitingForLock ask for database state that no manifest holds and stay; the per-connection prepares of the pooled clients stay by G5.32-5. Change: the version comes from the library that the driver links: the Go SQLite driver package registers modernc.org/sqlite/lib.SQLITE_VERSION with the new orm.RegisterSQLiteVersion, PHP reads PDO::ATTR_SERVER_VERSION, TypeScript process.versions.sqlite and Rust libsqlite3_sys::SQLITE_VERSION of the SQLite that sqlx bundles; the TypeScript connect opens one connection of the pool and returns it without a statement (reserve and release), and the unused unobserved is removed. The new rule connectProbeErrors of scripts/repo/probes.mjs, run by make repo-check over the client runtime sources, fails on SQL sqlite_version() and a TypeScript 'SELECT 1'. docs/spec/dsl.md (+ko) states it. Red: the rule over the previous sources reported the five lines above. Green: make repo-check and its case a client connects without a probe statement pass; node tests/go-test.mjs ./clients/go/orm ./clients/go/orm/pg ./clients/go/orm/sqlite ./clients/go/model, TypeScript dsn.mjs 16 and model.mjs 57, PHP dsn.php and model_test.php 66, and the Rust workspace 216 pass on MySQL, PostgreSQL and SQLite; the Go symbol snapshot and its hash are recorded and the interface self-test passes. make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-4-1 | Record the libsqlite3-sys dependency of orm in bench/rust/Cargo.lock. Cause: G5.32-4 added libsqlite3-sys to clients/rust/orm/Cargo.toml. clients/rust/Cargo.lock already listed it under orm, but the bench workspace, which uses the orm crate through its path, has a lock file of its own that did not, so cargo fetch --locked --manifest-path bench/rust/Cargo.toml stopped with "cannot update the lock file … because --locked was passed", and make rust-fetch and every target that depends on it failed. Change: cargo update --workspace --offline --manifest-path bench/rust/Cargo.toml updates only the entries of the workspace members, adding libsqlite3-sys (0.37.0, already locked) to the dependencies of orm and changing no version. Red: make rust-fetch failed with that error. Green: make rust-fetch passes, and make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-5 | Return the Go PostgreSQL client to the pgx statement cache (revert of G5.32-1). Cause: G5.32-1 compared first runs only. A Go pool keeps its connections beyond a request, so each connection prepares a statement text once and then reuses it with the server plan; only PHP, whose connection lives for one request, runs every statement on a new connection. Measured on PostgreSQL with the bench database and a page of GetBySeq, GetsByServiceSeqAndIsClose, GetCountByServiceSeq and a 10-row list (pool of 4 connections, 4 workers, 800 to 1600 pages after 200 warm-up pages, three rounds): with the pgx exec query mode of G5.32-1 the steady-state p50 was 24 to 44 % slower (7.8 to 8.9 ms against 5.5 to 6.3 ms), the backend CPU per page 22 to 31 % higher (2.9 to 3.0 ms against 2.2 to 2.5 ms) and the throughput 20 to 33 % lower (386 to 466 against 572 to 697 pages per second); only the first page on a new connection was 0 to 9 % faster. The same comparison on MySQL (driver-side interpolation against server-side prepares) stayed within the noise of the loaded machine, so the MySQL clients keep their server-side prepares and no MySQL change lands. Change: the client DSN no longer sets default_query_exec_mode=exec, and DB.stmt and txConn.stmt prepare on every database again (modelStatement and unprepared are removed). The criterion of server_transactions is rewritten: a short-lived connection (PHP) sends each statement in one round trip, and a pooled client prepares a statement text once per connection and reuses it; first_run_clients lists PHP and TypeScript (pg sends the Parse with the first Bind and Execute), first_run_prepare_clients lists Go and Rust, whose first run spends one transaction per event plus one per statement text, and the Go test asserts it. docs/operations/perf.md (F1, +ko) records the rule, the first-run and second-run counts of every client (Go 13 / 8, Rust 13 / 8, TypeScript 8 / 8, PHP 8 / 8) and the measurements; docs/spec/config.md (+ko) names the Go client among the clients with named prepared statements again. Red: the measurements above. Green: TestStatementEventsServerTransactions passes with 13 and 8, TestParseDSN with the native DSNs without the query mode, node tests/go-test.mjs ./clients/go/orm ./clients/go/orm/pg ./clients/go/model passes on MySQL, PostgreSQL and SQLite, the Go symbol snapshot and its hash are recorded, and make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-6 | Make the slow statements of the timeout and cancellation tests wait for a row lock that another connection holds. Cause: the tests made a statement slow with a raw condition: pg_sleep, SLEEP, a self-join of MD5 values on MySQL or a recursive CTE on SQLite (Go clients/go/orm/cancel_test.go TestWithContextCancels and timezone_test.go TestStatementTimeout, TestStatementTimeoutThroughAPooler; PHP clients/php/tests/model_test.php statement timeout and pooler cases; TypeScript clients/typescript/tests/model.mjs statementTimeout, pooler and cancellation; Rust clients/rust/orm/tests/zone.rs statement_timeout, statement_timeout_through_a_pooler, dropping_a_query_cancels_it, dropping_a_transaction_frees_a_single_connection). Those conditions use the raw API that G5.32 removes and functions that no neutral expression has. Change: a test opens another connection that updates every row of the table in an open transaction (Go holdRows, PHP holdRows and, for the pooler case, holdRowsFor in another process, TypeScript holdRows, Rust HeldRows); the bounded statement is a locking read in a transaction (lockedRead, locked_read), which MySQL bounds with max_execution_time and PostgreSQL with statement_timeout, and the cancelled statement is an update of a held row. On MySQL and PostgreSQL the Go cancellation waits until the server reports the statement waiting for the lock. SQLite waits for a lock in its busy handler, which a cancelled context does not interrupt, so Go cancels the SQLite context before the statement, as the TypeScript test already did. The pooler cases keep the rows held for a declared 400 ms, past the 200 ms bound, to show that the plain connection is not bounded. internal/testdb.Open opens a SQLite DSN with the SQLite driver. | — | Evidence: the converted tests pass: Go ./clients/go/orm (all tests on MySQL, PostgreSQL and SQLite), PHP model_test.php 66, TypeScript model.mjs 57 and Rust --test zone 11; none of them uses the raw API or a sleep function. make owner-check with the changed paths passes. | [o] |
| G5.32-7 | Add negated condition groups, the query extension not of the neutral expression grammar. Cause: the conditions that raw SQL served are written with the typed DSL, whose element set is the dbspec check predicate set (comparison, in, is null, and, or, grouping, columns and typed values) plus between and the fixed column and value functions; it had no not, so a negated condition needed raw SQL. Change: the IR group has not (Go ir.Group.Not, PHP, Rust and TypeScript not, the IRGroup record of contracts/interfaces.json); a nested group with not renders NOT (…) in the four planners, and a top-level where or on group with not is IR_INVALID. The models have not(fn), andNot(fn) and orNot(fn) (Rust not, and_not, or_not): Go through the generator template and Core.AddNegatedGroup, PHP and TypeScript in the base model, Rust in fixed.rs.txt and Core::add_negated_group; the four generators reserve the names and the column name not. docs/spec/dsl.md (+ko) adds section 2.1.1, the query expression grammar, which names its relation to the dbspec check grammar and its three query-only extensions, and docs/spec/plan.md, usage.md, interfaces.md and interface-implementation.md (+ko) name the negated groups. The planner fixture adds planner_not_group and planner_rejects_top_not, and the conformance vector expression_forms runs andNot, a leading not and orNot in the four runners; its expectations are recorded on MySQL, PostgreSQL and SQLite from the runs whose other 26 vectors matched their recorded expectations. Red: the previous Go engine rejected the planner_not_group request with IR_INVALID: json: unknown field "not", and the runners did not build without the new methods. Green: the twelve planner cases pass in Go, PHP, TypeScript and Rust, expression_forms gives the same output in the four clients on the three databases (PostgreSQL: 428, 3 rows, 2000), the generated Go models are regenerated, the symbol snapshots of the four languages and their hashes are recorded and the interface self-test passes. make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-7-1 | Key the Go plan cache on the negation of a group. Cause: G5.32-7 added not to the IR group, and the Go client keys its plan cache by walking the IR fields (shape.go); the walk did not feed Group.Not, so a negated group and the same group without not had one key and the second request reused the first plan. TestShapeCoversEveryField catches a field the walk misses and failed after G5.32-7. Change: shape.group feeds g.Not. PHP, TypeScript and Rust key the cache by the serialized IR, which carries not. Red: go test ./clients/go/orm -run TestShapeCoversEveryField failed with .Query.Relations[0].Query.Relations[0].Query.Where.Not: same shape key as .Query.Relations[0].Query.Relations[0].Query.Where! and the same for On.Not. Green: the test passes. make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-8 | Remove the raw SQL API from the four clients. Cause: the raw entry points carried SQL text into the IR (expr of a predicate, an output column, an order item and an assignment, and group_by_expr), and the planners bound the ? of that text as slots without a type (rawParam), so a plan could not say what every slot binds and the Rust executor bound those values by their own type (value_type, UntypedNull). The raw API is removed; the typed DSL with the query expression grammar (G5.32-7) writes the conditions. Change: the IR has no SQL text: Go ir.Expr, ir.GroupExpr, Query.GroupByExpr, Columns.Expr, Pred.Expr, Order.Expr and Assign.Expr/Ps are removed, so a request with those fields is IR_INVALID (unknown field), and the PHP Validator records, TypeScript ir.ts, Rust ir.rs and the IRExpr and IRGroupExpr records and fields of contracts/interfaces.json follow. The four planners lose renderExpr, the placeholder filling (fillPlaceholders, fill) and the untyped rawParam/raw_param slot, and the dialects lose CurrentTime and the $CURRENT_TIME token that only fragments used. The clients lose raw, andRaw, orRaw, orderByRaw, groupByRaw, setRaw<Col>, addRawColumn<Alias> and the format string of addColumn<Col>Alias<Name>, which now takes a column function only (Go orm.Func, PHP Func, TypeScript ColumnFunction, Rust orm::Func), with their core functions (Go Core.Raw, AddColumnFormat, AddRawColumn, OrderByRaw, GroupByRaw, SetRaw; PHP Request::raw; TypeScript Core.raw, addRawColumn, addColumnFormat, now addColumnFunc; Rust Core::raw, add_column_format, add_raw_column, order_by_raw, group_by_raw, set_raw, add_column_as, ColumnFormat, IntoColumnFormat, Binds and args::bind). The four generators neither emit nor reserve those names, and raw is no longer a reserved column name; the Go and TypeScript models are regenerated. The Rust executor binds a PostgreSQL value only as its slot's type: value_type and UntypedNull are removed. The columns conformance vector, which added a format column, now adds addColumnStartDtAliasStartYear(year()); it showed that the Rust client could not read a MySQL YEAR cell, which YEAR(col) returns (sqlx's i64 does not accept YEAR), so row.rs decodes YEAR with year_cell without that type check. Tests: the engine adds TestRawFormsRejected (predicate, column, order, group_by_expr and assignment forms are IR_INVALID), TestRandomOrder and TestBinaryContains replace the two raw cases, and the group_by_expr cases of TestAggregates become a two-column group_by; TestJoinAliasNamespaces names its columns with add. The client tests write geReadCount(20), seq(x) and orderBy<Col>Asc() for their raw conditions and orders and drop the raw and format columns of the columns-and-subqueries cases (Go clients/go/model/model_test.go, PHP model_test.php, TypeScript model.mjs, coverage_audit_triggers.mjs, dbspec_runtime_db.mjs, restore_case.mjs, schema-set.mjs, Rust tests/src/integration.rs, orm-build/tests/generate.rs). The conformance vector raw_forms and its four runner blocks are removed, columns is recorded again on MySQL, PostgreSQL and SQLite, and check run|compare|record take -vector NAME (tests/conformance/README.md, +ko). contracts/features.json drops the input raw_sets. docs/spec/dsl.md, protocol.md (which also gains the not of a group that G5.32-7 left out), dialects.md, plan.md and usage.md (+ko) drop the raw forms. Red: TestRawFormsRejected against the engine of main failed with "kind":"all","entity":"place","where":{"items":[{"pred":{"expr":"{price} > ?","ps":[0]}}]}: want IR_INVALID, got <nil>, and before the YEAR decode the Rust runner failed columns on MySQL with mismatched types; Rust type core::option::Option<i64> (as SQL type BIGINT) is not compatible with SQL type YEAR. Green: go test ./engine/..., go test ./generator/, the Go planner and shape cases (TestCoveragePlanner*, TestShapeCoversEveryField), the twelve planner cases in PHP, TypeScript and Rust (coverage_planner) and the Rust orm-build generate tests pass; go run ./tests/conformance/check run -vector columns gives 1 vector × 4 languages identical on the three databases; the symbol snapshots of the four languages and their hashes are recorded, the component page is generated and go run ./tests/interfaces/check --self-test passes. make owner-check with the changed paths passes. | — | — | [o] |
| G5.32-9 | Compare the TimeZone source of server_transactions only on a direct connection. Cause: G5.31 made the case require pg_settings source client for TimeZone, to catch a client that sets the time zone with a SET statement outside the statement events. Through PgBouncer (client-pooler-check) the client still sends TimeZone as a startup parameter, but the pooler applies the startup parameters of its clients to its shared server connection with SET, so the source is session whatever the client does. The requirement is that the connection runs in UTC without a statement per request: the setting and the transaction counts show it on both paths, and the source shows the startup parameter only where the client reaches the server itself. PgBouncer cannot pass a client parameter to a server connection as a startup parameter (track_extra_parameters only adds parameters it applies with SET), so the test servers stay as they are. Change: the case reads the path from the environment (ORM_TEST_POSTGRES_DSN differs from ORM_TEST_POSTGRES_SERVER_DSN through a pooler) and compares the source only on a direct connection, in Go (TestStatementEventsServerTransactions), PHP (statement_events_run.php), TypeScript (statement-events-run.mjs) and Rust (statement_events_server_transactions); the note of server_transactions in tests/events/vectors.json says so. Red: through ORM_TEST_PGBOUNCER_DSN the Go case failed with TimeZone setting and source = UTC, session, want UTC, client (13 and 8 transactions, as on the direct path), and the PHP and TypeScript cases on main failed with TimeZone setting and source = [["UTC","session"]]. Green: the four cases pass on the direct path and through PgBouncer with the same counts (Go 13 and 8, PHP and TypeScript 8 and 8, Rust passes). make owner-check with the changed paths passes. | — | — | [o] |
| G5.33 | Keep runs from different checkouts and sessions from breaking each other's shared test resources. A resource that many runs use at once is leased: users hold a shared lease, and an operation that destroys or rebuilds it holds the exclusive lease, which is refused while any lease is held, naming the holders; a resource that one run can use alone has one holder; a dead holder's lease is reported and removed only explicitly. Everything else becomes per run. The sub-items G5.33-1 to G5.33-11 cover the test servers, the bench and decimal databases, the shared Rust target directory with its test binaries and builds outside the Makefile, the remaining run files, the TypeScript build output and its readers, and a race of the lease program. | — | Evidence: make owner-check over the 32 paths of the branch, rebased onto G5.32-3, selected 10 make targets; 9 passed (test-servers-check 1 m 29 s, docs-verify-idempotent 55 s) and repo-check failed on the stale BENCH_SQLITE_DSN expectation, which the second G5.33-2 commit corrects; make repo-check then passes its 54 cases. Evidence of the second round (G5.30-14 and G5.33-5 to G5.33-10): make owner-check over the 22 paths of the branch, rebased onto G5.32-5, selected 72 verification commands and 35 coverage parts of 9 features, 1 helper check and 11 make targets with the databases of its run, and passed all 32 steps (the longest authenticated_encryption/verification 2 m 5 s); a first run stopped at dsn-mysql-tls-rust on the lease race that G5.33-10 corrects, a second one stopped when the disk fell below 3 GB. After the rebase onto G5.32-4, which also changed scripts/repo/check.mjs, node --test scripts/repo/check.test.mjs passes its 56 cases. The shared bench and decimal resources that no run uses since G5.33-2 were removed from the running servers after checking that no connection or open file used them: the MySQL and PostgreSQL databases orm_bench (134 MB and 181 MB) and orm_decimal_case (the replicas applied the drops), .runtime/servers/orm_bench.sqlite (109 MB), .runtime/servers/send-savepoint.sqlite, .runtime/decimal-env, .runtime/decimal-case.sqlite and .runtime/decimal-owned. orm_bench.sqlite had last been written at 11:33, before G5.33-2 reached main at 11:46 and before the environment file of the servers was rewritten at 11:47, while that file still named it in BENCH_SQLITE_DSN; no orm path names it now, and the header of scripts/test-servers.sh, which still described the shared bench database, was corrected. Evidence of the third round (G5.30-15, G5.33-8 and G5.33-9): make owner-check over the 28 paths of the branch, rebased onto G5.32-4-1, selected 2 verification commands and 6 coverage parts of 3 features and 10 make targets with the databases of its run, and passed all 18 steps (owners/make/testcase-check 1 m 7 s, owners/interface_contract/coverage 1 m 5 s). G5.33-11 gives each run's environment its send-savepoint SQLite file; make owner-check over the 7 paths of G5.33-11 and G5.34-1 passed its 7 make targets. | [o] |
| G5.33-1 | Lease the test servers. Cause: the servers of make test-servers (.runtime/servers on the fixed ports 33171, 55471, 33181, 55481, 33182 and 55482) are used by the runs of every checkout through TEST_ENV and by other tools, but nothing recorded who used them: make test-servers-stop stopped them and removed .runtime/servers, and the MySQL migration of scripts/test-servers-mysql.mjs inside make test-servers stopped MySQL and moved its data, while other runs were in the middle of their cases. Now the new Go command tests/lease (.runtime/bin/lease, built by the Makefile as $(RUN_LONG) go-build/lease) keeps the leases of a directory as files written atomically (rename for shared, link for exclusive) with the holder's checkout, process id, start time and command; hold keeps a lease until a given process exits, observed through internal/procevent (the exit watch of G5.30-13-1, moved there, and a directory watch: kqueue on darwin, inotify on linux), run keeps one while a command runs and with --wait waits on directory change events instead of refusing, list names the holders and clear-dead removes the leases whose process is gone and names them; a dead lease is never taken over, and --wait refuses at once when one blocks. scripts/test-servers.sh writes ORM_TEST_SERVERS_LEASES (.runtime/servers.leases) into the environment file; WITH_TEST_ENV, which every make line that reads TEST_ENV starts with (and now make check and make owner-check), holds a shared lease for the life of its line, and fails naming the fix when the environment file has no lease directory; stop, a fresh start and the MySQL migration (before its first change, also when resumed) hold the exclusive lease and fail with exit code 3 naming each holder. make test-servers-leases and make test-servers-leases-clear list the leases and remove the dead ones. CONTRIBUTING.md and its Korean pair describe the leases. The running servers were not restarted; their environment file gets the lease directory on the next make test-servers, which keeps the running servers. Red: the new cases over the scripts of main: stopping the test servers is refused while another run holds a lease, naming the holder of scripts/test-servers.test.mjs failed because the stop printed test-servers: removed .../.runtime/servers while the lease was held, and the MySQL migration is refused while another run holds a lease of the servers of scripts/test-servers-mysql.test.mjs failed because the migration of a throwaway MySQL fixture ran all its steps; a make line of main's Makefile that read TEST_ENV and slept 6 s left an exclusive lease free (exclusive-ran-during-main-run). Green: both cases pass (113 ms; 15.2 s with the MySQL fixture on ports 39305 and 39306, its marker still lower_case_table_names=2 and no .runtime/mysql-migration), the stop case also stops the servers once the holder ends; during the same make line of this Makefile, lease run <servers.leases> exclusive was refused with exit 3 naming shared lease of pid ... (running) from .../orm-shared-resources-G5.33 since ...: /bin/sh -c ... sleep 6, and the lease was gone after the line; make dialect-facts-check passes with a lease, and an environment file without the lease directory fails with names no lease directory; run make test-servers to rewrite it; node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-2 | Give every run its own bench and decimal databases. Cause: make test-servers seeded one bench database for all runs (orm_bench on MySQL and PostgreSQL and .runtime/servers/orm_bench.sqlite) and its environment file named it in BENCH_* and ORM_BENCH_MYSQL_DSN; the Makefile named one decimal database (DECIMAL_ENV = .runtime/decimal-env, orm_decimal_case, set up by make decimal-db-setup). Only make check made its own (scripts/check/run.mjs); a target run directly, such as make rust-driver-check or make perf-check, and make owner-check, which passed the shared DECIMAL_ENV and server environment to its targets and commands (scripts/features/owners.mjs), wrote or read the shared databases while other runs and scripts/bench-db.sh dropped and recreated them. Now no bench or decimal database is shared: the environment file of make test-servers exports only ORM_RUN_MYSQL_DSN, ORM_RUN_POSTGRES_DSN (database orm_run, a name that scripts/check/databases.sh replaces) and ORM_RUN_SQLITE_QUERY, and the start seeds no bench database; databases.sh builds the per-run DSNs from them; DECIMAL_ENV has no default and decimal-db-setup is removed; make owner-check creates the databases of its run with databases.sh when it runs a feature, a helper or a make target that reads the server environment (usesTestEnv), loads their environment file (environmentFile) into its commands, passes them as TEST_ENV and DECIMAL_ENV to its targets, and drops them at the end; the new make run-databases TARGETS="..." runs targets directly with databases of their own through scripts/check/run.mjs. A target that reads them fails when it runs with the server environment alone (ORM_BENCH_MYSQL_DSN is required ...; decimal-physical-check names make run-databases). CONTRIBUTING.md, tests/conformance/README.md and their Korean pairs describe it. The running servers keep their old orm_bench database untouched; their environment file gets the ORM_RUN_* variables on the next make test-servers. Red: the new case the server environment names no bench or decimal database that runs share of scripts/test-servers.test.mjs failed on the script and Makefile of main with the server environment names a shared bench database. Green: that case and owner-check creates the databases of its run for a target that reads the server environment of scripts/features/owners.test.mjs pass; make run-databases TARGETS=perf-check with an environment file that has the ORM_RUN_* variables created and seeded orm_check_99833_ec8f23fe_bench and _decimal (2 m 12 s), passed perf-check (23.3 s) on them and dropped them (1.5 s); the repository case the CI workflow provides every variable of make test-servers expects ORM_RUN_MYSQL_DSN, ORM_RUN_POSTGRES_DSN, ORM_RUN_SQLITE_QUERY and ORM_TEST_SERVERS_LEASES instead of BENCH_SQLITE_DSN, which make owner-check found still listed; make perf-check with that environment alone fails with ORM_BENCH_MYSQL_DSN is required; it names the seeded bench database, and bench tests never skip; node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-3 | Run the Rust programs of a make target from copies of its own, and build into the shared Rust target directory under a lease. Cause: every checkout builds into the one CARGO_TARGET_DIR (the disk is tight, so it stays shared), and six make targets built a program there and then ran it from there: dbspec-apply-pairs-check (debug/examples/dbspec_apply), dbspec-introspect-compare-check (dbspec_introspect), dbspec-compare-check (dbspec_compare, dbspec_stress, dbspec_apply, through tests/cargo-target.mjs), dbspec-stress-bench and timing-check (release/examples/dbspec_stress), example-check (debug/complex, debug/demo) and case-database-check (debug/integration); between the build and the run another checkout could rebuild the program, so the run executed the other checkout's code. The stress documents were written into the target directory with > ($(CARGO_TARGET_DIR)/dbspec/stress*.dbs), where two runs overwrote each other's. Now RUN_DIR (.runtime/run/<target>-<make pid>) holds the files of one target run and RUN_TARGET copies of its programs in the layout of the target directory; CARGO_COPY runs the build and scripts/cargo-build-copy.sh copies the named artifacts into RUN_TARGET, both under the exclusive lease of $(CARGO_TARGET_DIR)/.leases (tests/lease run --wait, which waits on directory change events while another build holds it); the runs use the copies (DBSPEC_APPLY_RUST, DBSPEC_INTROSPECT_RUST, EXAMPLE_RUST_*, TIMING_RUST_STRESS, and CARGO_TARGET_DIR=$(RUN_TARGET) for the scripts that locate programs through it); the stress documents are written into RUN_DIR, which each target removes at its end; and every other cargo build, check or clippy run of the Makefile holds the same lease (CARGO_LEASED), so no build falls between a build and its copy. make lease-tool builds the lease program first (rust-fetch, case-database-check and rust-check depend on it). sharedTargetErrors of scripts/repo/testcases.mjs, run by make repo-check, fails a Makefile line outside comments that uses a path under $(CARGO_TARGET_DIR)/ (except the lease directory) and a cargo build without $(CARGO_LEASED) or $(CARGO_COPY). Red: over the Makefile of main, sharedTargetErrors returned 31 errors, the 9 paths under the target directory (DBSPEC_STRESS_DOCUMENT = $(CARGO_TARGET_DIR)/dbspec/stress.dbs first) and 22 builds without the lease. Green: the new case the Makefile runs no program from the shared Rust target directory and builds there under its lease of scripts/repo/check.test.mjs passes; make dbspec-compare-check with the target directory of the main checkout, while another process held the exclusive lease of its .leases for 8 s, printed lease: waiting for the exclusive lease of .../clients/rust/target.leases; held by: ... before its first build, copied debug/examples/dbspec_compare, dbspec_stress and dbspec_apply into its run directory, passed all 168 cases on those copies, and left no .runtime/run directory; after the lease directory moved into the target directory, the same target passed again with no failure; node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-4 | Keep the remaining run files per run, and give the TypeScript build output of a checkout one holder. Cause: SEND_SQLITE_DSN named send-savepoint.sqlite next to TEST_ENV (.runtime/servers/ for every direct run) and WITH_TEST_ENV exported it to every line; timing-check built its Go test binary into .runtime/timing/dbspec.test; two runs used the same file. The TypeScript client's build output (clients/typescript/dist and the generated models) is rebuilt by 18 make targets, so a run could execute it while another run of the same checkout rewrote it. Now SEND_SQLITE_DSN is $(RUN_DIR)/send-savepoint.sqlite, which only rust-send-savepoint-check passes as ORM_SEND_SQLITE_DSN and removes at its end, and TIMING_GO_DBSPEC_TEST is $(RUN_DIR)/dbspec.test; every make target that builds the TypeScript client starts with $(HOLD_TYPESCRIPT), which holds the exclusive lease of .runtime/typescript.leases for the make process (tests/lease hold --wait --pid, new: it waits on directory change events, and a process that already holds the exclusive lease keeps it). make repo-check fails a Makefile line that names a fixed .runtime file or a file next to TEST_ENV other than the server environment, the tools, the run directories and the lease directories (runtimePathErrors), and a target that builds the TypeScript client without starting with $(HOLD_TYPESCRIPT) (typescriptHolderErrors); clients/rust/README.md and its Korean pair describe the SQLite file. Red: over the Makefile of main, runtimePathErrors returned SEND_SQLITE_DSN, DECIMAL_ENV and TIMING_GO_DBSPEC_TEST, and typescriptHolderErrors returned 18 targets. Green: the new case the files of one run live in its run directory and a TypeScript build holds its output of scripts/repo/check.test.mjs passes; of two make typescript-build runs started 1.5 s apart in one checkout, the second printed lease: waiting for the exclusive lease of .../.runtime/typescript.leases; held by: ... and built only after the first ended (19.4 s and 10.7 s); make rust-send-savepoint-check passed its 3 cases twice with the SQLite file of its run directory and left no .runtime/run directory; node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-5 | Run every cargo test from a copy of its test binaries. Cause: after G5.33-3 each cargo test run still followed its cargo test --no-run build and then let cargo execute the test binaries of the shared target directory, in 11 Makefile lines (testcase-check, the dbspec Rust targets, rust-send-savepoint-check, rust-driver-check), 14 runs of 11 verification commands of contracts/features.json and the Rust lane of scripts/client-db-test.sh; a build from another checkout between the two, or cargo's own fingerprint, which does not tell checkouts apart (G5.22-1), made the run execute another checkout's tests. Now tests/cargo-test.mjs <name> -- cargo [+toolchain] test ... builds with the same options as the long operation rust-build/<name> (cargo test --no-run --message-format=json-render-diagnostics under lease run <CARGO_LEASES> exclusive --wait, compiler output as step lines, no deadline), copies every test binary that cargo reports into .runtime/run/cargo-test-<pid>-<random> inside that lease, then runs each copy in its package directory with CARGO_MANIFEST_DIR, the test name filters and the arguments after --, stops at the first failing binary with its exit code, and removes the copies; the Rust sources have no runnable doctest (their code blocks are text). The Makefile (CARGO_TEST, with LEASE and CARGO_LEASES exported), the verification commands and client-db-test.sh run all cargo test runs through it, and their separate --no-run builds are gone. unbuiltCargoTestErrors of make repo-check now fails a cargo test run outside tests/cargo-test.mjs; tests/cargo-test.test.mjs (in testcase-check) checks the argument split and the reading of cargo messages. Red: with a fixture crate whose test fails unless it runs from /.runtime/run/, cargo test --test probe failed with ran the test binary of the shared target directory: /tmp/orm-ct.bjSa/target/debug/deps/probe-d379e64323e50d0a; over the tree before this change the new rule reported the 14 verification command runs and scripts/client-db-test.sh. Green: node tests/cargo-test.mjs probe -- cargo test --test probe -- --nocapture on the same crate printed RUN rust-build/probe no-deadline, the copy line, cargo-test: running probe (.../.runtime/run/cargo-test-13938-e6c8d95e/0-probe-...), CARGO_MANIFEST_DIR /private/tmp/orm-ct.VkhR/crate and test result: ok. 1 passed, exited 0 and left no run directory; node --test tests/cargo-test.test.mjs passes its 2 cases, node --test scripts/repo/check.test.mjs its 53 and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-6 | Build into the shared Rust target directory under its lease outside the Makefile too. Cause: G5.33-3 leased the builds of the Makefile only; the helper rust-test-helpers and the command chunking-rust of contracts/features.json, the coverage builds of scripts/features/coverage.mjs (cargo test --no-run --workspace, whose test binaries it then ran from the target directory), the integration build of scripts/client-db-test.sh, and the cargo check runs of scripts/package-check.sh and scripts/check-rust-150.sh built without it, and chunking-rust and client-db-test.sh ran ${CARGO_TARGET_DIR}/debug/integration from the target directory. Now each runs under "$LEASE" run "$CARGO_LEASES" exclusive --wait -- (the Makefile exports both, and package-check and rust-150-check depend on lease-tool); chunking-rust and client-db-test.sh build through scripts/cargo-build-copy.sh into .runtime/run/... and run that copy, which they remove; coverage.mjs builds through tests/cargo-test.mjs --copy, which also copies each binary's dep-info, and runs the copies in its own temporary directory. Copies are copy-on-write clones where the file system has them (COPYFILE_FICLONE, cp -c on macOS, cp --reflink=auto on Linux): 500 MB cloned three times took no disk space, a plain copy took 500 MB. unleasedCargoErrors of make repo-check fails a cargo build without the lease and a run of a program under CARGO_TARGET_DIR in the verification commands, the root package.json scripts and every script they reach. Red: over the tree before this change it returned 7 errors: the builds of chunking-rust, rust-test-helpers, client-db-test.sh, package-check.sh and check-rust-150.sh, and the target-directory runs of chunking-rust and client-db-test.sh. Green: the new case commands outside the Makefile build into the shared Rust target directory only under its lease passes; coverage.mjs --feature dsn_connection --part owner/rust with the databases of its run passed its build (1 m 59 s, 77 test binaries copied) and both runs on MySQL, PostgreSQL and SQLite from the copies; check.mjs --run --feature parameter_chunking --command chunking-rust copied debug/integration into its run directory and passed (26.5 s); node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-7 | Hold the TypeScript build output while reading it too. Cause: G5.33-4 gave the targets that build the TypeScript client the exclusive lease of .runtime/typescript.leases, but decimal-physical-check, which runs clients/typescript/tests/decimal_model_db.mjs on the client's dist, read it without a lease, conformance-check built it (npm run build inside tests/conformance/check run) without one, and the feature steps of make owner-check ran TypeScript tests and coverage on it without one, so another run of the checkout could rewrite it under them. A holder also blocked its own children: feature-check holds the exclusive lease and its verification commands run make dbspec-ts-check and make dbspec-compare-check, which ask for it again in another make process. Now READ_TYPESCRIPT holds a shared lease for the make process of a reading target (readers run together, and a building target waits until they end), decimal-physical-check starts with it and conformance-check with HOLD_TYPESCRIPT; scripts/features/owners.mjs holds the exclusive lease from its first helper or feature step to its end, because some of those steps build the output; and tests/lease treats a lease that the process or one of its ancestors already holds exclusively as held, found through ps -o ppid=. make repo-check fails a target that runs, through node, a file that imports the client's dist directly, through a relative import or by running a clients/typescript file on a line with node, without starting with $(READ_TYPESCRIPT) or $(HOLD_TYPESCRIPT) (typescriptReaderErrors), and typescriptHolderErrors counts tests/conformance/check run as a build. Red: over the Makefile of the previous commit the two rules reported decimal-physical-check and conformance-check; with the lease program of the previous commit, a shell holding the exclusive lease saw its child refused (lease: refused the exclusive lease of ...; held by: exclusive lease of pid 1805 (running) ...). Green: the new case a target that runs code using the TypeScript build output holds it passes; with a probe Makefile that includes this one, a reading target started 1.5 s after a building target that sleeps 4 s printed lease: waiting for the shared lease of .../.runtime/typescript.leases and ran after 3 s, and a building target whose recipe runs another building target in a sub-make printed inner-ran-under-outer-hold without waiting; node --test scripts/repo/check.test.mjs and node scripts/repo/check.mjs pass. | — | — | [o] |
| G5.33-8 | Build the Rust programs of the Go checkers under the target lease and run copies. Cause: tests/conformance/check (conformance/build of run, and the physical tests TestPhysicalResultRunners and TestPhysicalRustGroupBoolean) ran cargo build ... --bin conformance without the lease of the shared Rust target directory and then ran $CARGO_TARGET_DIR/debug/conformance, and tests/interfaces/check built orm-interface-symbols the same way and ran the executable cargo reported in the target directory; rust_tool_test.go built that extractor into a temporary target inside a one-minute test case. Besides, scripts/check/run.mjs passed no CARGO_TARGET_DIR to its sub-makes, so make run-databases from a worktree built into the worktree's own target directory. Now buildRustRunner and buildRust run lease run <CARGO_LEASES> exclusive --wait -- sh scripts/cargo-build-copy.sh <run dir> <artifact> -- cargo build ... and run the copy (rustRunner, the extractor in a temporary directory removed at the end); the two physical tests build through testcase.RunLong into the test's directory; rust_tool_test.go is removed, since interface-check runs that build; interface-check, conformance-rust-group-check and conformance-result-physical-check depend on lease-tool, and the last also holds the TypeScript build output; run.mjs passes its CARGO_TARGET_DIR on the make command line. goCargoErrors of make repo-check fails a Go file that runs cargo without the lease and the copy script. Red: over the files of the previous commit, goCargoErrors reported tests/conformance/check/main.go, rust_group_physical_test.go and tests/interfaces/check/main.go; the first make run-databases TARGETS=conformance-rust-group-check from this worktree built 957 MB into the worktree's clients/rust/target. Green: make interface-check with the target directory of main printed cargo-build-copy: .../orm/clients/rust/target/debug/orm-interface-symbols copied to /var/folders/... and passed all its cases; make run-databases TARGETS=conformance-rust-group-check copied .../orm/clients/rust/target/debug/conformance (PASS conformance/rust-build elapsed=39.87s), passed TestPhysicalRustGroupBoolean on MySQL, PostgreSQL and SQLite from the copy, and left no target directory in the worktree; node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-9 | Give Rust tests the paths of package programs when they run. Cause: 12 lines in 7 Rust tests (bench/rust/tests/dsn.rs and seeded.rs, and client_bench_args.rs, client_bench_dsn.rs, coverage_conformance_verification.rs, coverage_performance_gate.rs and example_dsn.rs of clients/rust/tests) took the program they run from env!("CARGO_BIN_EXE_<name>"), which cargo resolves at compile time to a path in the shared target directory; even a test binary copied by tests/cargo-test.mjs (G5.33-5) still ran a program that another checkout could rebuild there. Now orm_testcase::program(name) reads ORM_PROGRAM_<NAME> when the test runs and panics naming the variable when it is unset; tests/cargo-test.mjs --copy also copies the package programs that cargo builds with the tests (programs) into the run directory under the same lease and passes them as ORM_PROGRAM_<NAME> (programEnvironment), and scripts/features/coverage.mjs gives the same variables to the test binaries it runs. binExeErrors of make repo-check fails a Rust line outside comments that uses env!("CARGO_BIN_EXE_. The bench Rust tests could not run on this tree: cargo fetch --locked --manifest-path bench/rust/Cargo.toml fails on main as well (cannot update the lock file), so rust-fetch fails. Red: over the files of the previous commit binExeErrors reported the 12 lines; after the change, a plain cargo test --test client_bench_args fails with ORM_PROGRAM_CLIENT_BENCH is unset; run the test through tests/cargo-test.mjs, which gives the copied program. Green: node tests/cargo-test.mjs programs-probe -- cargo test ... --test client_bench_args --test example_dsn with the target directory of main copied client_bench, complex, demo and the other package programs into the run directory and passed both tests' 4 cases from the copies; the new cases of tests/cargo-test.test.mjs and scripts/repo/check.test.mjs pass, and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.33-10 | Tell a lease that has just been released from a dead one. Cause: when tests/lease was refused, it read the blocking leases first and asked whether their processes were alive afterwards; a holder that removed its file and exited in between was reported as a dead lease, which --wait refuses at once, and a new holder that had already created exclusive.json again was mistaken for the old one. During make owner-check, dsn-mysql-tls-rust failed so with exclusive lease of pid 64521 (dead) ... a dead lease is never taken over while dsn-rust had just released the target lease. Now a refusal checks again, after the liveness check, that the blocking file still holds the same holder (process id and start time); a lease released in between makes the waiter try again at once, and only a lease whose file still names a process that is gone counts as dead. Red: 200 rounds of two lease run <dir> exclusive --wait -- true started together refused 102 with the lease program of the previous commit. Green: two runs of 300 rounds refused none. | — | — | [o] |
| G5.33-11 | Give the Rust client database tests of every run their send-savepoint SQLite file. Cause: G5.33-4 moved SEND_SQLITE_DSN into the run directory of rust-send-savepoint-check and stopped WITH_TEST_ENV from exporting ORM_SEND_SQLITE_DSN, but the Rust lane of client-db-check (scripts/client-db-test.sh, cargo test --workspace through tests/cargo-test.mjs) runs tx::send_tests as well, so in the full run of G5.32 tx::send_tests::sqlite_send_savepoint panicked with declared test DSN is required (tx_send_tests.rs:9). Now scripts/check/databases.sh writes ORM_SEND_SQLITE_DSN with a SQLite file in the run's own directory into the environment file of each run, which make check, make owner-check and make run-databases give their targets; rust-send-savepoint-check still passes its own. Red: with the environment file of a run created by databases.sh without that line (as the previous script wrote it), tests/cargo-test.mjs running cargo test ... --lib tx::send_tests::sqlite_send_savepoint printed FAIL tx::send_tests::sqlite_send_savepoint elapsed=2ms: declared test DSN is required. Green: with the environment file the new script wrote (ORM_SEND_SQLITE_DSN='sqlite://.../.runtime/check/orm_probe_85875/send-savepoint.sqlite') the same case printed PASS tx::send_tests::sqlite_send_savepoint elapsed=16ms; the run's databases were dropped afterwards. | — | — | [o] |
| G5.33-12 | Let the children of an exclusive lease holder share the lease one exclusive holder at a time. Cause: tests/lease let any descendant of a process holding the exclusive lease use that lease as its own, so the processes that one holder runs at once all held it together. make feature-check holds the exclusive lease of the TypeScript build output and scripts/features/check.mjs runs feature steps in parallel lanes, so in the full run of G5.32-9 schema_definition/schema-compare rebuilt clients/typescript/dist with tsc while schema-typescript (make dbspec-ts-check) was loading it; dbspec-rules.mjs failed to load with The requested module './parse.js' does not provide an export named 'parseDocument' (from dist/dbspec/introspect_catalog.js), which node --test reported as test at clients/typescript/tests/dbspec-rules.mjs:1:1 'test failed' beside 226 passing cases. The test file and its case helper were not at fault. Now a descendant takes its lease inside the ancestor's lease, in the directory within-<pid>-<start time> of that lease, by the same rules: siblings refuse or wait for each other's exclusive lease, readers share, and only the same process asking again uses its own lease. The inner directory is removed when the ancestor's lease is released, and lease list and lease clear-dead also cover the inner leases. Red: the new case of scripts/test-servers.test.mjs (children of an exclusive holder share its lease one exclusive holder at a time) with the lease program of the previous commit printed exclusive=0, shared=0 where a sibling held the exclusive lease. Green: with the new program it printed exclusive=3, shared=3, after=0, readers=0 and named the sibling holder, in three runs. | — | — | [o] |
| G5.33-13 | Run the interface results checker of CI through make. Cause: G5.33-8 builds the Rust extractor of tests/interfaces/check under the lease of the shared target directory and reads LEASE and CARGO_LEASES, which the Makefile exports, but the CI step common state and execution contracts ran node tests/go-run.mjs interfaces-check ./tests/interfaces/check --results ... directly, so the CI run of G5.34-2 passed make check and failed that step with FAIL interfaces/rust-build elapsed=0s: LEASE and CARGO_LEASES are unset; run this through make, which exports them. Now the new target make interface-results-check runs the three result checks (tests/conformance/out, its postgres and sqlite directories) after lease-tool, and the step runs that target; the checker keeps refusing without the lease variables. make repo-check gains ciLeaseErrors (scripts/repo/ci.mjs): a workflow step that runs, outside make, a repository program whose source reads LEASE (a Go package through tests/go-run.mjs or go run, a Node script, a shell script) fails. Red: the step's first command run outside make printed the same FAIL interfaces/rust-build ... LEASE and CARGO_LEASES are unset, and the new case of scripts/repo/check.test.mjs reported the three lines of that step in ci.yml. Green: make interface-results-check passed interfaces/rust-build, go, php, rust and typescript, and the repository cases (59) pass. The local interfaces/results comparison failed on statement counts against the development machine's tests/conformance/out, an output of an earlier local run; the CI run of D4 passed it with the same expected sequences (contracts/interfaces.json). | — | — | [o] |
| G5.33-14 | Check the conformance results against the shared state expectations inside the run that produced them, and name every difference. Cause: the statements count of each sequence in contracts/interfaces.json counted only model statements (select, insert, update, delete), the statements that the clients' OnQuery hook reported before T42; T42 (T42) recorded every statement event in the conformance vectors and outputs, transaction control included, but kept the counts, so the interfaces checker, which compared the count with all statements of an output, failed on the current output of the full run of G5.32-9 with only go: state contract now_defaults failed (expected 3 against 5, and for transactions 5 against 14). The CI run of D4, before T42, had passed. The check also read tests/conformance/out, which any earlier run had left on disk, so its result depended on what had run before. Now a sequence lists the kinds of its statements in order, utility statements excepted, because their number and place differ per dialect (transactions sends four on MySQL, GET_LOCK to SET @orm.ormtest.actor = NULL, and two on PostgreSQL and SQLite) and the vectors list them exactly. The lists come from the vectors of the three dialects, which agree without utility statements; their model statements are exactly the old counts, and the transaction control statements that the clients send are added: now_defaults begin and commit; creates_and_save begin, two savepoints with their releases, and commit; delete_recursive begin, a savepoint with its release, and commit; transactions begin, savepoint, rollback_to, release and rollback; restore begin and rollback; terminal_reuse, relation_empty and errors are unchanged. make conformance-check runs the runners with check run -out $(RUN_DIR)/out (a directory that must not exist yet), checks those outputs with interfaces-check --results (repeatable) in the same recipe and removes the directory, so nothing reads tests/conformance/out any more and the interface-results-check target and the .gitignore entry are gone. Differences explain themselves (internal/stmtdiff): the interfaces checker names added and missing statements with position, kind and SQL with a count per kind group, a different result with both values, a statement without or with an unknown kind, and a missing output with its path; the conformance comparison names the differing fields, and added and missing statements with kind, SQL, binds and error, in place of two whole JSON dumps. make fuzz-check joins CHECK_TARGETS, CI runs make check alone after its setup (the fuzz, interface results and server environment steps are removed), and make repo-check gains ciAfterCheckErrors, which fails a command step after make check, and drops the rule that required exporting the server environment for steps outside make. Red: on the full run's output the previous checker printed FAIL interfaces/results elapsed=3ms: go: state contract now_defaults failed and nothing more; ciAfterCheckErrors reported the fuzz and interface results steps of the previous ci.yml. Green: make run-databases TARGETS=conformance-check passed twice in a row, then after an unrelated make fuzz-check once more, every time with interfaces/results passing on its own run's outputs while a planted tests/conformance/out with wrong contents stayed unread, and left no run directory; the cases of internal/stmtdiff, tests/interfaces/check (statement differences, results, missing output) and tests/conformance/check (new output directory, difference report) pass, and the repository cases (59) pass. | — | — | [o] |
| G5.35 | Check the formatting of every Rust workspace. Cause: make rust-fmt-check ran cargo fmt --all --check in clients/rust only, and its inputs in contracts/check-inputs.json named only clients/rust/**/*.rs, so bench/rust and tests/interfaces/rust were never checked and had drifted (25 and 10 Diff in blocks). Now rust-fmt-check also checks both workspaces (rust-fmt/bench, rust-fmt/interfaces), their sources are inputs of the target, and cargo fmt --all formatted both; the change is formatting only. Red: with the extended target over the unformatted sources, make rust-fmt-check printed PASS rust-fmt and FAIL rust-fmt/bench elapsed=4.365s: cargo exited with 1. Green: after formatting it prints PASS rust-fmt, PASS rust-fmt/bench and PASS rust-fmt/interfaces and exits 0. make owner-check over the paths that differed from main when it ran (78: the 11 of this item and 67 that main had gained meanwhile) passed its 17 steps, rust-fmt-check among them (1.3 s). | — | — | [o] |
| G5.34 | Refuse the full suite before any step unless it may run. Cause: make check started its steps in any state of the checkout, with items in progress, with uncommitted changes and again on a tree whose full suite had run, so the rule that the full suite runs once, when every active item is complete, held only when the person who ran it applied it. | Acceptance: the first line of make check and of the new make rerun-failed is node scripts/check/full-run.mjs decide <entry>, which decides before any step (before TEST_ENV is read and the lease program is built) and prints the decision with its reasons; it refuses with exit status 2 while an item of docs/checklist.md, sub-items included, is in progress, naming each ID and title, while tracked files have uncommitted changes, while the record names a run whose process still runs, and, for make check, when .runtime/full-run.json records a full run of the same tree (git rev-parse HEAD^{tree}), naming that run; the runner (scripts/check/run.mjs --full-run and --rerun-failed) decides again while .runtime/full-run.lock is created exclusively and writes the record before the first step and after each step starts and ends: the tree, the commit, the result, the targets that did not pass, the database steps and every target with its status and times, so a killed run stays incomplete; make rerun-failed runs the database steps and only the targets of the current tree's record that did not pass, and is refused without such a record; make bench and make run-databases run without the guard; full-run-check, an owner target of CHECK_TARGETS with the inputs scripts/check/**, Makefile and tests/testcase.mjs, runs the cases. A fresh checkout has no record, so CI runs make check on a push, and a push happens only when every item is complete. No repository rule is added: full-run-check already fails when either recipe does not start with the guard. Red: make check and make rerun-failed start with the guard in scripts/check/full-run.test.mjs failed because the check recipe was $(WITH_TEST_ENV) node scripts/check/run.mjs $(abspath $(TEST_ENV)) $(CHECK_TARGETS), so with a checklist holding items in progress the runner would start. Green: the 9 cases of scripts/check/full-run.test.mjs pass on a temporary checkout whose runner runs stub commands instead of the database steps and make targets: an active item and an active nested sub-item are refused and listed by decide and by the runner while no step runs, an uncommitted change is refused, a second full run of the same tree is refused naming the first, a rerun without a record is refused, a rerun runs the database steps and only the failed target b and then has nothing to rerun, and a run killed with SIGKILL stays incomplete with its target running and its rerun runs the three unfinished targets; make owner-check with the 13 changed paths selected 8 owner targets and passed all of them: checklist-check, full-run-check, version-check, repo-check, docs-rules-check, docs-check, docs-verify-idempotent and feature-unit-check; in this worktree node scripts/check/full-run.mjs decide check and make rerun-failed were refused with exit status 2, listing G5.32 and G5.34 while it was in progress and the uncommitted files, the rerun also with no full run is recorded; make check was not run. G5.34-1 records failed and unfinished targets apart; make owner-check over the 7 paths of G5.33-11 and G5.34-1 passed its 7 make targets, full-run-check among them (2.0 s). | — | [o] |
| G5.34-1 | Record failed and unfinished targets apart in the full-run record. Cause: scripts/check/full-run.mjs wrote every target into failed when a full run started and replaced the list only when the run finished, so the record of the full run of G5.32, stopped during feature-check, listed all 48 targets under failed although its targets[] showed 44 as passed, 2 as failed, 1 as running and 1 as not run. Now summarize derives failed (targets whose status is failed) and incomplete (running or not run) from the targets each time the record is written, the first record starts with incomplete holding every target and failed empty, a finished run fails when either list is not empty, and the refusal and the final line name both lists. make rerun-failed still selects every target that did not pass, which is the failed and the unfinished ones; a record without incomplete, as older ones are, is still read. Red: with the recorder of the previous commit, the new case a killed run records failed and unfinished targets apart of scripts/check/full-run.test.mjs (a stub run where a passes, b fails and c ends the runner with SIGKILL) failed: failed held a, b and c and incomplete was missing. Green: that case passes with failed ['b'], incomplete ['c'] and a rerun of exactly b and c; summarize separates failed from unfinished targets passes, and node --test scripts/check/full-run.test.mjs passes its 11 cases. | — | — | [o] |
| G5.34-2 | Let make rerun-failed run on a commit that descends from the recorded one. Cause: scripts/check/full-run.mjs allowed a rerun only on the tree of the recorded full run, so a failure of the code found by the full run of G5.32-9 and fixed by a later commit could only get a new full suite; the targets that had passed would run again for a change that does not touch them. Now a rerun is allowed when the current commit is of the recorded tree or descends from the recorded commit, which is the commit of the last rerun or else of the full run (git merge-base --is-ancestor). It reruns the targets that did not pass (failed and unfinished) and the owner targets that the paths changed since that commit (git diff --name-only) select in contracts/check-inputs.json, with the selection of make owner-check (selectTargets of scripts/features/owners.mjs), and prints each owner target with its paths. It sets the selected targets back to not run before it starts, so a killed rerun leaves them unfinished, and records its commit, tree, base commit and owner targets, so the next rerun counts changes from it. A commit that does not descend is refused with both commits; every other refusal (items in progress, uncommitted changes, a running run) stays. AGENTS.md and the Makefile comment describe the new rule. Red: the three new cases of scripts/check/full-run.test.mjs failed with the previous guard, the descendant decision with the recorded full run is of tree t1, not of the current tree t2, and the rerun on a new commit of the stub checkout with full-run: refused make rerun-failed. Green: all 14 cases of the file pass, including a killed rerun whose owner target c stays not run and is rerun, a later rerun with nothing to rerun, and a checkout of the earlier commit that is refused. | — | — | [o] |
| G5.36 | Keep PHP connections across the requests of a php-fpm worker, and report a lost connection with one code in every client. Cause: Orm::connect (clients/php/src/Orm.php) built a new PDO for every Db, and Config refused poolIdleSize and poolLifetimeMs because "the PHP client has none", so every request opened a server session with startup and authentication: 956 PHP page requests on PostgreSQL opened 956 server sessions, where the Node client used 20 pooled sessions. A connection that the server ended (a killed session, a restart, an idle limit) failed with DRIVER in every client, so no rule could tell it from another driver error without reading the message. Change: poolSize keeps its meaning of the other clients, the most connections a process holds for a database at once, and gives PHP a pool in the process (clients/php/src/Connection.php): a Db takes the first free slot of the pool of its DSN and connection settings, the first poolIdleSize slots (all when zero) are PDO persistent connections that stay in the process for the Db of a later request, a later slot closes with its Db, close() and the end of the Db return the slot, and a Db that finds every slot held by an open Db of the process gets CONFIG, because a PHP request cannot wait for itself; poolSize zero keeps one connection per Db, and poolLifetimeMs other than zero stays CONFIG, because PDO cannot close a kept connection. poolSize: 1 is one connection per worker. The session settings are set only when a connection opens: PostgreSQL startup parameters as before, and MySQL SET time_zone = '+00:00', SESSION max_execution_time = <ms> as the init command of the connection instead of statements after the connect; SQLite runs its pragmas on every connect, in process. A request that ends inside a transaction (exit, a fatal error) has the transaction ended by a shutdown function that Db::begin registers once per request: the MySQL named locks are released, the MySQL local values cleared, the SQLite modes reset and the transaction rolled back. The new driver code CONNECTION_LOST (docs/spec/errors.yaml, generated into Code.php, codes.go and codes.rs) names a connection the server ended or that broke: MySQL 2006, 2013 and 4031, PostgreSQL SQLSTATE class 08 and 57P01, 57P02 and 57P05, and the driver's error for a connection that is no longer open: pdo_pgsql's error without a server SQLSTATE on a connection whose backend pid is 0 (OrmException::fromDriver now takes the PDO), go-sql-driver ErrInvalidConn, driver.ErrBadConn and pgx ErrConnClosed in Go, an I/O error of end of stream, reset, abort, broken pipe or not connected in Rust, a fatal mysql2 error with a code of a lost connection or none, and the statement error of a node-postgres session that reported a connection error, in TypeScript. No client matches a message text for it. The PHP client sends a statement once more on a new connection when it fails with CONNECTION_LOST and no statement of the current Db has run on that connection (Db::send); a statement sent through the PDO handle of the Db counts as run, each attempt is a statement event, and every other error is reported unchanged. Red: clients/php/tests/pool_test.php (new) starts a PHP built-in server, one process that handles requests one after the other and starts the PHP state of each anew as a php-fpm worker does, with the router pool_request.php, and runs on a case database through the server DSNs. On the client of G5.32-9 (with only the generated Code.php of this item) it failed every case but pool_request_end/sqlite: pool_reuse mysql "the two requests use one session: 65554 and 65556", postgres 60596 and 60600, sqlite total_changes 0 in the second request; pool_request_end mysql and postgres on two sessions; pool_lost_between_requests found no session left to end; pool_bounds got CONFIG: poolIdleSize and poolLifetimeMs configure a connection pool, and the PHP client has none; resend_first_statement mysql DRIVER: ... 2006 MySQL server has gone away and postgres DRIVER: ... 7 FATAL: terminating connection due to administrator command; no_resend_after_statement [null,"DRIVER"]. With the pool but without the request-end shutdown function, pool_request_end failed on mysql ("the local value of the ended transaction is cleared: 'left'", "the named lock ... is released: 65531") and sqlite (READ_ONLY ... attempt to write a readonly database after a read-only transaction that the request left), and passed on postgres, whose transaction-local values and advisory locks end with the transaction. The rollback_failed case of the rollback tests now also requires CONNECTION_LOST for the callback and the rollback error on MySQL and PostgreSQL; before the mapping it failed in every client: PHP DRIVER: ... 2006 MySQL server has gone away and DRIVER: ... 7 no connection to the server, Go DRIVER: invalid connection, DRIVER: FATAL: terminating connection due to administrator command (SQLSTATE 57P01) and DRIVER: driver: bad connection, Rust DRIVER: error communicating with database: peer closed connection without sending TLS close_notify and DRIVER: terminating connection due to administrator command (SQLSTATE 57P01), TypeScript DRIVER: mysql: Can't add new command when connection is in closed state and DRIVER: postgres: Client has encountered a connection error and is not queryable. Green: php clients/php/tests/pool_test.php passes its 15 cases: pool_bounds (sqlite, mysql, postgres: a third Db of a pool of two is CONFIG, the first connection stays for the next Db, the one beyond the idle size closes with its Db, close() returns it), pool_reuse (one session for two requests, and in both the session settings of the connect: PostgreSQL TimeZone UTC and statement_timeout 4321 from the source client, MySQL time_zone +00:00 and max_execution_time 4321, SQLite foreign_keys 1 and busy_timeout 5000), pool_request_end (same session, the left row rolled back, no MySQL local value or named lock left, a write after an ended read-only transaction), pool_lost_between_requests (mysql, postgres: a new session after the test ended the old one), resend_first_statement (mysql, postgres: events [CONNECTION_LOST, null] for the same statement and its result) and no_resend_after_statement (CONNECTION_LOST after one event [null, CONNECTION_LOST]). The rollback tests pass: PHP rollback_test.php 9 cases, Go ^Test(Savepoint)?RollbackFailed 6, Rust --test rollback rollback_failed 6, TypeScript rollback.mjs 9, and TypeScript idle-connection-end.mjs 2. The new verification commands pool-php of dsn_connection and rollback-go, rollback-php, rollback-rust and rollback-typescript of transactions run these tests; go run ./tests/interfaces/check -language php, typescript and go pass after the PHP and TypeScript symbol snapshots were recorded. make owner-check over the 36 changed paths selected 21 verification commands and 14 coverage parts of 6 features and 10 make targets and passed its 21 steps; its first run failed docs-rules-check (Korean words that docs.writing-style forbids) and queries-php and chunking-php, whose model_test.php cases built a Db from a test PDO, which now passes through Connection::fromPdo. | — | — | [o] |
| G5.37 | Let addTablesAndColumns create the missing indexes of existing tables, and refuse a missing unique key with its reason. Cause: the step functions of the four clients (dbspec.AddTablesAndColumnsSteps of engine/dbspec/add_tables_and_columns.go for Go, Dbspec::addTablesAndColumnsSteps, add_tables_and_columns_steps of orm-schema, addTablesAndColumnsSteps of the TypeScript dbspec) accepted only create_table and the add_column of a column that is null or has a default and returned every other difference as SCHEMA_DIFFERS, so a set that adds an index to an existing table could not be upgraded: adding index ix_audit_request (request_id) to a table audit of an installed set failed on every database with SCHEMA_DIFFERS: ... add_index audit.ix_audit_request. An index (index, which dbspec keeps apart from unique) refuses no row, so creating it cannot fail on the rows a table holds; a unique key can. Change: the four step functions accept add_index and plan it with the tables and columns in one plan (CREATE INDEX), and return the created index as table.index after the added columns of its table; a missing unique key stays a difference, now named add_unique <table>.<name>: a missing unique key can fail on the existing rows; add it with a plan, and the SCHEMA_DIFFERS message of the four clients says the set differs "beyond missing tables, missing columns that are null or have a default and missing indexes". docs/spec/schema.md, usage.md, interfaces.md (en/ko), docs/spec/errors.yaml with the generated Code.php, codes.go and codes.rs, the schema_install entry of contracts/features.json with docs/features.md, and the PHP and Rust READMEs state the rule. Red: the new case add_tables_and_columns_index of the four addTablesAndColumns tests installs version 1 of contracts/fixtures/add_tables_and_columns and applies unique.dbs and then index.dbs (a nullable column and ix_addcol_item_label); with the step functions of G5.33-13 it failed on SQLite, MySQL and PostgreSQL in every client: the unique key was named only add_unique addcol_item.uq_addcol_item_label, and the index set failed with SCHEMA_DIFFERS: ... add_index addcol_item.ix_addcol_item_label (PHP 3 failed cases, Go TestAddTablesAndColumnsIndex{SQLite,MySQL,Postgres}, TypeScript 3, Rust add_tables_and_columns_index on sqlite first). Green: the case passes on the three databases in every client: the missing unique key is SCHEMA_DIFFERS with the reason, the index set returns [addcol_item.note, addcol_item.ix_addcol_item_label, addcol_item_history.note], a second call returns [], and install of the index set then changes nothing, so the index exists as declared; index left the list of refused fixtures. The whole addTablesAndColumns tests pass: PHP add_tables_and_columns_test.php 15 cases, Go ^TestAddTablesAndColumns 15, TypeScript add-tables-and-columns.mjs 15, Rust --test add_tables_and_columns 5 cases over the three databases each. make owner-check over the 33 changed paths of G5.37 and G5.37-1 selected 11 verification commands and 8 coverage parts of 2 features and 11 make targets and passed its 17 steps, test-servers-check among them (1 m 19 s). | — | — | [o] |
| G5.37-1 | Make the binary logs of the test MySQL servers expire. Cause: scripts/test-servers.sh started both mysqld with the compiled binary log retention, 30 days (binlog_expire_logs_seconds 2592000, source COMPILED) and max_binlog_size 1 GB; the expiry of 3600 s set earlier with SET PERSIST did not survive the rebuild of the data directories, and an expiry alone does not act, because the server removes expired logs only when it starts and when it rotates the log. Seven files of about 1.1 GB each had piled up on the primary and the replica, and the shared disk fell to 2 GB free. Change: MYSQLD_BINLOG (--binlog-expire-logs-seconds=3600 --max-binlog-size=67108864) is passed to both mysqld, so the log rotates every 64 MiB and the server removes a log an hour after its last write; the running shared servers are not restarted, and the next make test-servers applies it. Red: the new case the test MySQL server removes an expired binary log by itself of scripts/test-servers.test.mjs starts a fixture mysqld in a temporary data directory on a free port, with the retention options that the script declares scaled to an expiry of 2 s and a size of 4096 bytes, writes ten rows, waits 3 s and writes ten more; with the script of G5.33-13 it failed: binlog.000001 (13196 bytes, last written more than 3 s ago) is kept past binlog_expire_logs_seconds=2: the server removes expired logs only when it rotates the log, and max_binlog_size=1073741824 was never reached; scripts/test-servers.sh sets no --binlog-expire-logs-seconds; scripts/test-servers.sh sets no --max-binlog-size. Green: the case passes: the server removed binlog.000001 itself, and both mysqld of the script take $MYSQLD_BINLOG. | — | — | [o] |
| G5.39 | Make a docs page that does not load say why. Cause: in the full make check on G5.37, docs-check failed docs-static/page/examples/complex-query.html after 15.009 s with only page.goto: Timeout 15000ms exceeded ... waiting until "load", while every other page loaded in about 100 ms; the case named no request, error or measurement, so the failure could not be explained from the log. The page has the same resources as every other page (the stylesheets, vp-icons.css, the preloaded font inter-roman-latin, favicon.svg; no image, frame or external resource of its own) and the build and the local server have no defect that the evidence shows. Reproduction with the same static context on the built site, 60 rounds of the 48 pages while the host load average was about 165, stalled three pages in a row once: ko/dsl.html with the font and favicon.svg pending since +60 ms, then the main documents of ko/examples/complex-query.html and ko/feature-coverage.html pending for 15 s each, so the whole path between the browser and the local server stopped for about 45 s rather than one resource of one page. A second reproduction with a request log in the server, 150 rounds while the load fell from 168 to 25, stalled none, and this branch's npm run docs:check loaded every page in 54 to 72 ms for the two complex-query pages. Now every page opening of the docs check (each static page, the search and the theme and mobile cases, and the mobile reload) runs through scripts/docs/load.mjs, which on failure adds to Playwright's message every pending request with its resource type, URL, the time it was sent and its age, and whether the local server received it and when it answered (the server of scripts/docs/lib.mjs now keeps a request log), every failed request with its HTTP status or browser reason, the console errors and page errors, the longest and 99th percentile event loop delay of the process that runs the local server, and the host load average with the CPU count. The per-page Playwright timeout of 15 s and the case deadline stay. Red: the new case of scripts/docs/load.test.mjs (a page with a stylesheet the server never answers, a missing image and a script that logs an error and throws) got only page.waitForLoadState: Timeout 1000ms exceeded. from the previous loading. Green: it gets the pending stylesheet with the server's view, HTTP 404 image .../missing.png, both console errors, the page error, the event loop delay and the host load, in every run; npm run docs:check passes with all 48 pages, links, search and theme and mobile. | — | — | [o] |
| G5.38 | Run the full suite on GitHub CI after a push, never stop on a failure, and leave enough information to fix every failure before the next CI run. Cause: the full suite ran locally before a push, and a run could end early or silently: scripts/check/run.mjs skipped every target, unrecorded, when databases/create failed; a target recipe stopped at its first failing line, and scripts and runners inside targets stopped at their first failing command or test binary; the output of a target was only printed, so a failure on CI could be read only in the scrolling job log, and nothing collected the run directories, the environment or a summary. | Acceptance: the full suite runs on CI after a push; every target runs unless a setup step it needs failed, which records it as not-run with the failed step and its first failure lines; inside a target, independent parts run after a failed one; each failure is recorded with its exact command, its inputs, its output with the expected and the actual value, the cases that did not finish and their last step, and the environment of the run; the run ends with a summary of every target's status, time and first failure lines, which CI publishes as the job summary and uploads with the report of that run id as an artifact, also when the runner crashes; the job fails when a target failed or did not run. Locally only the Red/Green tests and the owner check of the changed paths run. The sub-items G5.38-1 to G5.38-5 each carry one part. | Evidence: make owner-check over the 29 paths that the five sub-items change selected 9 make targets and passed all of them: checklist-check, full-run-check (the 16 runner cases, the 2 recipe-part cases and the 2 continuation cases of scripts/check/continue.test.mjs), version-check, testcase-check (its five parts, tests/cargo-test.test.mjs among them), repo-check, docs-rules-check, docs-check, docs-verify-idempotent and feature-unit-check. Neither make check nor make rerun-failed ran: the first CI run after a push is the first full run under these rules. | [o] |
| G5.38-1 | Record a target that a failed setup step blocks as not-run, run the others, and write a report of every run. Cause: runChecks ran the targets only when databases/create passed, so a failed database setup left every target, including those that read no database, unrun and unrecorded; it printed the output of a target and kept no file of it, and the run directory of a failed target, the environment and a summary were nowhere. Change: the runner runs two setup steps, servers (it reads the server environment file itself, export NAME='value' lines, and holds the shared lease of the servers, in place of $(WITH_TEST_ENV) in the recipes of check, rerun-failed and run-databases) and databases/create; every target of contracts/check-inputs.json declares needs (["databases"] for the 26 targets that read the servers and the run's databases, [] for the 23 others), checkInputErrors of scripts/features/owners.mjs rejects a missing or unknown one, and a target whose setup step failed is recorded not-run with the setup step <step> failed: <its first failure lines> while the other targets run; the record calls a step that has not started pending. Each target runs as make -k. scripts/check/report.mjs writes the report .runtime/check/<run id>/report of the run, whose id is ORM_CHECK_RUN_ID (CI gives the run id and attempt) or a new name: environment.txt (commit, tree, OS, Node, Go, PHP, cargo, mysqld, postgres, disk), targets/<target>.log (the exact make command, the recipe of the Makefile, the declared inputs and the whole output), targets/<target>/run/ (the run directory .runtime/run/<target>-<make pid> that a failed target left, without build targets, node_modules, database files and binary files, which MANIFEST.txt lists with their sizes), and summary.md. The record of each target keeps its log path, time and first failure lines: the FAIL, --- FAIL, not ok and panic lines, then each case that started and never ended, named with its last step, so a case stopped by its deadline or by a dead process says what was pending; a target whose cases report no failure keeps the last lines of its output and the way its command ended. A log or kept file larger than 1 MiB keeps its last 256 KiB, which its first line and the summary state. scripts/check/summary.mjs (the CI summary step) writes the summary of ORM_CHECK_RUN_ID to GITHUB_STEP_SUMMARY and the report; when the runner did not finish it says so and lists the steps as last recorded, and when the record has no such run it says that make check stopped before the runner recorded it. Red: with the runner of G5.37, the new case a failed setup step blocks only the targets that need it, and the report explains every failure of scripts/check/full-run.test.mjs (a stub suite of four targets, a and c needing databases, a failing database setup, b failing with rows of t: expected 3, actual 5 and a case that does not finish, d passing) ran ['sh create', 'sh drop'] and none of the targets; the crash case could not find scripts/check/summary.mjs; the recipe case found $(WITH_TEST_ENV) in check; and checkInputErrors accepted a target without needs and one with needs: ['servers']. Green: the setup case runs sh create, b, d and sh drop, records a and c not-run with the setup step databases/create failed: ... Access denied for user ..., b failed with its FAIL line and case b/pending did not finish; its last step: STEP b/pending elapsed=1ms: waiting for the lock of t, and d passed, exits 1, and writes environment.txt, targets/b.log with the command and the output, the kept run directory with big.log as [the last 262144 of 2097156 bytes of ...], and summary.md with every row, which the summary step also writes to GITHUB_STEP_SUMMARY; the crash case, killed in target c, publishes result **crashed** and The runner did not finish: the runner process <pid> ended without recording the end of run ci_99_2 with b failed and c running, and a run without a record says make check recorded no run ci_77_1. node --test scripts/check/full-run.test.mjs passes its 16 cases and node --test scripts/features/owners.test.mjs its 15. | — | — | [o] |
| G5.38-2 | Run the independent commands of a target as parts that make -k runs after a failed one. Cause: make stops a recipe at its first failing line, and 13 targets of CHECK_TARGETS held commands that read nothing of each other in one recipe: the unit tests and the check of checklist-check, version-check, repo-check and git-check, the five runners of testcase-check, the type check and the tests of ts-check, the four cargo runs of rust-check, the three workspaces of rust-fmt-check, the two fuzz runs of fuzz-check, the probe and the physical tests of dialect-facts-check, the three checks of feature-unit-check, the coverage and the verification commands of feature-check, and the seven PHP tests of client-unit-check, which one && chain joined; a failure of the first hid the result of the others until the next run. The other targets of more than one line build or prepare what their next line reads, so their order stays. Change: each of the 13 targets lists its parts as prerequisites, <target>/<part>, with one command each (40 parts, declared .PHONY); a part that needs a lease or a build (ts-check/hold, feature-check/build, lease-tool, rust-fetch) has it as its own prerequisite, so a failed setup part stops only the parts that need it; the runner (G5.38-1) and make owner-check (makeArguments of scripts/features/owners.mjs) run a target with -k. Red: with the Makefile of G5.38-1, the new case a target of independent commands runs each as a part of its own of scripts/check/full-run.test.mjs listed every one of the 13 targets with its commands in one recipe (checklist-check: prerequisites checklist-check: instead of checklist-check/unit checklist-check/run and so on), the case the parts of a target after a failed part still run under make -k, which runs the definition of checklist-check in a temporary Makefile with a node that fails the unit tests, ran only --test scripts/checklist/check.test.mjs, and makeArguments gave no -k. Green: the structure case passes for the 13 targets, the make -k case runs both parts and ends with make --no-print-directory -k checklist-check exited with 2, and node --test scripts/check/full-run.test.mjs passes its 18 cases and node --test scripts/features/owners.test.mjs its 15; make -n -k checklist-check client-unit-check ts-check lists the commands of every part. | — | — | [o] |
| G5.38-3 | Run the independent commands of the test scripts after a failed one. Cause: the scripts inside the targets stopped at their first failure: scripts/client-db-test.sh (client-db-check, client-pooler-check) ran each lane under set -eu, so a failing test file ended its lane before the other 10 PHP test files, the second Go run, the Rust integration program and the TypeScript tests; scripts/typescript/sqlite-test.sh did the same with its nine TypeScript tests; and tests/cargo-test.mjs, which runs the test binaries of every cargo test of the checks, returned at the first failing binary. No other runner stops at its first failure: node --test, tests/go-test.mjs (go test without -failfast), the PHP and TypeScript test scripts and the Rust cases continue after a failed case. Change: the lanes and sqlite-test.sh run each test through keep, which runs the command, adds it with its exit status to FAILED when it fails and goes on; each lane ends with finish, which prints client-db-test: the <lane> lane failed: and every failed command and exits 1; a build that the next command reads (the Rust integration program, the TypeScript build) still stops what needs it, and the Rust lane records a failed integration build as the build of the integration program (exit <n>), so the integration program did not run; tests/cargo-test.mjs runs every binary as cargo test --no-fail-fast does, prints cargo-test: <n> of <m> test binaries failed: <target> (exit <code>), ... and exits with the first failing code. The commands stay one per line, so make repo-check still finds every test file they run. The new cases run in full-run-check, whose inputs now include both scripts. Red: with the scripts of G5.38-2, the cases of the new scripts/check/continue.test.mjs, which put a php or node that fails the first test in front of PATH, ran 1 of 11 PHP tests and 2 of 10 TypeScript commands (the build and model.mjs), and the new case cargo-test runs every test binary after a failed one and names the failures of tests/cargo-test.test.mjs (a fake cargo and lease, three binaries, the second failing with exit 4) ran one and two only. Green: the PHP lane runs all 11 and prints client-db-test: the php lane failed: with php clients/php/tests/model_test.php (exit 3), the TypeScript script runs all 10 and names node clients/typescript/tests/model.mjs (exit 3), cargo-test runs the three binaries and prints cargo-test: 1 of 3 test binaries failed: two (exit 4) and exits 4; node --test scripts/check/continue.test.mjs passes its 2 cases and node --test tests/cargo-test.test.mjs its 4. | — | — | [o] |
| G5.38-4 | Let CI run make check after a failed server start, publish its summary and upload the report of that run id alone. Cause: .github/workflows/ci.yml stopped the job when make test-servers failed, so no target ran, not even those that need no server; after make check it published nothing, so a failure could be read only in the job log, and ciAfterCheckErrors of scripts/repo/ci.mjs rejected every command step after make check but accepted any action step there. Change: the database servers step has continue-on-error: true, so make check runs and records the targets that need the servers as not-run with the failure (G5.38-1); make check gets ORM_CHECK_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}; after it, two if: always() steps follow: summary (node scripts/check/summary.mjs, which writes the summary of that run id, also of a runner that did not finish, to GITHUB_STEP_SUMMARY and to the report, and copies the run record as record.json and the logs of the test servers, servers/*.log without their data directories, into the report) and report (actions/upload-artifact@v4, artifact check-<run id>-<attempt>, path .runtime/check/ci_<run id>_<attempt>/report/ only, if-no-files-found: error), so no file of another run is collected and no database data directory or build target is uploaded. The job fails when make check fails, that is when a target failed or did not run. ciAfterCheckErrors now requires the ORM_CHECK_RUN_ID of the make check step and exactly the declared summary and report steps after it (AFTER_CHECK), and names every other step, a changed upload path and a missing step. full-run-check holds the TypeScript build output with $(READ_TYPESCRIPT), because its new test runs scripts/typescript/sqlite-test.sh, which make repo-check counts as a reader of it. Red: with scripts/repo/ci.mjs of G5.38-3, the new case a workflow runs only the summary and the report upload after make check of scripts/repo/check.test.mjs failed on the minimal workflow with the declared steps: ci.yml step "summary" runs node scripts/check/summary.mjs after make check; the previous case had allowed any action step after make check; and with scripts/check/summary.mjs of G5.38-3 the new case the summary step keeps the record and the server logs in the report of the run found no record.json in the report. Green: the repository workflow and the minimal one pass; a fuzz step, a actions/cache step, an upload of .runtime/check/ci_<id>_<attempt>/ instead of its report/, a workflow without the two steps and a make check without ORM_CHECK_RUN_ID each return their error; the report holds record.json equal to the record, servers/postgres.log whole, servers/mysql.log as [the last 262144 of 2097169 bytes of ...] and no servers/mysql data directory. node --test scripts/repo/check.test.mjs passes its 59 cases, node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs 21 and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.38-5 | State in AGENTS.md and AGENTS.ko.md that the full suite runs on CI after a push. Cause: AGENTS.md said that the full suite runs exactly once, locally, when every active item is complete, and that one then runs make check once before a push; the full suite now runs on CI, and one CI run must collect enough information to fix every failure it found before the next CI run. Change: the rule now reads, in both languages: the full suite runs on GitHub CI after a push; it never stops at a failure and completes the suite; each failure records its inputs, the exact command, its output, the expected against the actual value and the environment facts relevant to it, so it can be diagnosed without running it again locally, and a failing case states what failed, where and why; the run ends with the summary that CI publishes and uploads with the report of the run id, and the job fails when a target failed or did not run; a local make check remains possible but is no required step before a push; a push needs the owner's approval each time and happens only when no item is in progress; a CI failure is fixed as a checklist item, and no push follows a CI run in progress to react to it. The guard text of make check in AGENTS keeps its refusals and drops the instruction to run make check once locally; the comments of the check recipe and of scripts/check/full-run.mjs say where the full suite runs. fullSuiteRuleErrors of scripts/repo/ci.mjs, which make repo-check runs, requires the rule in both files and rejects the previous one. Red: the previous AGENTS.md and AGENTS.ko.md gave 12 errors, among them AGENTS.md does not state the full-suite rule: "runs on GitHub CI after a push" and AGENTS.md still states the local full-suite rule: "runs exactly once, when every active checklist item is complete". Green: both files pass, the case AGENTS states that the full suite runs on CI after a push of scripts/repo/check.test.mjs passes, and node scripts/repo/check.mjs passes. | — | — | [o] |
| G5.40 | Count a test as a reader of the TypeScript build output only when it really reads it, and drop the lease that the wrong count asked for. Cause: usesTypescriptOutput of scripts/repo/testcases.mjs, which typescriptReaderErrors (make repo-check) follows, took every clients/typescript/...mjs path on a line with the word node for a file that the test runs; scripts/check/continue.test.mjs names node clients/typescript/tests/model.mjs only as the expected output of a fake node, so the rule counted it as a reader, and G5.38-3 added $(READ_TYPESCRIPT) to full-run-check, which serialized that target against the TypeScript builds for nothing. The same rule missed real reads: an import of ../dist/ whose name list spans lines (clients/typescript/tests/model.mjs), and a run through process.execPath. Change: a JavaScript file runs a TypeScript test only in the two forms that execute it (nodeRuns): a program and its argument array ('node', ['<file>', ...] or process.execPath, ['<file>', ...]), and the shell string of an exec, execSync, spawn, spawnSync, execFile or execFileSync call ('node [options] <file>'); a path named in an expected output, a stub, a regular expression or a comment is no run. An import of the build output is read also when its { ... } name list spans lines; only names, as, commas and whitespace may stand in it, so a function body followed by a comment that quotes an import is no import. full-run-check loses $(READ_TYPESCRIPT) and its lease-tool prerequisite. No path is exempted. Red: with the rule of G5.38-5, the new case a test that only names a TypeScript test path does not read the build output of scripts/repo/check.test.mjs reported scripts/names.test.mjs (which names the path in an assertion, a regular expression and a stub) as a reader and did not report scripts/exec-path.mjs (spawnSync(process.execPath, [...])) and scripts/multi.mjs (a test with a multi-line import of ../dist/index.js). Green: the case reports exactly scripts/array.mjs, scripts/exec-path.mjs, scripts/shell.mjs and scripts/multi.mjs, and neither scripts/names.test.mjs nor scripts/comment.mjs; on the repository scripts/check/continue.test.mjs is no reader, scripts/case-database-check.mjs and clients/typescript/tests/model.mjs are readers, node --test scripts/repo/check.test.mjs passes its 61 cases and node scripts/repo/check.mjs passes without the lease of full-run-check. | — | — | [o] |
| G5.41 | Let a checklist state marker appear only as the state of an item. Cause: docs/checklist.md and docs/checklist.ko.md held a legend of the four markers on line 3 and quoted markers in the text of three items (English lines 454, 505 and 517, Korean 452, 503 and 515), so a stop hook that counted markers took them for items in progress, and scripts/checklist/check.mjs let any line hold a marker. Now make checklist-check fails on every marker of the four states that is not the leading state of an item or sub-item, in a heading, a legend, item text, continuation text or inline code, naming the file, line and column, with no exception or allow-list. The legend is removed from both checklists, which hold only items; the four states are defined in AGENTS.md and AGENTS.ko.md, which already did so, and they now also state that a marker appears only as an item's state. The three item texts say "in progress" or "marked complete" in words. Bracket markers remain in prose outside the checklists, in AGENTS.md and AGENTS.ko.md (4 lines each, the state definitions and the full suite rule), CONTRIBUTING.md and CONTRIBUTING.ko.md (1 line each) and CHANGELOG.md and CHANGELOG.ko.md (the G5.34 entry); the checker covers the checklists only. Red: the new case of scripts/checklist/check.test.mjs, with markers in a legend, item text, inline code and continuation text, got no error from the previous checker, and the new checker reported 24 locations in the previous checklists. Green: the case gets each marker with file, line and column, and the rewritten checklists pass. | — | — | [o] |
| G5.41-1 | Reject the GitHub task list markers x and X outside an item state too. Cause: G5.41 rejected only the four markers of this checklist, but GitHub task lists also read a bracketed x or X as a state, so a reader could still take one in item text for an item. Now scripts/checklist/check.mjs counts those two as markers as well and reports each outside the leading state with file, line and column; a leading x stays one invalid state error. Every non-blank line of a checklist is a heading (starts with #), an item or sub-item (- and a state in brackets and an ID, optionally indented), or a line indented by two or more spaces after an item, which Markdown renders inside that item; any other line, a paragraph or a legend, is unnumbered content in task tracker, and an indented line after a heading is too. Red: the new case of scripts/checklist/check.test.mjs with an x in Korean item text and an X in a continuation line got no error. Green: it reports docs/checklist.ko.md:3:22 and docs/checklist.ko.md:5:8, a leading x gives invalid state, and both checklists pass. | — | — | [o] |
| G5.42 | Make every browser wait of the docs check say why it failed. Cause: G5.39 explained page loads only; during the owner-check of G5.41, docs-static/theme-and-mobile failed with locator.click: Timeout 15000ms exceeded and the call log line waiting for getByRole('switch').first(), which says neither whether the switch existed, was hidden or was covered, nor what the page held, so the failure could not be explained, and passing reruns prove nothing about it. Now scripts/docs/steps.mjs (browserSteps) is the one helper through which scripts/docs/check.mjs opens and reloads pages, clicks, fills, waits for elements, URLs and conditions, reads text and evaluates in the page, in the static page cases and in the search, theme, mobile and reload steps. On failure it adds to Playwright's message what was waited for, the number of elements the locator matches and for each one whether it is visible (with its display and visibility), enabled, its box and the element on top at its center (itself, or the element that covers it) with its HTML and its parent's, or, when none matches, the accessibility tree of the page; then the page URL and the record of G5.39 (pending and failed requests with the server's view, console and page errors, event loop delay and host load); page opening and reload keep the load record of scripts/docs/load.mjs. rawWaits finds a Playwright wait (goto, reload, click, fill, waitFor, waitForURL, waitForFunction, innerText, evaluate and the other waiting methods) in the docs check outside the helper, ignoring the code that runs in the browser, and a case of scripts/docs/steps.test.mjs requires none; the per-step Playwright timeout of 15 s stays. The cause of that one failure is not established: the static HTML of interfaces.html holds two switches, and the next failure will say which state the page was in. Red: a click on the switch of a page without one and of a page where an overlay covers it both gave only locator.click: Timeout 1000ms exceeded.. Green: the first says no element matches getByRole('switch').first() with the accessibility tree (navigation, button "Menu", main, heading "Docs"), the second 1 element(s) match and #1 visible (display block, visibility visible), enabled, box 10,10 40x20, is covered at its center 30,20 by <div class="overlay" ...>, both with the request, error, event loop and load lines; the three cases pass three times in a row, the raw wait case reports a direct click, waitForFunction and innerText of a fixture and none in check.mjs, and npm run docs:check passes all 48 pages, links, search and theme and mobile. | — | — | [o] |
| G5.43 | Fix what the first CI full run (commit G5.41-1) showed of the runner and its report, and find what filled /tmp on the runner. Cause: the run crashed (runner pid 7516 ended without recording its end, databases/drop stayed running), its artifact held only record.json, summary.md and servers/ although the summary named environment.txt and targets/*.log, the first failure lines of client-db-check were PASS and STEP lines, and 25 targets failed from 10:41:47 to the end with disk quota exceeded (errno 122) on /tmp, while the root filesystem kept more than 92 GiB free. | Acceptance: a write that fails is recorded and never ends the runner; the report survives the run; the first failure lines are failure lines; the report records the free space of /, of /tmp and of the run and the largest directories at the start, after each target and at a failure, and a target that fails for lack of space says so with those numbers; the cause of the /tmp exhaustion is G5.44, which needs the measurements of G5.43-4 from a CI run. The sub-items G5.43-1 to G5.43-4 carry these parts. | Evidence: the Red/Green of each sub-item; make owner-check over the 10 paths that the four sub-items change selected 7 make targets and passed all of them: checklist-check, full-run-check (29.7 s), version-check, repo-check, docs-rules-check, docs-check and docs-verify-idempotent. Neither make check nor make rerun-failed ran. | [o] |
| G5.43-1 | Record a failed report or record write and an unhandled error of the runner instead of ending the run. Cause: the job log of the run of G5.41-1 ends the runner with Error: ENOENT: no such file or directory, open '.../.runtime/check/ci_<run id>_1/report/targets/databases-drop.log' thrown from Readable.push and Pipe.onStreamRead: scripts/check/databases.sh drop ends with rm -rf "$dir", the run directory, which since G5.38-1 also holds the report (G5.43-2), and the next output line of the drop step called appendFileSync on the removed log inside the stream callback of the child process, where the exception was not caught and ended the process; no handler recorded it, so the record kept databases/drop running and the summary step could only say that the runner ended without recording its end. Change: reportWriter of scripts/check/report.mjs does every write of the report (the logs, environment.txt, kept run directories, the log tails, the summary); a write never throws and never recreates a missing directory: it collects report write failed: <path>: <code>: <message>, which the runner stores in the record of the step it happened in (reportErrors) or of the run, prints, and counts as a failure of the run (exit 1, result failed); the summary lists them. The recorder of scripts/check/full-run.mjs writes the record the same way (record write failed: ...). handleCrashes of scripts/check/run.mjs turns an uncaughtException or unhandledRejection into runner error: <stack> in the record, the result crashed with that reason, the summary and exit 1; scripts/check/summary.mjs prints that reason for a crashed run. Red: with the runner of G5.41-1, the new case a report write that fails is recorded and does not stop the runner of scripts/check/full-run.test.mjs (target b removes the report directory and writes one more line) ran only sh create, a and b and ended the runner, and the case an unhandled error of the runner is recorded as the reason of a crashed run (target b throws outside any callback of the runner) left the run incomplete. Green: the first case runs a, b, c and sh drop, records b passed with report write failed: .../report/targets/b.log: ENOENT: ..., the run failed, prints the failed writes and exits 1 without a signal; the second records crashed with runner error: Error: boom outside the runner and its stack, clears the runner entry, exits 1, and the summary says The runner did not finish: runner error: Error: boom outside the runner. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs passes its 23 cases. | — | — | [o] |
| G5.43-2 | Keep the report of a run apart from the run's databases, and cap each target log while it is written. Cause: the artifact of the run of G5.41-1 held only record.json, summary.md and servers/, which scripts/check/summary.mjs writes after the runner. The runner had written environment.txt and every targets/*.log, but G5.38-1 placed the report in .runtime/check/<run id>/report, inside .runtime/check/<run id>, the directory that the runner gives scripts/check/databases.sh for the run's databases and that drop removes with rm -rf "$dir"; the drop step removed the whole report, the kept run directories of the failed targets with it, and then crashed the runner (G5.43-1). The files were neither left unwritten nor written elsewhere. A target log also grew without bound while it ran and was cut to its last 256 KiB only afterwards, which dropped its head: the command, the recipe and the inputs. Change: the databases of a run live in .runtime/check/<run id>/databases, which databases.sh creates and drop removes, and the report stays beside it in .runtime/check/<run id>/report, which nothing removes; cappedLog of scripts/check/report.mjs writes a target log through the writer of G5.43-1 up to 1 MiB, keeps only the last 256 KiB of the rest in memory and closes the log with [... <n> bytes of output omitted; the last <m> bytes follow ...] and that tail, so a log holds its head and its end and never exceeds 1.25 MiB, also while it runs; the record keeps the omitted bytes as truncated. A reserve file is not added: the report sat on the root filesystem, which kept 92 GiB free; the writer of G5.43-1 records any write that still fails. Red: with the runner of G5.43-1, the new case removing the run databases leaves the report of the run of scripts/check/full-run.test.mjs (a drop that removes its directory as databases.sh does) found report/environment.txt is missing, and the case a target log keeps its head and its tail within the size limit (target b prints 3 MiB) found a log of 3105026 bytes cut afterwards to [the last 262144 of 3105026 bytes of this log], without its target b and command: lines. Green: after the drop report/ holds environment.txt, summary.md and the logs of servers, databases/create, a, b, c and databases/drop, the databases directory is gone, and no write failed; the log of b starts with target b and its command, holds line 0, the omission line and line 3071 at its end, stays within 1 MiB plus 256 KiB, and the record gives b a positive truncated. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs passes its 25 cases. | — | — | [o] |
| G5.43-3 | Take the failure and error lines of a target as its first failure lines, also behind lane prefixes. Cause: the summary of the run of G5.41-1 gave client-db-check the first failure lines [rust] STEP joins_and_relations/mysql ... created / ... dropped / [rust] PASS .... scripts/client-db-test.sh prefixes every line of a parallel lane with [<lane>] , and failures of scripts/check/report.mjs matched FAIL, RUN, STEP and PASS only at the start of a line, so it found no failure in the target and fell back to its last 20 lines, which were the passing cases of the other lanes; it also knew no error-level line, so [rust] sqlite: schema().install: DRIVER: disk I/O error, the Rust panic, (exit 101) and make: *** ... Error 1 were missed. Change: failureLine strips a lane prefix and a GitHub annotation and takes, in the order they appear, FAIL and TIMEOUT lines, --- FAIL, not ok, panics, lines with the word error (error, Error, ERROR), failed:, failed lanes, (exit <n>), exited with <n>, and disk quota exceeded, No space left on device, errno 28 or 122, ENOSPC and EDQUOT; a PASS or RUN line is no failure; cases are tracked per lane, so a case of one lane that does not finish is still named with its last step. The last lines of the output stay the fallback only when a failed target printed none of these. The fixture scripts/check/fixtures/client-db-check-lanes.txt holds the last 25 lines of that target from the job log, without the runner's own step prefix. Red: with the extractor of G5.43-2, the new case the first failure lines of a target are its failure and error lines of scripts/check/full-run.test.mjs gave the last 20 lines of the fixture, starting with [rust] STEP joins_and_relations/mysql elapsed=47ms: database orm_case_33354_8 created, the PASS and RUN lines among them. Green: the case gives exactly the panic line, [rust] sqlite: schema().install: DRIVER: disk I/O error, the [rust] FAIL columns_and_subqueries/sqlite ... line, the two lines of the failed rust lane, ./scripts/client-db-test.sh: 107: echo: echo: I/O error, client-db-test: failed lanes: rust, make: *** [Makefile:549: client-db-check] Error 1 and the exit of make. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs passes its 26 cases. | — | — | [o] |
| G5.43-4 | Record the free space of /, /tmp and the run at the start and after every step, and say so plainly when a target runs out of space. Cause: the run of G5.41-1 printed only free disk <n> GiB of the root filesystem before each target, which stayed above 92 GiB, while 25 targets failed with disk quota exceeded on /tmp; nothing recorded the space of /tmp, its mount, what it held or which process held removed files open, and a target that failed for lack of space showed no line saying so. Change: diskSnapshot of scripts/check/report.mjs records df -k of /, /tmp, the temporary directory of the process (TMPDIR) and the run directory, findmnt of /tmp (on Linux: the tmpfs size and quota options), the 20 largest entries of /tmp, of TMPDIR, of .runtime/run and of .runtime/check/<run id> (du -sxk), and the files under /tmp that were removed but are still open (lsof +L1); a command that is missing is recorded as missing. The runner writes one snapshot at the start (report/disk/00-start.txt, disk of the run in the record) and one after every step (report/disk/<n>-<step>.txt, disk of the step, and a free space after the step line in its log); the snapshot after a failed step is the space at that failure. When the failure lines of a step hold disk quota exceeded, No space left on device, errno 28 or 122, ENOSPC or EDQUOT, the first failure line becomes out of space: the target failed with ENOSPC or EDQUOT; / <n> GiB free, /tmp <n> GiB free, ...; the largest under /tmp: .... The free disk step line is gone. Red: with the runner of G5.43-3, the new case a target that runs out of space says so with the free space of / and /tmp of scripts/check/full-run.test.mjs (target b fails with write /tmp/go-build.../_testmain.go: disk quota exceeded) found the quota line itself as the first failure line and no report/disk. Green: b's first failure line is the out-of-space line with the free space of / and /tmp and the largest entries of /tmp, its second the quota line; the record holds disk for the run and for b; report/disk holds 00-start.txt and the snapshots of servers, databases/create, a, b, c and databases/drop, each with df, the largest entries of /tmp and the lsof +L1 section. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs passes its 27 cases; runChecks takes the snapshot function as snapshot, and the stub suites use a fixed snapshot except the space case, so full-run-check does not read /tmp for every stub step. make owner-check over the paths that G5.43-1 to G5.43-4 change passed (see G5.43). | — | — | [o] |
| G5.44 | Find what fills /tmp on the CI runner during client-db-check and remove the cause. Cause: in the run of G5.41-1 the first disk quota exceeded (errno 122, EDQUOT) on /tmp came at 10:41:47, at the end of the Rust lane of client-db-check, and /tmp stayed full until the run ended at 10:46, so what filled it outlived the processes of that target, while the root filesystem kept more than 92 GiB free; the job log shows no df of /tmp and no listing of it. | Acceptance: the snapshots of G5.43-4 (df of /, /tmp and the run, findmnt /tmp, the largest entries of /tmp, lsof +L1 on /tmp) of a CI run name what grows; that writer is fixed so that /tmp holds no file of a finished target, with a Red/Green case of its own; a local measurement of the Rust lane of client-db-check (du of /tmp and $TMPDIR, lsof +L1, the run directories, before and after) is recorded when the free disk of the machine is at least 3 GB. | Evidence: the disk snapshots of the CI run of G5.43-4 name the writer, the fixture mysqld of the binary log case of test-servers-check, whose removed log grew from 930 MB after that target to 6.5 GB at client-db-check, and G5.44-1 stops that server before its directory is removed, with a Red/Green that shows the server now exits and nothing is left; G5.44-2 and G5.44-3 fix the report defects that run showed. The local measurement of the Rust lane is not needed, because the snapshots found the writer outside that lane. The /tmp series of the next CI run confirms the fix; if /tmp still grows there, that becomes a new item with its snapshot. | [o] |
| G5.44-1 | Stop the fixture MySQL server of the binary log case before its directories are removed, and fail the case when the server does not stop. Cause: the CI run of G5.43-4 records /tmp, a 7.8 GiB tmpfs with usrquota, at 151,200 KiB used through repo-check, at 1,165,248 KiB right after test-servers-check, and at 6,606,232 KiB, its quota, at client-db-check, after which 25 targets failed with EDQUOT. In every snapshot from test-servers-check on, lsof +L1 names the same process, mysqld 10079, writing to /tmp/orm-binlog-AHN00Q/mysqld.log (deleted) on fd 1 and 2: 930,269,067 bytes after test-servers-check and 6,493,765,632 bytes at client-db-check, with its InnoDB files open in the same removed directory. The largest entries of /tmp never showed it, because a removed file has no entry. The case "the test MySQL server removes an expired binary log by itself" of scripts/test-servers.test.mjs ran mysqladmin shutdown over TCP, which returns before the server exits, ignored its result and removed the data directory at once. A server whose data directory goes away while it shuts down never exits: sample of four such servers left on a development machine by earlier runs showed the InnoDB thread log_files_governor spinning, retrying its redo log file in the missing directory, and lsof put their removed logs at 6.9 to 9.2 GB by SIZE/OFF. Stopping the four freed about 15 GB, less than the 34 GB those offsets add up to, so SIZE/OFF overstates the blocks such a log holds. The case still passed. Change: the case stops the server with STOP_PROCESS TERM <pid> (tests/stop-process, which returns on the exit event of the process), asserts that it stopped and that the process is gone, and removes its directories only then; a server that does not stop fails the case and keeps its directories, so it does not spin. Red: with the old shutdown and a check before the removal, the case failed with mysqld 26697 still runs after its shutdown returned, and that server then exited by itself because its directory was kept. Green: the case passes in 7.9 s, no orm-binlog process or directory remains after it, and make test-servers-check passes. | — | — | [o] |
| G5.44-2 | Take the first failure lines of a target from failures and not from case names, and label a SQLite disk I/O error as out of space only when the snapshot shows a file system without free space. Cause: in the run of G5.43-4 the first failure lines of client-db-check were [typescript] STEP driver-error/trigger_refused/sqlite ...: database file ... created lines. The pattern \b(?:error|Error|ERROR)\b of failureLine matched error in the case name driver-error/..., and the first real failure, [rust] FAIL audit_record_transaction ...: install: DRIVER: disk I/O error, came later. That SQLite error (778) got no out-of-space line, and the out-of-space line named only du entries of /tmp, which cannot show a removed file. A disk I/O error has other causes than space, so the code alone must not make it out of space. Change: a step line (STEP <case> ...: <message>) is judged by its message only, and an error word counts only when no -, /, . or letter comes right before it. spaceCause puts the out-of-space line first for an ENOSPC or EDQUOT line as before, and for a disk I/O error line only when a file system of the snapshot after the step has 0 KiB free, naming that file system. spaceLine also names the largest removed but open file under /tmp from lsof +L1, with its SIZE/OFF and its process. Red: with the runner of main, the new case a case name that holds an error word does not make its step lines failures got the four driver-error STEP lines and an every-error-in-order STEP line among its first failure lines. The new case a disk I/O error is out of space only while a file system has no free space got the plain FAIL line first where /tmp had 0 KiB free. Green: the first failure lines are the Rust FAIL, panic, quota and compile error lines; with /tmp at 0 KiB the first line is out of space: the target failed with a disk I/O error while /tmp had no free space; ...; the largest removed but open file: 6192.9 MiB by lsof SIZE/OFF, /tmp/orm-binlog-AHN00Q/mysqld.log (mysqld 10079); with free space the FAIL line stays first. | — | — | [o] |
| G5.44-3 | Keep the npm debug log of a failed target in its report and take its error lines as the first failure lines. Cause: in the run of G5.43-4 package-check showed only npm error A complete log of this run can be found in: <npm cache>/_logs/2026-10-05T11_42_43_213Z-debug-0.log. That file is outside the report and the artifact, so the error of npm could not be read without running it again. Change: the runner notes every path that a step line names in A complete log of this run can be found in:. For a failed step it copies that file to report/targets/<step>/ (its last 256 KiB when it exceeds 1 MiB), puts its <n> error <message> lines first among the first failure lines as npm debug log <copy>: <message>, and says so when the file is missing. Red: with the runner of main, the new case a failed npm command keeps its debug log and its error lines in the report got the npm error A complete log ... line first and no copy in the report. Green: the first failure lines are npm debug log .runtime/check/ci_13_1/report/targets/b/2026-10-05T11_42_43_213Z-debug-0.log: code E404 and its 404 line, and the copy equals the debug log. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs passes its 30 cases, and make owner-check over the four changed scripts passed full-run-check, repo-check and test-servers-check. | — | — | [o] |
| G5.47 | Remove the PHP connection pool of G5.36: the PHP client opens one connection per Db and keeps none across requests. Cause: G5.36 kept PDO persistent slots orm:<pool>:<slot> in each php-fpm worker. A slot is not shared across workers and cannot wait, so the server connections grow with workers × slots × connection settings, and a kept connection carries the session state of one request into the next. A php-fpm worker serves one request at a time and shares nothing with the next, so PHP cannot pool connections well inside the process. A pooler outside it can: PgBouncer in transaction mode and ProxySQL share a few server connections among all workers, docs/spec/config.md "Connection poolers" describes both, and make client-pooler-check runs the four clients through them. Change: clients/php/src/Connection.php with its slots and $held is gone, and Orm::connect opens the PDO and gives it to the Db. Db loses endRequest and its shutdown function, __destruct, $sent, the resend of a first statement after CONNECTION_LOST and reconnect. A connection now closes with its request, and the server then rolls back an open transaction and releases its named locks and session values; a test MySQL session showed IS_FREE_LOCK 0 while it held GET_LOCK and 1 once its PDO was freed. With no reused idle connection, the first statement of a Db runs on the connection connect just opened, so the resend has no case left. Config keeps poolSize, poolIdleSize and poolLifetimeMs, and Orm::connect refuses any of them other than zero with CONFIG, naming PgBouncer and ProxySQL. Deleting the fields would shift the positional arguments after poolSize and silently change statementTimeoutMs. Ignoring a value would let a caller believe the connections are pooled when they are not. Db::poolSize() is gone, because a method that always returns 0 describes nothing. stats() reports maxOpenConnections 1, the true maximum of a Db. MySQL still sets its session with one init command. finish still releases named locks, clears local values and resets SQLite modes at the end of every transaction, because a Db runs several transactions in one request. CONNECTION_LOST, OrmException::fromDriver($e, $driver, $pdo) and poolSize of Go, Rust and TypeScript stay. pool_test.php and pool_request.php are gone, and docs/spec/config.md, usage.md, protocol.md and the dsn_connection feature now say that PHP opens a connection per request and that PgBouncer or ProxySQL pool PHP connections. Red: with the PHP source of main, the case pool options of clients/php/tests/model_test.php connected with poolSize: 3 and reported maxOpenConnections 3, not 1. The new case first_statement_lost of clients/php/tests/rollback_test.php ends the server session of a new Db before its first statement. On MySQL and PostgreSQL it found no error and the events ["CONNECTION_LOST",null], so the client sent the statement again. Green: pool options passes on SQLite, MySQL and PostgreSQL with CONFIG naming the poolers for poolSize 3 and -1, poolIdleSize 1 and poolLifetimeMs 1000. first_statement_lost passes on MySQL and PostgreSQL with CONNECTION_LOST and the single event ["CONNECTION_LOST"]. model_test.php, rollback_test.php, schema_set_test.php, driver_error_test.php and runtime_db_test.php pass, and so does the scoped owner check. | — | — | [o] |
| G5.45 | Return a connection to the pool only when its transaction ended cleanly, and close it otherwise, in the Go, TypeScript and Rust clients. Cause: a transaction ends by releasing its MySQL named locks, clearing its MySQL local values, resetting its SQLite modes and running COMMIT or ROLLBACK, because that state outlives the transaction on a pooled connection. After a failed step the state of the session is unknown. A failed RELEASE_LOCK can leave the lock held, and the next user of the pooled connection then holds a lock it never took. The clients still put such connections back. Go txConn.rollback (clients/go/orm/tx.go) returned the connection with conn.Close() whatever its cleanup and ROLLBACK reported, and commit rolled back through that path after a failed cleanup. TypeScript finish (clients/typescript/src/database.ts) discarded the session only when ROLLBACK or COMMIT failed, not when only the cleanup failed. Rust rollback (clients/rust/orm/src/tx.rs) marked the connection completed after a successful ROLLBACK even when the cleanup before it had failed; its commit path already closed the connection, because the failed cleanup returned before end. The rule is that a pooled connection is clean or discarded: a client that cannot show the session is clean closes it, and the server ends its transaction, locks and values with the session. Change: Go records a failed cleanup at commit in txConn.unclean, and rollback closes the session (closeSession) before conn.Close() when that flag is set or any cleanup step or the ROLLBACK failed. TypeScript sets discard when a cleanup statement failed, apart from a statement whose only failure was its subscriber, because such a statement took effect. Rust end takes clean and marks the connection completed only when it is true, and rollback passes whether the cleanup succeeded. docs/spec/interfaces.md states the rule; the TypeScript SQLite driver holds its one connection, which it cannot close. The PHP client opens one connection per Db (G5.47) and has no pool. Red: on MySQL with a pool of one connection, a transaction whose RELEASE_LOCK failed, or whose lock was released early so that RELEASE_LOCK returned 0, was followed by a use of the same session. The new Go test TestFailedTransactionEndDiscardsConnection found session 69697 returned to the pool after its transaction end failed for a rejected release at rollback and at commit and for a rejected ROLLBACK, and the lock was still held 5 s later. The new TypeScript case mysql/failedEndDiscards found the same for rollback and commit (session 69805). The new Rust test failed_cleanup_discards_the_connection failed with rollback: session 69907 returned to the pool after its cleanup failed. Green: those three pass; a transaction that ends cleanly keeps its session in each of them, and another connection takes the lock of a failed end at once. TestTransactionReports*, TestSavepointReportsFailedEnd and TestCancelledTransactionClearsSessionState pass, the TypeScript model.mjs passes, and the Rust tx::tests pass. make owner-check over the ten changed paths passed version-check, repo-check, docs-rules-check, docs-check, docs-verify-idempotent, go-fmt-check, go-vet-check, rust-fmt-check and the verification and coverage of model_queries, parameter_chunking and interface_contract. | — | — | [o] |
| G5.45-1 | Make owner-check run the transaction end tests of a changed file. Cause: make owner-check over the files of G5.45 selected no command that runs clients/go/orm/transaction_end_test.go ("an input of no verification command") or the tests inside clients/rust/orm/src/tx.rs. tx::send_tests::sqlite_send_savepoint then failed when run by hand with declared test DSN is required, because only a make target gives ORM_SEND_SQLITE_DSN. Through make rust-send-savepoint-check it passed, three of three twice. A file that holds tests, or that is a test of a feature, must select the command that runs them. Change: the feature transactions of contracts/features.json declares transaction-end-go, which runs TestTransactionReports*, TestSavepointReportsFailedEnd, TestFailedTransactionEndDiscardsConnection and TestCancelledTransactionClearsSessionState from transaction_end_test.go, context_reset_test.go and cancelled_session_test.go. It also declares transaction-end-rust, which runs the tx:: tests of the Rust libraries with the run's environment from tx.rs and tx_send_tests.rs, and lists both Rust files among its tests. failing_driver_test.go stays a helper that selects its own check. Red: the new case a changed file selects the transaction end tests that it holds or that test it of scripts/features/owners.test.mjs found no transactions command for clients/rust/orm/src/tx.rs. Green: tx.rs and tx_send_tests.rs select transactions/transaction-end-rust, and the three Go files select transactions/transaction-end-go; owners.test.mjs passes its 16 cases. make owner-check ran both commands, with TestFailedTransactionEndDiscardsConnection, tx::send_tests::sqlite_send_savepoint and tx::tests::failed_cleanup_discards_the_connection passing, and passed all its 12 steps. | — | — | [o] |
| G5.46 | Build Rust into the target directory of each checkout and refuse the target directory of another checkout. Cause: worktrees built Rust into clients/rust/target of the main checkout through CARGO_TARGET_DIR on the make command line, and run.mjs and owner-check passed that value on to every target. Cargo decides that an artifact is fresh from the paths of its sources relative to the package and their modification times. It records no checkout that built it: the dep-info of rustc names sources relative to the workspace, and the uplifted dep-info of cargo is rewritten with the current checkout's path. So a checkout whose sources are older than another checkout's build tests that other checkout's code, and nothing in the target directory can show which checkout built an artifact. A detector would have to record and compare build owners and modification times, and a missed or stale record would test the wrong code without a sign. Not sharing removes the cause. The cost is disk, temporary for each worktree: the target directory of this worktree reached 3.6 GiB in its owner check. Change: the Makefile stops with CARGO_TARGET_DIR=<dir> is outside this checkout <checkout>; each checkout builds Rust into its own target directory (AGENTS.md), because cargo would reuse what another checkout built as fresh when the command line names a target directory outside the checkout; a run directory inside it stays accepted. run.mjs and owner-check no longer pass CARGO_TARGET_DIR to the targets they run, so each target uses the Makefile's directory of its checkout. AGENTS.md and AGENTS.ko.md state the rule, its reason, and that the leases still order the builds of one checkout's runs and that a worktree's target directory goes with the worktree. Comments that described the target directory as shared by several checkouts now say that the concurrent runs of one checkout share it. Red: the new case cargo reuses what another checkout built into a shared target, and a target of its own builds this checkout of tests/cargo-test.test.mjs built checkout a ("from a") into a shared target and then checkout b, whose source says "from b" with an older modification time: cargo rebuilt nothing and b ran "from a". With the Makefile of main, the new case make refuses a Rust target directory outside its checkout failed with make ran with the target directory of another checkout. Green: in its own target directory b runs "from b", make refuses the other checkout's directory with the message above and accepts its own and a run directory inside it, and tests/cargo-test.test.mjs passes its six cases. make owner-check over the 15 changed files, run without a CARGO_TARGET_DIR override and so building into this worktree's own target, passed full-run-check, version-check, testcase-check, repo-check, docs-rules-check, go-fmt-check, go-vet-check, rust-fmt-check, feature-unit-check and the verification and coverage of interface_contract and conformance_verification. | — | — | [o] |
| G5.49 | Fix the two failures of the CI run of G5.44: git-check and client-pooler-check. Cause: the run passed 50 targets and nothing ran out of space, which confirmed G5.44, and failed git-check with FAIL git-subjects ...: the process ended inside the case; its error is above and client-pooler-check with FAIL first_statement_lost/mysql: the Db holds the only other session of the case database: [1514,1515]. The sub-items G5.49-1 to G5.49-3 carry the parts. | — | Evidence: the Red/Green of each sub-item; make owner-check over the 11 changed files passed full-run-check, version-check, testcase-check, repo-check, test-servers-check, git-check, docs-rules-check, feature-unit-check and the verification of transactions. | [o] |
| G5.49-1 | Refuse a commit whose subject breaks git.subject-format before it exists. Cause: nothing checked a subject when it was written: git-check reads the history only after a push, when a pushed commit can no longer change. Change: the tracked hook .githooks/commit-msg runs node scripts/git/check.mjs --message <file>, which checks the subject of the message being committed (comment lines skipped, a merge not checked) and refuses a bad one with the rule and the reason. The Makefile sets core.hooksPath to .githooks at parse time when it differs, so every make run installs it in the checkout. git-check declares .githooks/** and the Makefile as inputs, and AGENTS.md states the hook. Red: without the hook and its installation, the new case a commit whose subject breaks the rule is refused by the commit-msg hook that make installs of scripts/git/check.test.mjs failed with a subject of 53 characters was committed. Green: the subject check passes; in a temporary repository whose core.hooksPath is this checkout's .githooks, the 53-character subject is refused with git.subject-format: subject exceeds 50 characters: ... (contracts/rules.json; the commit is refused) and no commit exists, the 46-character subject is committed, and make -n in a fresh repository sets its core.hooksPath to .githooks; scripts/git/check.test.mjs passes its five cases. | — | — | [o] |
| G5.49-2 | Put the error of a case that ended its process into its FAIL line. Cause: sections() of tests/testcase.mjs wrote FAIL <case> ...: the process ended inside the case; its error is above when a case ended its process. The summary of a make check run takes the FAIL line as the first failure line, so the git-check row showed no error, and the actual line git.subject-format: G5.44: ... was only in the target log. Change: while a case is open, sections() keeps the message of an uncaught error (uncaughtExceptionMonitor) or else the last line written to stderr, and the FAIL line reads the process ended inside the case: <that error> (no error was written when there is none). Red: with the runner of main, the case testcase/sections found no FAIL fixture/section-crash ...: the process ended inside the case: uncaught section error. Green: that line holds the uncaught error, and a case that writes git.subject-format: G5.44: subject exceeds 50 characters to stderr and calls process.exit(1) gets that line in its FAIL line; tests/testcase.test.mjs passes its six cases. | — | — | [o] |
| G5.49-3 | Find the session of the new Db in first_statement_lost as the session it opened, also behind a pooler. Cause: under client-pooler-check the case failed with the Db holds the only other session of the case database: [1514,1515] and then ended a session that was not the Db's. The fixture is installed through the pooler (ProxySQL, PgBouncer), which keeps its own server sessions open in the case database after its client closes, so the case database held more than the Db's session. The Db itself connects to the server directly. The case asserted the same thing behind a pooler, and the assertion stays: the first statement fails with CONNECTION_LOST, has one event, and is not sent again. Change: the case reads the sessions of the case database through the server before and after the Db connects, checks that the Db opened exactly one, and ends only that one. Red: with the pooler DSNs that client-pooler-check uses, the case failed on MySQL with [70292,70293] and on PostgreSQL with [37655,37679], then with no error and the events [null]. Green: it passes on MySQL and PostgreSQL behind ProxySQL and PgBouncer and without them, and the other cases of rollback_test.php pass. | — | — | [o] |
| G5.50 | Make codec-check compare only the codec outputs that it produced in its own run. Cause: codec-check ran only php tests/codec/check.php, which read tests/codec/out/{go,rust,typescript}.json. Other targets wrote those files whenever they ran the Go, Rust and TypeScript codec tests, and the local files dated from an earlier run of the day. So the check compared whatever an earlier run of any checkout state had left, not the code of this run. The Go writer also ignored the error of os.MkdirAll and wrote the file in place. Change: codec-check is now the parts codec-check/go, codec-check/rust and codec-check/typescript, which run the three codec tests with ORM_CODEC_OUT set to a directory of this make run (CODEC_OUT, .runtime/run/codec-check-<make pid>), and codec-check/compare, which runs the PHP checker on that directory after all three and removes it. The writers are independent, so under make -k a failed one does not stop the others. A writer writes only when ORM_CODEC_OUT is set, checks every error, and writes through a temporary file and a rename. The checker requires ORM_CODEC_OUT and reads exactly go.json, rust.json and typescript.json there. tests/codec/out/ is no longer written or ignored, and tests/codec/README.md describes the run. Red: in a checkout of main holding the earlier outputs, a Go encoder that appends a byte before base64 made TestCodecVectors fail, yet make codec-check passed all three of its cases on the old files. Green: with the same broken encoder make -k codec-check fails in codec-check/go while codec-check/rust and codec-check/typescript still run. With the encoder restored, make codec-check passes TestCodecVectors, codec::tests::vectors, codec-vector and the PHP cases with 3 other-language outputs of 80 vectors, and leaves no run directory. make owner-check over the seven changed files passed all its 13 steps. | — | — | [o] |
| G5.51 | Fail owner-check for a changed path that selects no behaviour test, and connect every tracked file to the test that reads it or declare its scope. Cause: owner-check only printed <path> is an input of no verification command, coverage part or helper; make check runs what reads it and exited 0. A scan of the tracked files found 237 that selected no feature command, coverage part or helper and no owner target except the lint targets: repo-check matched every path through **, and go-fmt-check, go-vet-check, rust-fmt-check and docs-rules-check matched by extension. Among them were the generator and cmd/orm-gen, the TypeScript index, the generated error codes, internal/*, the physical tests of tests/dialects, tests/codec and tests/conformance. So make owner-check PATHS=generator/gen_go.go ran only lints and passed. Change: a target of contracts/check-inputs.json declares inputs, the files whose behaviour it tests, apart from lints, the patterns it only lints. repo-check now has the inputs scripts/repo/** and the lints **, docs-rules-check has the inputs contracts/rules.json and scripts/docs/** and the lints **/*.md, and the formatters have only lints. contracts/check-inputs.json also has paths, a scope lint or suite with a reason for each exact path that no test can own. Patterns, untracked paths, other scopes and empty reasons are declaration errors. A changed path that selects no feature command, coverage part, helper or target through its inputs, and has no declared scope, now fails owner-check with the path and how to declare it; a removed path is not checked. The 237 files are now connected. 27 targets that were suite-scoped and read specific files are owner targets with those inputs: go-model-check, client-unit-check, php-min-check, ts-min-check, rust-150-check, rust-driver-check, example-check, fuzz-check, dialect-facts-check, package-check, ts-model-check, codec-check and 15 dbspec targets. The six whole-suite targets stay suite. 40 new verification commands run the tests that no command ran; a Go command runs the Test and Fuzz functions of its files. Existing commands gained the fixtures, Faults.php, sqlite-paths.json and sources that their tests read, and 12 helpers own scripts and generated files. The helper error-codes regenerates clients/go/orm/codes.go, clients/php/src/Code.php and clients/rust/orm/src/codes.rs from docs/spec/errors.yaml and compares them; nothing checked those generated files before. Sixteen paths declare a scope. lint: .gitignore, LICENSE, clients/rust/rustfmt.toml. suite, each with the mechanism by which it affects every target: .github/runner, .github/workflows/ci.yml, .github/workflows/docs-pages.yml, .node-version, .php-version, clients/php/composer.lock, go.mod, go.sum, rust-toolchain.toml, internal/testdb/testdb.go, the Rust case-clock and case-database libraries and scripts/cargo-build-copy.sh. Red: with the owner-check of main, owners.mjs --list generator/gen_go.go selected only repo-check, go-fmt-check and go-vet-check and exited 0. Green: it selects go-model-check, and a file that nothing declares exits 1 with selects no behaviour test. The new cases of scripts/features/owners.test.mjs check the selection of lints, untested paths and scope errors, the generator source, and every tracked file: 0 undeclared files of 1027. make owner-check over the declaration changes ran all 127 selected verification commands, 58 coverage parts and 12 helpers once. After the fixes below and in G5.51-1, the failed steps passed on their rerun. | — | — | [o] |
| G5.51-1 | Leave the ORM's own SQLite row lock table out of the database state that the conformance and decimal checks compare. Cause: once the helper decimal-physical ran make decimal-physical-check, that check failed with sqlite/go/1: database state changed, twice. The target is not in CHECK_TARGETS, so nothing had run it. Before its first transaction a SQLite client creates orm__row_lock outside the transaction (docs/operations/usage.md), and snapshotDatabase of tests/conformance/check/state.go hashed every table, so the first client's run changed the state although its rolled-back test left no data. Change: snapshotDatabase skips the table orm__row_lock. The feature conformance_verification declares conformance-check-go, which runs the tests of tests/conformance/check, because before only go vet selected those files. The fixes of the first run are part of G5.51: error-codes runs orm-gen through tests/go-run.mjs (repository rule), the helpers pass DECIMAL_ENV, preempted-timing runs make timing-check, which declares the programs of that test, and docs/spec/errors.yaml selects error-codes. Red: the new TestSnapshotIgnoresTheRowLockTable failed with the state reader of main. Green: it passes with the other state tests, decimal-physical passes on MySQL, PostgreSQL and SQLite, and conformance-runners and conformance-check-go pass. | — | — | [o] |
| G5.52 | Run every CI setup step and make check after a failed setup step, and record the targets that need what it installs as not-run. Cause: in .github/workflows/ci.yml only the step database servers had continue-on-error, and the other installs (Go, the Node dependencies, Rust, the two PHP releases, the PHP SQLite check, composer, the server programs) stopped the job at a failure. Then make check, the summary and the report did not run, so one failed install hid every target and left no report. continue-on-error also removed a failure from the job's result. In .github/workflows/docs-pages.yml, a failed install likewise skipped the build, the idempotence check and the smoke test. Change: every setup step of ci.yml has an id and if: ${{ !cancelled() }}, so it runs after a failed earlier one. make check, the summary and the report run with if: ${{ !cancelled() }}, and make check receives the step results as ORM_CI_SETUP: ${{ toJSON(steps) }}. scripts/check/ci-setup.mjs maps each step id to what it installs: go, node-modules, rust, php-min, php, composer, server-programs and databases. rust-cache installs nothing a target needs, and the checkout and Node are what the runner itself needs. The runner records each step that did not succeed as the setup step ci/<id>, and records a target that needs what it installs as not-run with the CI setup step <id> failed; its output is in the job log of that step, while every other target runs. A failed server-programs step also blocks databases. contracts/check-inputs.json declares these needs for the 49 targets, and owner-check refuses a need that no step provides. docs-pages.yml gives each step an id and if: ${{ !cancelled() ... }}, the builds run only after the Node dependencies, the smoke test only after the build and the browser, and the upload only when every step passed. No workflow uses continue-on-error. The new rule ciSetupErrors of scripts/repo/ci.mjs enforces this. The summary and report rule now requires if: ${{ !cancelled() }}, and AGENTS.md and AGENTS.ko.md state that a failed install never stops the suite. Red: with the runner of main, the new case a failed CI setup step marks the targets that need it not-run and runs every other target of scripts/check/full-run.test.mjs ran target a although its need rust had failed. The new case a workflow runs every setup step and make check after a failed setup step and passes the results on of scripts/repo/check.test.mjs finds errors in the workflows of G5.44. Green: the run records ci/rust failed, a not-run with that step and b and c passed, and rust-cache is not recorded; the rule passes the changed workflows and reports a missing id, an unknown id, a missing condition, continue-on-error and a missing ORM_CI_SETUP. make owner-check over the changed files passed full-run-check, version-check, repo-check, docs-rules-check and feature-unit-check; the workflows themselves run in the next CI run. | — | — | [o] |
| G5.53 | Run the independent parts of a script, a recipe and a verification command after a failed part, and refuse recipes and commands that run independent checks in sequence. Cause: a read-only audit of main G5.46 found several places that still stopped at the first failure. scripts/client-db-test.sh ran its lanes in sequence in the current shell, so the exit 1 of a failed lane's finish ended the run before the next lane. scripts/perf-test.sh, scripts/package-check.sh and databases.sh drop ran independent steps under set -e, so a failed MySQL drop left the PostgreSQL databases and the run directory behind. Seven recipes ran independent tests or lints in one recipe, whose first failure stopped the rest even under make -k: dbspec-php-check, dbspec-rust-check, conformance-result-check, dbspec-compare-check, rust-send-savepoint-check (clippy before the tests) and dbspec-stress-bench. conformance-check ran its own conformance run only after its three prerequisite checks. Six verification commands of contracts/features.json chained two tests with &&. Change: the sequential lanes each run in a subshell, and the run ends with client-db-test: failed lanes: <lanes>. perf-test, package-check (its four package checks are functions that each run in a set -e subshell) and the drop keep going and end with perf-test: failed:, package-check: failed: and databases: drop failed: and the failed steps. The seven recipes are split into parts <target>/<part>, and parts that share files use the run directory of the top target through a pattern-specific RUN_DIR, which the top target removes after every part passed. conformance-check has the part conformance-check/run beside its three checks. The chained commands are separate commands: generation-scan-typescript, generation-lib-rust, encryption-model-go, encryption-codec-rust, constraints-engine-go and constraints-driver-go, and catalog-lib-rust, which also takes over catalog-unit-rust of G5.51. The new repository rules independentTestErrors and chainedCommandErrors of scripts/repo/ci.mjs refuse a recipe with two or more different test or lint lines and a command that chains tests with &&. A second run of the same test, a compile without a run (go test -c, cargo test --no-run), an input written to a file and the build of a dependency's tool do not count. Red: the new cases of scripts/check/continue.test.mjs found that the TypeScript lane did not run after the failed PHP lane, that only the first of the four performance checks ran, that composer, cargo and go did not run after a failed npm pack, and that psql did not run and the directory stayed after a failed MySQL drop. The new repository case found the recipes of G5.52. Green: those cases pass, every lane, performance check, package and drop step runs and the failures are named; the repository case passes for the Makefile and contracts/features.json and refuses its fixtures. make dbspec-php-check, dbspec-rust-check, conformance-result-check, rust-send-savepoint-check, dbspec-compare-check and dbspec-stress-bench pass and leave no run directory, and make run-databases TARGETS=conformance-check passes. make owner-check over the ten changed files passed its 17 steps, among them package-check and the verification of the four features with split commands. | — | — | [o] |
| G5.54 | Fail a test run that selects no test. Cause: a run that selected nothing passed. node tests/go-test.mjs -v -run '^TestNoSuchName$' . exited 0, tests/cargo-test.mjs ran every binary and passed when its name filters matched no test, and ORM_CLIENT_DB_LANGS=typscript ran no lane and printed client-db-test: every lane passed:. A scan of every Go -run selection of the Makefile, the scripts and contracts/features.json with go test -list found one that ran nothing: decimal-go-physical of model_queries and model_writes selected ^TestDecimalPhysical(MySQL|Postgres|SQLite)$ without -tags decimalphysical, the build tag of that test file, so its three tests never ran (0 listed without the tag, 3 with it). Change: tests/go-test.mjs runs the test step as go test -json and renders the Output of the test2json events, test output and build output alike, as the human-readable output. A run with -run fails with go-test: -run <pattern> selected no test in <packages> when no "Action":"run" event names a test. The count comes from the documented events, not from the === RUN text of -v. -run '^$' states an empty selection and is not counted. tests/cargo-test.mjs lists each test binary with --list and the filters and arguments before it runs any; when they select no test in all binaries it runs none and fails with cargo-test: the filters <filters> select no test in <n> test binaries, and otherwise it prints the number selected. scripts/client-db-test.sh refuses a name of ORM_CLIENT_DB_LANGS that is no lane and a selection without a lane, with exit 2. The two decimal-go-physical commands pass -tags decimalphysical. Red: with the runners of main, the new case a go test run whose -run selects no test fails (a temporary module and the real go test) exited 0, the new case cargo-test fails when the name filters select no test in any test binary ran both stub binaries and exited 0, and client-db-test refuses an unknown lane name and an empty selection passed typscript and ,. Green: those fail with the messages above, the selections ^TestPresent$ and two pass, and the case the selected tests are counted from the run events of test2json counts a run event without === RUN text and ignores === RUN text without a run event. The rescan finds no other empty Go selection, every Rust filter that owner-check ran selected tests, and make owner-check over the changed files passed its 11 steps, among them decimal-go-physical, which now runs its three tests. | — | — | [o] |
| G5.55 | Leave nothing behind after a test step. Cause: the temporary directory held 1513 entries of orm runs (923M), among them orm-testcase-* 399, orm-rust-sqlite-lock-* 242, orm-conformance-runners-* 12 (399M) and orm-interface-symbols-* 10, and .runtime/run held two run directories of killed runs. Five causes made them. Some code never removed its files: tests/testcase_test.php removed the .php file but not the empty file that tempnam made for its name, and clients/rust/orm/tests/sqlite_concurrency.rs never removed its directory. Some code skipped its removal on failure: tests/interfaces/check/main.go called os.Exit(1) after defer os.RemoveAll, which then never ran; tests/conformance/check/main.go ended through must before its os.RemoveAll; and the clients/rust/orm/src/tx.rs tests removed their directories only at their end, so a failed assertion left them. A run killed by a signal ran no finally (scripts/features/coverage.mjs, tests/cargo-test.mjs). The runner copied a failed target's run directory into the report and left it on disk. The rest came from code that no longer exists (orm-gen-cli-*, orm-ts-tools-*, all made before 10-03). 133 places make temporary files, so fixing the places found would not stop the next one; and a process that a passed step left running was not seen at all. Change: scripts/check/step.mjs runs a step of the check runner and of make owner-check with a temporary directory of its own under the system temporary directory, given as TMPDIR, which every client's temporary file call follows, and as the leader of a process group of its own. After the step it lists and ends the processes left in the group, then lists the entries left in the directory and the run directories .runtime/run/*-<make pid>. A passed step that left any of them fails with check: <label> left N processes: <pid> <command> or check: <label> left N temporary entries: <names>. A failed step's temporary entries are copied into the report (targets/<step>/tmp) and its run directories into targets/<step>/run as before. In both cases they are then removed and the processes ended, and the runner ends the groups of running steps when it gets a signal. The setup steps (servers, databases, CI setup) keep the system temporary directory and the runner's group, because a server they start outlives the step, and a daemonized mysqld uses TMPDIR as its own temporary directory. The check against the old code found no state shared between steps through the temporary directory: the leases are in .runtime, the conformance lock is a literal /tmp path, and the fixed temporary names are named per run. The fixes: the interface and conformance checks end with os.Exit(run()), and must and usage return through run by a panic it recovers, so their defers run and the conformance locks are released. tx.rs holds each test directory in a Scratch value and sqlite_concurrency.rs each database in a Database value with its own directory, both of which remove it on drop, also while a failed assertion unwinds. tests/testcase_test.php removes the tempnam file. tests/cargo-test.mjs makes its run directory under the temporary directory, so a killed run's copies go with the step. make repo-check refuses a Go function that calls os.Exit after a defer (scripts/repo/gosource.mjs). Two checks that read the temporary directory follow the step's: npm turns on Node's compile cache under TMPDIR, a cache shared between runs and not a leftover of a test, so the step gives npm NODE_COMPILE_CACHE in the system temporary directory, where it was before; and the socket case of make repo-check, which needs a root short enough for mysql.sock, makes its root under /tmp, because the step directory under the macOS temporary directory already leaves less room than that. AGENTS.md states the rule and the limit that a process that starts a session of its own leaves the group and is not seen. Red: with the runner of main, the new scripts/check/step.test.mjs cases failed: a step saw the system temporary directory as its own, a passed step that made orm-leftover-probe and one that backgrounded sleep 37 passed and left both, and a failed step's entries were not kept. The extended setup-failure case of full-run.test.mjs found .runtime/run/b-4242 still on disk after the report copy. The interface check of main, run with -language rust and a results directory that does not exist, failed and left orm-interface-symbols-2642790756. The conformance check of main, run with a failing build, left orm-conformance-runners-3878254187. tests/testcase_test.php left 7 files, and the sqlite-concurrency-rust command left orm-rust-sqlite-lock-69336 after passing. The new repository rule found the five os.Exit calls of the interface check of main. Green: the step cases pass, naming left 1 temporary entries: orm-leftover-probe and left 1 processes: <pid> sleep 37, with the sleep ended and nothing left. The run directory is gone after the copy. The interface and conformance checks fail as before and leave nothing, and the conformance lock is released. tests/testcase_test.php and sqlite-concurrency-rust (6 tests) leave nothing. scratch_is_removed_after_a_failed_assertion passes. The repository rule finds nothing in the tree. make owner-check over the 19 changed paths passed its 17 steps, among them full-run-check, repo-check, testcase-check, the verification of transactions, interface_contract and conformance_verification and the coverage of interface_contract, with no step leaving a temporary entry or a process. | — | — | [o] |
| G5.55-1 | Remove the binary logs that a run's writes leave on the MySQL test servers at the end of the run. Cause: tests drop their databases, but the binary log entries their writes produced stay on the primary and the replica (with log_replica_updates, the replica writes its own). The servers held 5 binary logs of about 4.5 GB each side, and one run of scripts/check/databases.sh create and drop added 177 MB to each, so the disk grew with every run. The declared retention (MYSQLD_BINLOG in scripts/test-servers.sh) bounds the logs only when a server runs with it: the running pair reported max_binlog_size 1073741824 and binlog_expire_logs_seconds 2592000, the defaults, because it was started before that declaration. The only reader of the test servers' binary logs is the replica. Change: databases.sh drop, the last step of the check runner and of make owner-check, which also runs after a failed step, now also purges the binary logs. It rotates the primary's log, waits with SOURCE_POS_WAIT until the replica has applied the primary up to that point, purges the primary's logs up to the current one, rotates the replica's binary and relay logs, and purges the replica's binary logs up to its current one; relay_log_purge removes the applied relay logs. When the replica has not applied everything, it keeps the logs and fails the drop step, naming the file and position. This is safe for other runs on the shared servers, because only logs the replica has applied go and the current file stays. The drop step names each failed part by a label, because the commands carry DSNs, and it prints no DSN. The fake programs of scripts/check/continue.test.mjs quote the failing pattern, which a pattern with spaces and backquotes broke. Red: with the databases.sh of main, one create and drop left the primary at 5 files of 4,688,489,440 bytes after 4,511,362,870 before, and the replica at 4,688,585,640 after 4,511,458,821. Green: the new case "dropping the run databases purges the binary logs that the replica applied" passes; it runs the purge in order against a fake mysql and keeps the logs, with exit 1 and the reason, when SOURCE_POS_WAIT returns NULL. One create and drop with the new script left 1 file of 158 bytes on the primary and 1 of 317 bytes on the replica, the replica still ran both threads with no error, and the server directories went from about 4.5 GB each to 567 MB and 551 MB. The drop step of each later make owner-check printed binary logs purged up to binlog.000014. | — | — | [o] |
| G5.56 | Let no ended holder block a lease or a lock. Cause: the CI run of G5.53 failed conformance-result-check/typescript with exit 3, lease: refused the exclusive lease of .runtime/typescript.leases; held by: shared lease of pid 128962 (dead) … decimal-physical-check; 22 s earlier the same lease was reported running. A lease hold lease is removed by the releaser process (release-on-exit) after it sees its holder end, so after a holder that ends normally the file stays until the releaser removes it. A waiter that woke in that interval saw the holder gone and the file present, called it dead and refused at once, because a dead lease was never taken over and --wait did not wait for it. The same rule also left a lease of a killed holder blocking every later run until someone ran clear-dead. Two other locks had the same defect: the conformance check locked a bench database with mkdir /tmp/orm-conformance-<hash>.lock, and scripts/decimal-bench-sqlite.sh with mkdir .runtime/decimal-bench.lock; a run that ended without removing its directory blocked every later run. Every other lease user (CARGO_LEASED, HOLD_TYPESCRIPT, READ_TYPESCRIPT, WITH_TEST_ENV, the runner's and owner-check's holds, cargo-test, the interface and conformance builds, test-servers.sh) goes through tests/lease, so the fix there covers them. Change: the releaser records its pid in the lease file before hold returns, so a lease whose holder ended while its releaser runs is being released. A waiting request then waits for a change of the directory or the end of the releaser, and lease list shows it as ended, releaser <pid> running. A lease whose holder and releaser both ended is dead and blocks no one. A dead shared lease is taken over by any request, because its holder only read the resource. A dead exclusive lease is taken over by the next exclusive request, which makes the resource again. A shared request is refused while a dead exclusive lease remains, with the reason that its holder may have left the resource half made. Each takeover is reported on stderr. The conformance check holds an flock of orm-conformance-<hash>.lock in the temporary directory, which the operating system releases however the process ends, and it checks after locking that the path still names the locked file, because a releasing run removes the file before it unlocks. A refusal names the holder's pid. decimal-bench-sqlite.sh prepares under an exclusive lease of .runtime/decimal-bench.leases (lease run --wait). CONTRIBUTING.md and its Korean translation state the rules. Red: with the lease program of main, the new cases of scripts/test-servers.test.mjs failed. A waiter facing an ended holder whose releaser still ran was refused at once. A dead shared lease refused an exclusive request. hold wrote no releaser into the lease file. The new Go test TestLockOfAnEndedRunDoesNotHold, with the main.go of main, failed with mkdir /tmp/orm-conformance-ad16a2804aebc7ba.lock: file exists after a run ended without releasing. The decimal-bench-sqlite.sh of main, with the directory a killed preparation leaves, exited 1 with decimal SQLite bench preparation is already running. Green: the waiter waits while the releaser runs and takes the lease over when the releaser ends without removing it. A dead shared lease is taken over by an exclusive request. A dead exclusive lease refuses a shared request with the reason and is taken over by an exclusive request. hold records a live releaser, which removes the lease when the holder ends. The conformance lock of an ended run no longer holds, and a running holder's lock refuses with its pid. make decimal-bench-sqlite with a dead exclusive lease reported lease: took over the dead exclusive lease of pid 50342 and prepared the database. make owner-check over the 8 changed paths passed its 13 steps, among them test-servers-check, full-run-check, repo-check, the decimal-bench-sqlite helper and the verification of conformance_verification. | — | — | [o] |
| G5.56-1 | Start the lock helper test with the shared testcase package. Cause: TestLockHelper of tests/conformance/check/lock_test.go, added by G5.56, skipped or ran without testcase.Start, so make repo-check failed on main with TestLockHelper does not start its case with internal/testcase, so it runs without a deadline or RUN line. The owner-check of G5.56 passed because the repository check reads tracked files and the test file was not yet tracked then. Change: the helper starts its case with testcase.Start (Compute), and the test that runs it reads the helper's output line by line until locked, because the helper now prints its RUN line first. Red: on main, the repository case "every Go and Rust test starts its case with the shared testcase package" failed with that line. Green: it passes, and TestLockOfAnEndedRunDoesNotHold passes. | — | — | [o] |
| G5.57 | Keep the errors that a FAIL line points at as first failure lines. Cause: in the CI run of G5.53, performance_gate/preempted-timing failed with FAIL TestStressDocument elapsed=10.25s: the errors reported above, and neither the summary nor the report said what those errors were. The Go testcase writes a failure as stress_test.go:80: <message> and then only that FAIL line. The first failure lines took only failure and error-level lines, and the report's target log keeps its first and last part, so the reported line was in neither. Change: failures() of scripts/check/report.mjs keeps the source-location report lines (<file>.go|rs|php|mjs|js|ts:<line>: <message>) of the current case, reading through the lane prefix and the nested STEP <case> elapsed=…: prefixes of a runner inside a runner. It forgets them when a case starts or passes, and puts them as first failure lines right before a failure line that ends with the errors reported above. They go into the run record and the summary, which the log cap does not shorten. Red: with the report.mjs of main, the new case "a FAIL that points at the errors reported above keeps those errors as first failure lines", on the nested lines of that run, found only the FAIL line. Green: the first failure lines are the stress_test.go:80 report and then the FAIL line, without the log line of a case that passed before. The earlier first-failure-line cases pass, and make owner-check over the changed paths passed full-run-check and repo-check. | — | — | [o] |
| G5.58 | Measure each stress parse against a reference workload of the same machine, printed and not yet asserted. Cause: the CI run of G5.53 failed the PHP stress parse at a median of 410.754 ms and, preempted, 444.392 ms of process CPU (getrusage user and system time, clients/php/tests/case_clock.php) against its 400 ms budget, and the same machine gave no evidence whether the budget or the code was wrong. The four budgets of docs/spec/dbspec.md are absolute times fixed from the first measurement on the development machine, which measures 300 to 349 ms for the same parse. A budget that holds on one machine and fails on another states nothing about the parser. A ratio to a fixed workload measured by the same clock in the same process would cancel the speed of the machine, if that ratio is stable across machines. Change: the Go, PHP, TypeScript and Rust stress tests also time a reference workload five times: one pass over the bytes of the same stress document computing h = h * 31 + b (mod 2^32), a sequential loop that no compiler can parallelize. They print its median CPU time and the ratio of the median parse to it, and assert nothing new. The budgets stay as they are until CI has printed its ratios. Red: the stress tests of main print no reference time and no ratio. Green: make dbspec-stress-bench passed and printed, on the development machine (two runs each), the reference median and ratio. Go: 2.106 and 2.561 ms, ratio 26.70 and 24.97 (parse 56.2 and 63.9 ms). PHP: 38.145 and 47.359 ms, ratio 8.24 and 6.35 (parse 314.3 and 300.6 ms). TypeScript: 2.282 and 2.509 ms, ratio 52.38 and 49.57 (parse 119.5 and 124.4 ms). Rust: 2.494 and 2.741 ms, ratio 23.98 and 19.87 (parse 59.8 and 54.4 ms). The checksum is 3992179881 in all four, so they did the same work. The ratio varies by up to 23 % between two runs on one machine (PHP, Rust), which bounds how tight a ratio budget can be. | — | — | [o] |
| G5.59 | Drop the databases of a killed run before a run makes its own. Cause: a run drops its bench and decimal databases in its last step, which a killed run never reaches; the shared MySQL and PostgreSQL test servers held orm_owner_81522_19e14363_bench and _decimal from such a run, and nothing removed them. Change: scripts/check/databases.sh create first runs drop_dead_runs (also the action drop-dead). It lists the databases of both servers and drops each orm_owner_<pid>_… and orm_check_<pid>_… bench or decimal database whose process no longer runs on this host (ps -p), naming each. The test servers listen on 127.0.0.1 only, so every run is a process of this host. Databases of live runs, CI names (ci_<id>) and other databases stay. Red: the databases.sh of main has no such step; the new case "a run first drops the databases of runs whose process no longer runs" failed with its usage line. Green: against fake mysql and psql listing seven databases, it drops the three of the ended pid on each server and keeps the live run's database, the CI database and orm_test; the other cases of scripts/check/continue.test.mjs pass. | — | — | [o] |
| G5.60 | Refuse an os.Exit reached through a helper after a defer. Cause: the rule of G5.55 refused only a direct os.Exit after a defer in the same function. Five programs reached it through a helper of the same package after defer db.Close(): bench/install (fail), examples/complex/go and examples/thin-slice/go (check, dsn), tests/conformance/runner_go (check) and tests/dbspec/introspect/go (fail). A failed step there ended the process without closing the connection, and in the examples, which show users how to write a program, the same pattern was taught. Change: deferredExitErrors of scripts/repo/gosource.mjs treats a function of the same package (directory) that calls os.Exit, directly or through such a function, as exiting, and refuses a call to it after a defer, naming the helper. The five programs end with os.Exit(run()). Their helpers panic with an exit code (type exit int) that run recovers into its result, so the deferred close runs, and a usage error inside run returns its code. Red: the widened rule found 18 calls in those five programs on main. Green: the new case with a check that calls a fail of another file of the package is refused with main calls check, which calls os.Exit, after a defer, and the tree has none. Each program still exits 2 on a usage error and 1 with its message on a failed connection or a missing ORM_BENCH_MYSQL_DSN. | — | — | [o] |
| G5.61 | Check the process group of every process that a script starts in a group of its own. Cause: G5.55 checks the process group of each step, but a process that starts a group of its own leaves the step's group, so the step does not see what that group leaves. Five scripts start their commands with spawn(…, { detached: true }) so that a deadline can kill the whole group: scripts/decimal-physical-check.mjs, scripts/group-rows-physical-check.mjs, scripts/unselected-column-physical-check.mjs, scripts/features/coverage.mjs and tests/timing/preempted.test.mjs. They killed the group only on a deadline or an output limit, so after a normal exit any process left in it ran on unseen. Change: endGroup of scripts/check/step.mjs lists the processes left in a group after its leader exits, ends them (TERM, then KILL after 3 s) and returns them. The five scripts call it when the command exits and fail the command with <program> left N processes: <pid> <command> when it returns any. make repo-check refuses a JavaScript file that starts a process with detached: true and never checks the group (detachedGroupErrors of scripts/repo/gosource.mjs), and it ignores the words in comments and string literals. The releaser of tests/lease is outside this rule, because it is meant to outlive its parent until the holder ends. Red: the new rule found the five scripts on main. Green: the rule finds none in the tree and refuses its fixture without endGroup. The new case "endGroup names and ends what a process of its own group left behind" gets <pid> sleep 39 for a group whose leader exited while a background sleep 39 ran, the sleep is ended, and a second call returns nothing. The step cases and scripts/features/coverage.test.mjs give the same results as on main. | — | — | [o] |
| G5.62 | Start the test servers outside the caller's process group. Cause: make test-servers started PostgreSQL, its replica, ProxySQL and PgBouncer through start_logged of scripts/test-servers.sh with their log reader shells in the process group of the shell that ran make (all in pgid 75915 on 10-06; only the mysqld pair, which daemonizes, had groups of its own). Ending that group, as an interrupt in a terminal, a CI step kill or a tool that ends its command's group does, also ended those servers, which other runs share. Change: start_logged runs each server and its log reader through tests/new-session (NEW_SESSION, built by make like STOP_PROCESS), which opens a new session (setsid) and then runs the command, so the servers and readers run outside the starting command's process group; the ready handshake through the FIFO is unchanged. make test-servers and make repo-check build it. Red: with the test-servers.sh of main, the start_logged case of scripts/repo/check.test.mjs failed with the server runs in the process group of the command that started it. Green: the case passes, the fake server's process group differs from the test's, and the repository tests pass (66 cases); the real servers start in CI. | — | — | [o] |
| G5.63 | Lock the table before a PostgreSQL insert publishes Locked. Cause: the CI run of G5.54 failed catalog_connection/catalog-rust with generated identity failures: ["postgres: generated insert must lock the table before publishing Locked"] at orm-build/tests/row_insert_identity.rs:109:5; the same case passed in the run of G5.53. On PostgreSQL, insert_row of clients/rust/orm-build/src/catalog/row_insert.rs took no lock of its own before publishing MutationPhase::Locked: the schema check (metadata::describe) reads only the catalogs, and whether its pg_get_expr held a lock on the table was a side effect, so a concurrent LOCK TABLE … ACCESS EXCLUSIVE NOWAIT sometimes succeeded, and a schema change could land between the check and the write. MySQL already takes its lock explicitly (SELECT 1 … FOR UPDATE). Change: on PostgreSQL the insert first runs LOCK TABLE <table> IN ROW EXCLUSIVE MODE (postgres_insert_lock), the lock an INSERT takes and one that conflicts with every schema change, before it checks the schema and publishes Locked. Red: on main the PostgreSQL insert held no explicit lock, and CI found the probe's lock granted. Green: the new unit case a_postgres_insert_takes_the_insert_lock_of_its_table pins the statement, quoting included; cargo fmt --check and cargo clippy -p orm-build --features live-db --lib --tests -D warnings pass. The database case runs in CI. | — | — | [o] |
| G5.64 | Require no local end-to-end check before a push. Cause: AGENTS.md required make owner-check before each commit, during an item allowed the Red/Green tests together with make owner-check, and asked to verify each linux-runner verification locally before a push. Those runs start real database servers, builds and suites on the development machine, which CI runs after every push anyway, and each of them held up the work for minutes. Change: AGENTS.md and its Korean translation state that during development only unit tests run, the Red/Green unit cases of what changed. End-to-end runs (real database servers, conformance across languages, browsers, containers, full builds), make owner-check and the full suite run in CI after the push, and no local check is required before a push. make owner-check stays a tool that no rule requires. A changed file's readers are run as their unit tests, and the linux-runner verifications run in CI. Red: the texts named make owner-check before each commit and a local linux-runner run before a push. Green: neither remains a requirement, and node scripts/docs/rules.mjs passes on both translations. | — | — | [o] |
| G5.64-1 | Check the new wording of the full-suite rule. Cause: make repo-check requires AGENTS.md and its translation to state the full-suite rule by fixed phrases (FULL_SUITE_RULE of scripts/repo/ci.mjs), among them "is no required step before a push" and "push 전에 필요한 단계가 아니다". G5.64 rewrote that sentence without updating the phrases, so main failed make repo-check with AGENTS.md does not state the full-suite rule; the docs-only commit ran only the document rules. Change: the rule requires "no local check is required" and "push 전에 필요한 로컬 검사는 없다", the wording G5.64 introduced. Red: on main the case "AGENTS states that the full suite runs on CI after a push" and the repository run failed on both translations. Green: both pass. | — | — | [o] |
| G5.65 | Run every check offline and download only through make install. Cause: checks reached the network in the middle of a run. rust-fetch, a prerequisite of 20 Rust targets, ran cargo fetch online inside make check; go test and go run downloaded missing modules from the default proxy; scripts/typescript/node-min.sh downloaded the lowest Node with curl during ts-min-check; and nothing told a run apart that found its downloads in a cache from one that fetched them, so the same tree could pass or fail by what a registry answered. A missing crate failed with cargo's "retry without the offline flag". Change: the Makefile exports CARGO_NET_OFFLINE=true, GOPROXY=off, npm_config_offline=true and COMPOSER_DISABLE_NETWORK=1, and ONLINE unsets them for the download targets only: make install with install-node, install-php, install-rust (rustup toolchain install and cargo fetch --locked for the three Cargo.lock files), install-go (go mod download) and install-node-min. scripts/check/downloads.mjs checks without network that the crates (cargo fetch --locked --offline), the Go modules (go mod download under GOPROXY=off), the npm packages of both package-lock.json files and the Composer packages of composer.lock are present at their locked versions, and names each missing one with run make install, which downloads it, never with a retry online. The runner records it as the setup step downloads, and a target that needs a missing download (rust, go, node-modules, composer) is not run, with that reason. make owner-check stops with the same messages. cargo-downloads-check replaces rust-fetch as the prerequisite of the Rust targets, and node-min.sh downloads only under make install-node-min. CI downloads in its setup steps (make install-rust, a go-modules step with make install-go, make install-node-min), and the repository server-order rule allows make install targets before the servers. make repo-check refuses a Makefile without the four offline settings, a download command outside an install target and $(ONLINE) outside one (offlineMakeErrors). AGENTS.md, CONTRIBUTING.md and docs/operations/docs-development.md with their translations state make install and the offline rule. Red: on main the Makefile exported none of the four settings and rust-fetch ran cargo fetch online, which the new rule names. Green: the four new cases of scripts/check/downloads.test.mjs pass: lock files against installed versions, a missing crate and Go module named with make install and no retry, node-min.sh refusing to download outside install-node-min, and the Makefile rule. The runner, continue, step, repository and owners unit tests pass (30, 9, 6, 65 and 19 cases), the check finds every download present in the main checkout, and it names the missing npm and Composer packages in a fresh worktree. | — | — | [o] |
| G5.64-2 | State in CONTRIBUTING.md that checks beyond unit tests run in CI. Cause: G5.64 rewrote AGENTS.md, but CONTRIBUTING.md and its translation still told a contributor to "run the relevant local checks" and to "run the full suite make check once, when every active checklist item is complete". Change: step 5 runs the unit tests of what changed, its Red/Green cases, with end-to-end runs, make owner-check and the full suite in CI after the push and no local check required before a push; step 6 states that make check runs in CI after every push, with its refusals as before. Red: the two sentences stood in both translations. Green: neither remains, and the document rules and the repository check (65 cases) pass. | — | — | [o] |
| G5.66 | Run every CI step through a make target. Cause: the workflows ran commands that no make target ran, so CI did things a local run could not repeat and no rule checked. ci.yml ran npm ci twice and npx playwright install, rustup toolchain install, an inline echo version=… for the lowest PHP, an inline php -r SQLite probe, composer install and a 15-line apt, curl and AppArmor script for the database server programs; docs-pages.yml ran npm ci and npx playwright install. Change: those steps run make install-node, make install-node-min, make install-browsers, make install-rust (which runs rustup toolchain install and prints rustc --version), make --no-print-directory ci-php-min-version >> "$GITHUB_OUTPUT", make ci-php-sqlite (scripts/ci/php-sqlite.php), make install-php and make install-server-programs (scripts/ci/server-programs.sh), and the node, browser and lowest-Node installs are separate setup steps (node-modules, node-min, browsers), each declared in scripts/check/ci-setup.mjs. make repo-check refuses a workflow step that runs anything but a make target (ciMakeErrors of scripts/repo/ci.mjs), except the summary step of ci.yml, which runs node scripts/check/summary.mjs also after a runner that did not finish and is named in CI_RUN_EXCEPTIONS. The server-order and rerun rules allow the declared CI setup steps, which install and probe and run no test, before the servers and outside make check. The PHP and Rust toolchain rules read the minimum PHP step and rustup toolchain install through those targets. The two new scripts are declared with scope suite. Red: the new rule found 24 commands outside make in the two workflows of main. Green: the new case "every CI step runs a make target" passes on its fixtures and finds nothing in the tree, the repository check passes (66 cases), and make ci-php-min-version prints version=8.4. | — | — | [o] |
| G5.67 | Leave a report of the documentation workflow. Cause: docs-pages.yml ran make docs-build, make docs-verify-idempotent and make docs-static-check as plain steps, kept nothing but the job log, and wrote no summary, so a failure left no report to read and a step skipped after a failed one said nothing about why. Change: one step runs make docs-ci, which runs the three targets through the runner of make check (scripts/check/run.mjs) without test servers (-). The runner then has no server or database step, records a target that needs a database as not-run with this run has no database servers, checks only the downloads that its targets need, and runs every target after a failed one. It writes the log of each target and a summary to .runtime/check/ci_<run id>_<attempt>_docs/report and, for a run without a full-run record, also to the job summary. A report step uploads that directory under !cancelled(), and the pages artifact is uploaded only when the documentation checks passed. Red: the workflow of main kept no report, and the runner required a servers file. Green: the new case "a run without servers runs the targets that need no database and reports them" runs docs-a, records db-b as not-run with that reason, leaves out the server and database steps, keeps the downloads step passing although a crate its targets do not need is missing, and writes the summary and the target log. The runner (31 cases) and repository (66 cases) tests pass. | — | — | [o] |
| G5.68 | Assert behaviour, not wall time, in two tests that broke under load. Cause: two tests bounded wall-clock time from above, which a loaded machine exceeds without any defect. clients/typescript/tests/model.mjs required a statement cancelled while it waited for a row lock to return within 4000 ms, and the start_logged case of scripts/repo/check.test.mjs required start_logged to return within 10 s and the slow server to be ready only after 2.5 s. The requirement is not a time in either: the statement must end because of the signal, and start_logged must return after the ready line and before the server ends. Change: the cancellation case checks that the statement returns CANCELED and that the signal had been aborted, while the other connection still holds the row, so neither a released lock nor a lock wait timeout could have ended it. The start_logged case checks that the fake server still runs when start_logged returns, and the slow server creates a marker file right before its ready line, which must exist when start_logged returns. The lower bounds that remain (a wait past a 200 ms bound, two builds through a slow go) do not break under load. Red: the two upper bounds of main. Green: the start_logged case passes on its behaviour, and the TypeScript case runs in CI. | — | — | [o] |
| G5.63-1 | Keep the PostgreSQL insert lock out of the Rust interface and declare its unit test. Cause: two CI runs failed conformance-check and interface_contract with rust: unexpected clients/rust/orm-build/src/catalog/row_insert.rs::postgres_insert_lock, because G5.63 declared the helper pub(super) and the interface check lists every non-private symbol against contracts/interfaces.json; and feature-unit-check failed every tracked file selects a behaviour test or declares its scope for clients/rust/orm-build/tests/unit/row_insert_lock.rs. Change: postgres_insert_lock is private to row_insert, whose child test module still reaches it, and the unit test file is an input of catalog_connection and of its catalog-lib-rust verification, beside the other catalog unit tests. Red: the two failures of those runs. Green: the unit case passes with the private helper, node scripts/features/owners.mjs --list selects catalog-lib-rust for the file, and the owners tests pass (19 cases). | — | — | [o] |
| G5.69 | Remove what the documentation browser leaves in the temporary directory. Cause: a CI run failed docs-check with check: docs-check left 1 temporary entries: .org.chromium.Chromium.1Yiv3c. Headless Chromium creates .org.chromium.Chromium.* in TMPDIR and, on Linux, sometimes leaves it after browser.close(); since G5.55 a passed step that leaves an entry fails. Four scripts of scripts/docs launched Chromium with the step's TMPDIR. Change: launchBrowser of scripts/docs/browser.mjs launches Chromium with a temporary directory of its own as TMPDIR and removes it when the browser closes, also after a failed close, and check.mjs, prepare.mjs, load.test.mjs and steps.test.mjs launch through it. npm run docs:static-check runs the new browser.test.mjs. Red: on main the four scripts called chromium.launch with the step's TMPDIR, which the new case refuses, and CI found the leftover. Green: the new case "the documentation browser gets a temporary directory of its own, removed when it closes" passes against a fake Chromium that leaves a .org.chromium.Chromium.left entry, and no script of scripts/docs launches Chromium directly. The real browser runs in CI. | — | — | [o] |
| G5.70 | Wait for this case's own lock wait before cancelling in the Go cancellation tests. Cause: a CI run failed TestWithContextCancelsInsideTransaction/postgres/nested=true with blocked read: CANCELED: timeout: context already done: context canceled (code "CANCELED"), want ROLLBACK. The case cancels its context once awaitLockWait sees a session of the case database waiting for a lock, which it counted with count(*) over all waiting sessions. The statement that the nested=false case had cancelled could still wait on the server after its connection closed, until the holder released the row, so the next case found that session, cancelled its context before its own statement started, and the statement ended as context already done with no rollback to fail. Change: the case reads the waiting sessions (pg_stat_activity pids, performance_schema.data_locks thread ids) before its statement starts, and awaitLockWait returns only when a session that was not among them waits (newWaiter); the other cancellation case does the same. Red: the run above; with the count of main, a session that waited before counts as this case's statement. Green: the new unit case TestNewWaiterIgnoresSessionsThatAlreadyWaited pins that a session waiting before is ignored and a new one is found, and go vet passes; the database cases run in CI. | — | — | [o] |
| G5.71 | Leave out the failure lines of a case that passed. Cause: in a CI run the first failure lines of feature-unit-check were FAIL sample/owner/typescript/none run 1 …: unexpected test output, duplicate observed case first and more. Those lines are the output of samples that a case of scripts/features/coverage.test.mjs makes fail on purpose, and that case passed; the real failure, every tracked file selects a behaviour test or declares its scope, came after them and fell outside the 20 first failure lines, so the summary named no real cause. Change: failures() of scripts/check/report.mjs records, for each case, how many failure lines there were at its RUN line, and when the case ends with PASS it drops the lines collected since, because they are that case's output and not a failure of the target. Lines are collected without the limit and cut to the first 20 only when they are read, so a dropped block no longer pushes a later failure out. Red: with the report.mjs of main, the new case "the failure lines of a case that passed are not the first failure lines of the target" got the 25 sample lines in place of the real failure. Green: its only first failure line is the FAIL line of the tracked-file case, and the earlier first-failure-line cases pass (32 cases). | — | — | [o] |
| G5.72 | Probe a catalog mutation's locks from inside its Locked publisher. Cause: a CI run failed native_updates_lock_compare_verify_and_rollback with update completed before held-lock probe: true at orm-build/tests/row_mutation.rs:137. The test drove the update's future until its publisher reported Locked and then stopped polling it, to probe the row lock from another connection; the identity test of row_insert_identity.rs probed the table lock the same way. Stopping the polls does not stop a future that needs no more polls: when the server answers each statement before the client reads (a loaded runner, a local server), the update reads every answer within the same poll and runs to its commit before the test gets to probe. Change: the probes run from the publisher itself at Locked, on a thread and a connection of their own (tests/lock_probe/mod.rs: row_lock_held, table_lock_held), and the publisher returns only after the probe; the mutation is thereby stopped exactly at Locked whatever the scheduling. After the probe the publisher sets the cancellation flag, so the mutation ends with JOB_CANCELLED and rolls back, which leaves the rows as the rest of each test expects. The new module is an input of the two catalog verifications. Red: the run above. Green: the new unit case a_probe_inside_the_locked_publisher_sees_the_mutation_at_locked, with a fake mutation that publishes Locked and finishes within its first poll, sees the mutation at Locked; cargo clippy -p orm-build --features live-db --tests -D warnings and cargo fmt --check pass. The database cases run in CI. | — | — | [o] |
| G5.74 | Print an allocating reference beside the Go stress parse. Cause: the checksum reference of G5.58 does not track the Go parse across machines: its ratio was 25.0 and 26.7 on the development machine and 38.7 to 43.0 on the CI runner, where a CI run measured the parse at 102 ms against the 100 ms budget. A byte loop measures the speed of one tight loop, while the parse slices strings, allocates and hashes, and the two scale differently from one processor to another. Change: TestStressDocument of engine/dbspec/stress_test.go also times, five times on the locked thread's CPU clock, an allocating reference: split the document into lines and words and count each word in a map (allocatingReference, 22066 distinct words). It prints its median CPU time and the parse's ratio to it, and asserts nothing new, until the CI runner shows whether that ratio holds. Red: the test of main prints only the checksum reference. Green: the test passes and prints stress allocating reference cpu median=… ratio=… words=22066; on the development machine five runs gave ratios 5.26, 5.89, 4.11, 5.33 and 5.03 (allocating reference 10.2 to 13.7 ms), beside checksum ratios 25.4 to 30.9. | — | — | [o] |
| G5.73 | Measure and report performance, and never fail a test on it. Cause: tests failed on measured time. The four stress tests failed when the median parse CPU time exceeded an absolute budget (Go 100 ms, PHP 400 ms, TypeScript 250 ms, Rust 300 ms); the PHP and Go budgets failed on the CI runner at 386–444 ms and 88–102 ms with no change in the parser; 20 Rust assertions and 5 PHP checks failed when a case's CPU time exceeded a limit; make perf-check failed when a hot-path ratio exceeded its bound; the Go introspection comparison failed above 5 s; and two Go cancellation tests failed when the cancelled statement returned after 4 s or 10 s. A reference-ratio budget was tried on the way: on macOS the PHP ratio moved from 6.35 to 10.1 and the Rust ratio from 17.3 to 24.0 across runs, because performance and efficiency cores change both times, while the CI runner gave 6.84–7.16 and 21.8–22.5. Performance is measured and reported and never fails a test; a warning is allowed. Change: each stress test prints the CPU and wall-clock time of each parse, the median, the median CPU time of the checksum reference, the ratio and the machine, and prints a WARNING line with the measurement, the reference and the machine when the ratio exceeds its reference value: 11 for PHP, 34 for Rust, 63 for Go and 185 for TypeScript, 1.5 times the highest ratio measured on the CI runner, so a warning marks a regression of about 50% or more. The CPU limits of the Rust and PHP cases, cpuCaseEnd, the Rust stress run, the hot-path gates of Go and PHP and the introspection comparison print a warning instead of failing; the two Go cancellation tests print the duration, since the statement returning CANCELED while the rows are still held already shows that the cancellation ended it; and the case-clock test compares its CPU time with its own wall-clock time. The testcase libraries gain warning (JavaScript, Rust), Warn (Go) and testcase_warning (PHP). The check runner keeps WARNING lines out of the first failure lines, records them per step, lists them in the summary under warnings and writes them as GitHub ::warning:: annotations. make repo-check refuses a test that fails on a measured time above a bound (timeFailureErrors: an if that fails when a measured time exceeds a fixed bound, or an assertion that a measured time stays below one; lower bounds, comparisons of two measurements and comparisons with 0 are correctness). The allocating reference of G5.74 is removed. docs/spec/dbspec.md, docs/operations/perf.md, docs/spec/dialects.md, the performance_gate feature and AGENTS.md, with their translations, state the rule, the reference values and why macOS only prints. Red: the repository rule found 38 time failures on main, and on main the PHP stress test failed on the CI runner at 406–444 ms. Green: the rule finds none in the tree; with its reference forced to 1, the PHP stress test printed WARNING dbspec_stress median parse CPU of 248.765 ms is 9.22 times the reference CPU of 26.986 ms, above the reference ratio 1.0 … and passed; the Go and Rust stress tests pass and print their ratios (26.93, 18.44); the new cases "a WARNING line is kept as a warning, not a failure, and the summary lists it" and "a test does not fail on a measured time above a bound" pass; go vet ./..., cargo clippy --workspace --tests --examples -D warnings and the runner (33) and repository (66) tests pass. | — | — | [o] |
| G5.75 | Let a new push cancel the run of the push before it. Cause: ci.yml declared no concurrency and docs-pages.yml kept its runs with cancel-in-progress: false, so every push started a run that went on after a newer push; with few runners, superseded runs of the same branch held them while only the newest head's run counts. Change: both workflows declare concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }, and make repo-check refuses a workflow that runs on push without that workflow-level group (concurrencyErrors of scripts/repo/ci.mjs). Red: the new rule found ci.yml and docs-pages.yml of main. Green: the new case "every workflow that runs on push cancels the run of the push before it" passes on its fixtures (a workflow without the group or with cancel-in-progress: false is refused, one without push is not checked) and finds nothing in the tree, and the repository check passes. | — | — | [o] |
| G5.76 | Record the PostgreSQL insert lock in the Rust symbol inventory and format the introspection test. Cause: the CI run of G5.75 failed three targets. conformance-check and the interface_contract coverage reported rust: unexpected clients/rust/orm-build/src/catalog/row_insert.rs::postgres_insert_lock: the Rust symbol extractor of tests/interfaces/rust lists every top-level function whatever its visibility, so making the helper private in G5.63-1 did not remove it, and the inventory contracts/symbols/rust.json lacked it. go-fmt-check failed on tests/dialects/introspect_compare_physical_test.go, whose new constant block G5.73 left unformatted. Change: the inventory records the helper (go run ./tests/interfaces/check -language rust -record), contracts/interfaces.json carries the new hash of the inventory, and the test file is formatted with gofmt. Before each commit the Go files are now checked with gofmt -l. Red: the run above. Green: go run ./tests/interfaces/check -language rust passes with 2781 native symbols, the Go, PHP and TypeScript interface checks pass, and gofmt -l lists no file. | — | — | [o] |
| G5.77 | Keep large test copies out of memory, guard group signals and stream the runner's resources. Cause: the second attempt of the CI run of G5.76 lost its hosted runner about 52 minutes into make check, with no summary or report uploaded; GitHub names a process that ends the runner, CPU or memory starvation or a blocked network. Two changes of this series could do it. (b) Since G5.55, tests/cargo-test.mjs copied every test binary it runs into the step's TMPDIR under /tmp, which on the CI runner is a tmpfs of 7.8 GiB in memory, so up to 19 Rust test binaries per verification lived in RAM beside the database servers, replicas, ProxySQL, PgBouncer and parallel builds on a 16 GB machine. (a) The group signals of G5.55 and G5.61 send kill(-group); nothing refused a group of 0 or 1 or the runner's own group, which would end the runner and its job. No log showed which, because the job log stopped without resource lines. Change: tests/cargo-test.mjs makes its run directory in the checkout's .runtime/run on disk, named after the step (ORM_STEP, which runStep now passes), and runStep removes a run directory named after its step as that step's leftover, so a killed copy is still removed. signalGroup of scripts/check/step.mjs refuses a group of 1 or less, the runner's own pid and its own process group, prints the refusal, and records every signal it sends. scripts/check/resources.mjs prints, every 30 s while the runner runs and at its start, RESOURCES lines to the job log: free -m, df -h / /tmp, the five processes with the largest RSS with their process groups, and the group signals sent since the last lines; the streamed log survives a lost runner. Red: on main the test copies went to /tmp and signalGroup had no guard; the control of the new case shows a raw signal to the runner's own group ends a fake runner with SIGTERM. Green: the new cases pass: "a group signal never reaches the process group of the runner" (a fake runner in its own group refuses five signals, its own group, 0, 1, -5 and its pid, runs a step that leaves a background process, and stays alive), "a run directory named after the step is a leftover of the step" and "the resource lines list the processes with the largest resident memory"; the cargo-test (7), step (9) and runner (33) tests pass. The next CI run shows the resource lines. | — | — | [o] |
| G5.78 | Find test files from the working directory, not through runtime.Caller. Cause: engine/dbspec/harness_test.go (repositoryRoot) and tests/interfaces/check/grouped_contract_test.go found the repository through runtime.Caller(0), which gives the source path compiled into the binary, not the checkout that runs it: a module path under -trimpath, and the directory of another checkout for a binary built there. | — | Evidence: both tests take the working directory (os.Getwd), which go test sets to the package directory, and go up to the root; callerPathErrors of scripts/repo/gosource.mjs refuses a runtime.Caller or runtime.Callers call in a tracked Go file with its line and the fix, and the repository unit case found the two calls in the previous sources and none now. TestFileVectors, TestGetsCountUsesDedicatedGroupedRowsInEveryClient and make repo-check pass. | [o] |
| G5.79 | Pin every toolchain the checks use. Cause: Node (.node-version) and Rust (rust-toolchain.toml) were exact and PHP (.php-version) was x.y, but CI installed the newest Go 1.27 (go-version: "1.27"), the newest Composer of setup-php, the newest libclang-dev and the newest PostgreSQL 17, and no check compared the Go or Composer that ran with a declaration, so a release of any of them changed the checks between two runs of the same commit. | — | Evidence: .go-version (1.27.0) and .composer-version (2.10.3) declare the exact releases; ci.yml reads go-version-file: .go-version and every setup-php step installs tools: composer:2.10.3; goVersionErrors and composerVersionErrors of scripts/repo/toolchains.mjs refuse an inexact declaration, a go.mod go line of another x.y, a workflow that declares the release itself or omits it, and a running go or composer of another release, each with its fix (two new repository unit cases); scripts/ci/php-extension-tools.sh installs libclang-21-dev, the LLVM of Ubuntu 26.04, and names it in LIBCLANG_PATH; scripts/ci/server-programs.sh requires PostgreSQL 17 and prints the PgBouncer and SQLite releases. setup-php and the PostgreSQL apt repository offer only the newest patch of PHP x.y and PostgreSQL 17, so those stay pinned at x.y and the major release. make repo-check, full-run-check and feature-unit-check pass. | [o] |
| G5.80 | Publish build outputs atomically: build into a temporary file or directory, then rename. Cause: go build -o wrote the lease program, stop-process and new-session of .runtime/bin, which every run of the checkout executes, and the timing test binary in place; node tests/dbspec/stress.mjs > <document> redirected into the stress documents; tsc wrote clients/typescript/dist file by file; scripts/cargo-build-copy.sh copied into the run directory in place; and scripts/typescript/node-min.sh extracted Node into its cache directory, so a stopped build left half a file or directory that a later run read as complete. | — | Evidence: scripts/publish-output.sh runs a command into <output>.next-<pid> (the argument @OUT@, or the standard output) and renames it over the output, removing it when the command fails (a failing command left the previous file); the Makefile builds the three programs, the stress documents and the timing binary through $(PUBLISH); scripts/typescript/build.mjs builds into dist.next-<pid> and renames it into place for TSC_BUILD and the npm build (the models stayed unchanged); cargo-build-copy.sh and node-min.sh copy and extract into a temporary path and rename. unpublishedOutputErrors of make repo-check refuses a Makefile line that writes a Go build output, redirects into a file or runs tsc in place, with the fix; it found the nine lines of the previous Makefile and none now. make repo-check, full-run-check, feature-unit-check and testcase-check/node pass. | [o] |
| G5.81 | Name the cause and the fix in every failure message of a missing run condition. Cause: 98 failure messages of the checks and tests (scripts, tests, bench, examples and the client tests) stopped at the cause when an environment variable, an argument or a tool was missing, such as ORM_TEST_MYSQL_DSN is required; database tests never skip, ORM_FEATURE_DSN is required, BENCH_MYSQL_DSN is required, test-servers: openssl is not installed and ORM_CHECK_RUN_ID is unset, so the reader had to find the target or step that provides it. | — | Evidence: each now adds the fix after the cause: the make target that sets the server environment, make check or make run-databases for the bench databases of a run, make feature-check for the coverage variables, make dbspec-compare-check for the stress document, make example-check for the built programs, make install-server-programs or the package manager for a tool, the argument to give, or the release to install; two test assertions of a missing class state what is missing instead. fixlessMessageErrors of scripts/repo/messages.mjs refuses a failure line of that scope that names a missing condition (is required, is unset, is not set, is not installed) without a fix clause (; run, ; set, ; install, ; give and the like, or , which); it found the 98 lines in the previous sources and none now. go vet with every build tag, cargo check of the Rust tests and bench, php -l, node --check, sh -n, rust-fmt-check, repo-check, full-run-check and feature-unit-check pass. | [o] |
| G5.82 | Hold every shared port and database by a lease or own it per run. Cause: the test servers listened on the fixed ports 33171, 55471, 33181, 55481, 33182 and 55482 of the Makefile, which no lease covered across checkouts: the lease of the servers lives in the .runtime of one checkout, so the servers of a second checkout (a worktree) contended for the same ports. The databases were already owned: each run creates its bench and decimal databases (scripts/check/databases.sh), each database case its orm_case_<pid>_<n> database (internal/testdb and its peers), the Rust tool tests name their tables in orm_tools by process id, SQLite files are per process, and every run holds the shared lease of its checkout's servers (WITH_TEST_ENV). | — | Evidence: test-servers.sh start chooses six free ports on 127.0.0.1 (scripts/free-ports.mjs) when it starts the servers of a checkout, so they own their ports from the moment they listen; a start or tls of running servers reads the ports from the DSNs of .runtime/servers/env (env_ports, which names the missing variable and the fix); the Makefile names no port; CONTRIBUTING.md and its Korean pair describe the chosen ports. fixedPortErrors of make repo-check refuses a literal *PORT= assignment and a test-servers.sh start with ports in the Makefile and the shell scripts; it found the seven lines of the previous Makefile and none now. The socket path case starts the script without ports. make repo-check, full-run-check, feature-unit-check, docs-rules-check and docs-check pass; the servers themselves start in CI. | [o] |
| G5.83 | State the rules of G5.78 to G5.82 in AGENTS.md. Cause: the pinned toolchains, the atomic build outputs, the failure messages with their fix, the leased or owned ports and databases and the ban on compile-time source paths were enforced by make repo-check but not stated as development rules, so a contributor met them only as check failures. | — | Evidence: AGENTS.md and AGENTS.ko.md state five rules before the unit-test rule: every toolchain pinned in one declaration (.node-version, .go-version, .composer-version, rust-toolchain.toml exact, .php-version and the PostgreSQL major as exact as their installers allow) and adopted with CI evidence; build outputs that others read published by building beside them and renaming ($(PUBLISH), scripts/typescript/build.mjs); failure messages naming the cause and the fix; shared ports and databases held by a lease or owned by one run, with no fixed TCP port; and files found from the working directory or the paths the Makefile gives, never from runtime.Caller. docs-rules-check, repo-check and feature-unit-check pass. | [o] |
| G5.48 | Refuse a push while a checklist item is in progress, in the pre-push hook and again in CI. Cause: AGENTS.md allowed a push only when no item is in progress, but nothing checked it before the push, and its first line still said "do not add a remote or push" although the repository has its GitHub remote. | — | Evidence: .githooks/pre-push (mode 100755) runs scripts/check/push-gate.mjs (the push check), which reads docs/checklist.md of every pushed commit and of the working tree with the guard's activeItems and refuses with each item's ID and title, the rule and the fix; an unreadable checklist, a git error or an exception also refuses. scripts/check/hooks.mjs reports why the hook would not run (core.hooksPath not .githooks, a missing or non-executable hook); the Makefile sets core.hooksPath at parse time for both hooks (merged with the commit-msg block of G5.49-1), make hooks sets it and checks it, make owner-check starts with make hooks-check, and the full suite's guard (scripts/check/full-run.mjs) refuses a checkout without the hook. .github/workflows/push-gate.yml runs make push-gate-commit COMMIT=<pushed sha> with actions pinned to the commits of their tags, which also requires the commit to track the hook with mode 100755 and writes the refusal as annotations and to the step summary. AGENTS.md (+ko) states the push rule and its checks. scripts/check/push-gate.test.mjs pushes to a temporary bare remote through the real hook (refused with active items in the pushed commit or the working tree, allowed otherwise), runs the CI command and hooks-check, and full-run.test.mjs refuses a checkout without core.hooksPath before any step; make full-run-check (63 tests), make repo-check, make docs-check and make hooks-check pass, and make push-gate-commit COMMIT=HEAD refused the commit before this one because it does not track the hook. | [o] |
| G5.85 | Make make feature-unit-check pass on main again. Cause: on G5.48 the owners test failed with ".github/workflows/push-gate.yml and clients/php-extension/tests/dbspec_apply_cleanup_test.php select no behaviour test and declare no scope" (both added by G5.48 and T9.13), and feature-docs-check failed with "generated document is stale: docs/features.md" because the generated docs/features.md (+ko) had been edited instead of the schema_definition description in contracts/features.json. | — | Evidence: contracts/check-inputs.json declares push-gate.yml as a suite input, contracts/features.json lists both apply cleanup tests in the schema_definition tests and in the inputs of make dbspec-php-extension-check, its schema_definition description (en and ko) says "code that calls them" as the document does, and node scripts/features/build.mjs writes docs/features.md (+ko) unchanged; make feature-unit-check (owners 19 tests, coverage 16) and make repo-check pass. | [o] |
| G5.86 | Name orm's PHP packages and namespaces after polyspec. Cause: the PHP client was the composer package orm/php-client in the namespace Orm, the extension orm/orm-dbspec in Orm\Dbspec\Native, and orm's committed generated models and test models used Polyspec\Orm\Tests\Model and other namespaces outside polyspec. Change: the composer packages are polyspec/orm and polyspec/orm-dbspec; the library namespace is Polyspec\Orm in src, testing, bin and every test, and the extension registers Polyspec\Orm\Dbspec\Native (stub, generated arginfo and its C function names); orm-gen keeps its required --namespace with no default, and orm's committed models in clients/php/gen use Polyspec\Orm\Tests\Model, its test models Polyspec\Orm\Tests\<name>, and its examples of a user setting Example\Model. A user changes use Orm\… to use Polyspec\Orm\…, requires polyspec/orm (and polyspec/orm-dbspec) and regenerates every PHP model set with orm-gen, because generated models import Polyspec\Orm\Model and Polyspec\Orm\StyledValue. The Go, Rust and TypeScript generators take no namespace. | — | Evidence: clients/php/gen is regenerated (12 entities); contracts/symbols/php.json and php-extension.json are re-recorded and contracts/interfaces.json and contracts/validate.go name the new classes; the test autoloader looks up the longer Polyspec\Orm\Tests\Model\ prefix first; make client-unit-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-compare-check, make interface-check, make repo-check, make version-check, make feature-unit-check, make full-run-check and make docs-check pass; the server suites run in CI. | [o] |
| G5.87 | Name orm's Rust crates and npm packages after polyspec, and repair the PHP lockfile. Cause: the Rust crates were orm, orm-build and orm-schema (libraries orm, orm_build, orm_schema), the npm package @polyspec/orm-typescript and the workspace package orm-docs, while polyspec packages are named polyspec-<name> and @polyspec/<name>; and G5.86 changed the name in clients/php/composer.json without the lock file's content hash, so make package-check failed on "The lock file is not up to date". Change: the crates are polyspec-orm, polyspec-orm-build and polyspec-orm-schema (libraries polyspec_orm, polyspec_orm_build, polyspec_orm_schema) in every manifest, path, -p argument, build-tool template and document, the npm package is @polyspec/orm and the workspace @polyspec/orm-workspace, and clients/php/composer.lock carries the hash of the renamed composer.json. The Go module is already github.com/polyspec/orm; the internal test crates keep their names. A user depends on polyspec-orm and builds with polyspec-orm-build, writes polyspec_orm:: paths and polyspec_orm::models!() (the build script regenerates the Rust models), installs @polyspec/orm and regenerates the TypeScript models with orm-gen, whose output now imports @polyspec/orm. | — | Evidence: cargo build --workspace --all-targets --all-features and the bench workspace build, make rust-fmt-check, make package-check (TypeScript pack, composer validate, the Rust crate as an external path dependency, the Go module), make ts-model-check, make go-model-check, make dbspec-ts-check, the TypeScript generate, scan, DSN and package declaration tests, make interface-check with re-recorded symbols, make repo-check, make version-check, make feature-unit-check, make full-run-check and make docs-check pass; the server suites run in CI. | [o] |
| G5.88 | Check the subjects of the pushed range or the pull request. Change: git-check (scripts/git/check.mjs) reads the subjects of the commits of ORM_GIT_RANGE (<base>..<head>), and of HEAD alone without it, and a range that does not resolve fails with its cause and how to give one. The make check step of the CI workflow sets ORM_GIT_RANGE to the range of the pull request, or of a push that adds to a branch; a new branch, a forced push and a manual run check HEAD. make repo-check fails a workflow whose make check step gives no such range. git.subject-format in contracts/rules.json holds the types and the subject length, the commit-msg hook checks each subject as it is written, and AGENTS.md states the range. The CI setup and independent-test cases of scripts/repo/check.test.mjs check written workflows and Makefiles and the tracked ones. | — | Evidence: Red: node --test scripts/git/check.test.mjs failed to import subjectRange from the check before the change, and the workflow rule refused the ci.yml without ORM_GIT_RANGE. Green: make git-check passes its 7 unit cases (the range cases read start..pushed with no error, start..next with the one lower-case subject, HEAD alone, and fail on missing..next) and the subject of HEAD; make repo-check/unit passes 74 cases and node scripts/repo/check.mjs passes; make full-run-check passes 63 cases and make testcase-check/node 6. | [o] |
| G5.89 | Depend on ordered-json by its repository. Change: clients/rust/orm/Cargo.toml names ordered-json by version and git, clients/typescript/package.json by github:polyspec/ordered-json and clients/php/composer.json by dev-main, so a lock update takes the repository's main; Cargo.lock and package-lock.json record the revision they resolve. docs/spec/codec.md names ordered-json 0.0.1 from its repository's main. | — | Evidence: cargo metadata --offline --locked passes in clients/rust and bench/rust and cargo check -p polyspec-orm --lib --offline --locked builds; npm ls --package-lock-only in clients/typescript lists ordered-json 0.0.1 from its git source. The lock files are updated to its main at publication by cargo update -p ordered-json and npm install in clients/typescript. | [o] |
| G5.90 | Require ordered-json's Go module by a release version. Cause: go.mod required the module of ordered-json by a pseudo-version, which names a commit, because ordered-json had no version tag of its Go module. | Acceptance: go.mod and go.sum require github.com/polyspec/ordered-json/packages/ordered-json-go at v0.0.7, the tag packages/ordered-json-go/v0.0.7. | Evidence: go build ./... and make go-vet-check exit 0. | [o] |
| G5.91 | Publish every change through a pull request and the merge queue. Change: .github/ruleset.json declares the GitHub ruleset main (pull request without approval, merge queue with REBASE, linear history, the GitHub Actions checks gate, test and build, no force-push, no deletion, no bypass actor) and the repository settings allow_rebase_merge, allow_auto_merge and delete_branch_on_merge; make github-ruleset applies them and make github-ruleset-check fails with each differing field (scripts/github/ruleset.mjs); the workflows ci, push-gate and docs-pages run on merge_group, and ORM_GIT_RANGE of a merge group is its base and head; AGENTS states the pull request flow. Red: scripts/github/ruleset.test.mjs failed to load without .github/ruleset.json. Green: its 8 cases pass with a fake GitHub CLI (make github-check, in CHECK_TARGETS). | — | — | [o] |
| G5.93 | Name orm's internal Rust crates after polyspec. Cause: the test, bench and symbol crates were orm-testcase, orm-case-clock, orm-case-database, orm-tests, orm-bench and orm-interface-symbols (libraries orm_testcase, orm_case_clock and orm_case_database), while polyspec crates are named polyspec-<name>; and scripts/version/check.mjs read the Cargo.lock versions and the manifest version pins of orm, orm-schema and orm-build, names that no package has, so it read none of them. Change: the crates are polyspec-orm-testcase, polyspec-orm-case-clock, polyspec-orm-case-database, polyspec-orm-tests, polyspec-orm-bench and polyspec-orm-interface-symbols (libraries polyspec_orm_testcase, polyspec_orm_case_clock and polyspec_orm_case_database) in every manifest, lock file, -p argument, Rust path, program path and document; the version check reads the polyspec-orm packages. Red: the new case the version of each Rust package in a lockfile and each version pin of a manifest are read of scripts/version/check.test.mjs failed. Green: cargo check --workspace --all-targets --all-features --offline --locked in clients/rust and cargo check --all-targets in bench/rust pass, the symbol tool builds as polyspec-orm-interface-symbols, and make rust-fmt-check, make version-check (4 cases), make repo-check (74 cases), make testcase-check/rust, make feature-unit-check and go vet of tests/interfaces/check and tests/conformance/check pass. | — | — | [o] |
| G5.94 | Require ordered-json's PHP package by its polyspec name. Cause: clients/php/composer.json required ordered-json/ordered-json and the PHP client called OrderedJson\Value, OrderedJson\parse and OrderedJson\stringify, while ordered-json's main publishes the package polyspec/ordered-json in the namespace Polyspec\OrderedJson. Change: clients/php/composer.json requires polyspec/ordered-json (dev-main from its repository), and clients/php/src (Codec, Model), the PHP tests, tests/codec/check.php, tests/conformance/result_php.php and docs/spec/codec.md (+ko) use Polyspec\OrderedJson; clients/php/composer.lock is updated to the package at publication by composer update polyspec/ordered-json. Red: with the PHP library of ordered-json's main, php clients/php/tests/styled_value_test.php and php tests/codec/check.php failed with Call to undefined function OrderedJson\parse(). Green: with that library, styled_value_test.php passes its cases, tests/codec/check.php passes codec/php-vectors and codec/php-errors, aes_json_test.php passes on SQLite, tests/conformance/result_php.php passes, and php -l passes for every changed file; the MySQL and PostgreSQL cases run in CI. | — | — | [o] |
| G5.95 | Depend on the packages of ordered-json by their names and lock them at its main. Change: clients/rust/orm/Cargo.toml names the crate polyspec-ordered-json under the dependency key ordered-json, so ordered_json:: paths stay; clients/typescript/package.json depends on @polyspec/ordered-json, the client sources and tests import it by that name, and the tests outside the client import it from clients/typescript/node_modules/@polyspec/ordered-json; clients/rust/Cargo.lock, bench/rust/Cargo.lock, clients/typescript/package-lock.json and clients/php/composer.lock resolve the packages from the branch main of polyspec/ordered-json, and go.mod requires the module github.com/polyspec/ordered-json/go at that main. Red: with the previous Cargo manifest, resolving against main failed with "no matching package named ordered-json found"; main publishes the npm package as @polyspec/ordered-json, and make codec-check failed with Cannot find module '…/node_modules/ordered-json/js/index.js' and, with the previous composer.lock, with Call to undefined function Polyspec\OrderedJson\parse(). Green: make ts-check, make interface-check, make rust-check and go build ./... pass, and make codec-check passes with G5.96. | — | — | [o] |
| G5.96 | Select the Rust test features by package name. Cause: the crates are named polyspec-orm and polyspec-orm-build, but ORM_RUST_TEST_FEATURES of the Makefile still named orm/test-faults,orm-build/live-db, so every cargo test that runs with --workspace --features $(ORM_RUST_TEST_FEATURES) failed before it compiled. Change: ORM_RUST_TEST_FEATURES is polyspec-orm/test-faults,polyspec-orm-build/live-db, and the comment above it names polyspec-orm-tests. Red: make codec-check failed in codec-check/rust with none of the selected packages contains these features: orm-build/live-db, orm/test-faults. Green: make codec-check passes. | — | — | [o] |
| G5.97 | Depend on ordered-json by its tags. Change: clients/rust/orm/Cargo.toml names the tag v0.0.1 of github.com/polyspec/ordered-json, clients/typescript/package.json depends on github:polyspec/ordered-json#v0.0.1, clients/php/composer.json requires polyspec/ordered-json 0.0.1 from its repository, and go.mod requires github.com/polyspec/ordered-json/go v0.0.1, the tag go/v0.0.1 of that repository; clients/rust/Cargo.lock, bench/rust/Cargo.lock, clients/typescript/package-lock.json, clients/php/composer.lock and go.sum resolve those tags instead of the branch main. Green: go build ./..., make rust-check, make ts-check, make codec-check, make version-check, make repo-check and make package-check pass. | — | — | [o] |
| G5.98 | Run every check in ci.yml on pull requests and merge groups. Cause: ci.yml and push-gate.yml ran on every push besides their other events, push-gate.yml did not run on pull requests, and the documentation checks ran in the job build of docs-pages.yml, which the ruleset required as the check build. Change: ci.yml runs on pull_request, merge_group and workflow_dispatch and holds every check job: test runs make check with the ORM_GIT_RANGE of the pull request or the merge group, and docs runs make docs-ci (docs-build, docs-verify-idempotent, docs-static-check) with the setup and report upload of that job; push-gate.yml runs on push (branches-ignore gh-readonly-queue/**), pull_request and merge_group with the job push-gate; docs-pages.yml runs on push to main and workflow_dispatch and only builds the site with make docs-build, uploads it and deploys it; .github/ruleset.json requires the checks push-gate, test and docs. make repo-check checks the on: block of the three workflows, refuses push, pull_request and merge_group in any other workflow, and reads the setup and the steps after make check per job. Red: the repository case every workflow runs on its declared events only failed with the triggers of ci.yml, push-gate.yml and docs-pages.yml, and make github-check failed because the ruleset lacked the check docs of ci.yml. Green: make repo-check, make github-check, make checklist-check, make docs-rules-check, make version-check and make git-check pass. | — | — | [o] |
| G5.99 | Install the browsers before the site build of docs-pages.yml. Cause: scripts/docs/prepare.mjs draws the Mermaid diagrams in Chromium, and the build job of docs-pages.yml ran make docs-build without make install-browsers, so the job failed with browserType.launch: Executable doesn't exist and the site was not published. Change: the build job runs make install-browsers before make docs-build, and make repo-check fails for a workflow job that runs make docs-build or make docs-ci without make install-browsers in an earlier step. Red: the new case every job that builds the site installs the browsers before it failed on docs-pages.yml. Green: make repo-check passes. | — | — | [o] |
| G5.100 | Render Mermaid in the browser of the reader and build and check the site with Node only. Cause: scripts/docs/prepare.mjs drew every Mermaid fence of docs/**/.md in headless Chromium into docs/public/diagrams/.svg and docs/.vitepress/generated/diagrams.json before the VitePress build, docs/.vitepress/config.mts replaced each fence by an image of that SVG, and make docs-static-check opened the built site in Chromium, so the site build and its checks needed a browser. Change: config.mts emits each Mermaid fence as its escaped source in <pre class="mermaid" v-pre>, and docs/.vitepress/theme/index.ts imports mermaid on the client after the page mounts and after each route change and runs it on those elements; scripts/docs/prepare.mjs, browser.mjs, steps.mjs, load.mjs and their tests, the target docs-static-check, the target install-browsers, the setup steps browsers of ci.yml and docs-pages.yml, the setup id browsers of scripts/check/ci-setup.mjs, the repository rule that required make install-browsers before a site build, and the dev dependencies playwright and markdown-it are removed; scripts/docs/check.mjs (make docs-check after the build) reads the built HTML with Node: every Markdown page has its HTML page, every internal link, stylesheet, image and script path names a file of docs/.vitepress/dist, every fragment names an id of its page, and the pages hold at least 20 Mermaid sources; make docs-ci runs docs-verify-idempotent and docs-check. Red: the case docs-site/links failed on a built page whose link named a missing anchor (Missing anchor) and on one whose link named a missing page (ENOENT). Green: with an empty PLAYWRIGHT_BROWSERS_PATH and no playwright package installed, make docs-build, make docs-check (48 pages, 566 internal targets, 20 Mermaid sources), make docs-verify-idempotent (229 identical files) and make docs-rules-check pass; make repo-check, make checklist-check, make version-check and make git-check pass. | — | — | [o] |
| G5.101 | Depend on the Rust crates by their package names in rust-150-check. Cause: scripts/check-rust-150.sh writes a crate whose Cargo.toml depends on the packages orm and orm-build, but the crates of clients/rust/orm and clients/rust/orm-build are the packages polyspec-orm and polyspec-orm-build, so cargo check failed with "no matching package named orm found" before it compiled the 150 generated models. Change: the generated Cargo.toml depends on polyspec-orm and polyspec-orm-build, the names that its build.rs and lib.rs call as polyspec_orm_build:: and polyspec_orm::. Red: make rust-150-check failed with "no matching package named orm found". Green: make rust-150-check passes with rust-150: 150 generated entities compiled. | — | — | [o] |
| G5.102 | Give a table named polyspec_orm the Rust model module polyspec_orm_model. Cause: the generated Rust models name the runtime crate by the relative path polyspec_orm::, but clients/rust/orm-build/src/generate.rs renames only the modules of tables named after a Rust keyword, orm, std, core or alloc, so a table polyspec_orm got the module polyspec_orm in model, which shadows the crate there, and the generated source failed to compile with "cannot find Schema in polyspec_orm". Change: the renamed module names are the Rust keywords, polyspec_orm, std, core and alloc; the new case a_table_named_like_the_runtime_crate_gets_a_model_module_of_its_own of clients/rust/orm-build/tests/generate.rs generates the models of a table polyspec_orm. Red: that case failed with the model module of table polyspec_orm is polyspec_orm_model, and a crate that generates the model of a table polyspec_orm failed to compile with "cannot find Schema in polyspec_orm". Green: cargo test -p polyspec-orm-build --test generate passes, and that crate compiles. | — | — | [o] |
| G5.103 | Call setjmp of the PHP extension parser in a function without locals that change before longjmp. Cause: run of clients/php-extension/src/parse.c calls setjmp(p->stop_jump) and keeps the local stopped across it, so GCC with -Wextra (-Wclobbered) warns variable 'stopped' might be clobbered by 'longjmp' or 'vfork' at parse.c:2093, and clients/php-extension/scripts/build.sh, which fails a build with compiler warnings, failed the extension build of dbspec-introspect-php-extension-check, dbspec-introspect-compare-check, dbspec-apply-php-extension-check and dbspec-apply-pairs-check on Linux. Change: the new function read_until_stop holds the setjmp and returns false after stop jumps back; it has no local that changes between setjmp and longjmp (C11 7.13.2.1), and run sets stopped from its result. expression_parse of expression.c, the other setjmp of the extension, changes only tree between setjmp and longjmp and returns without reading it after the jump. Red: compiling the extension with GCC 16.2 and the flags of config.m4 warned at parse.c:2093 with -Wclobbered. Green: the same GCC build has no warning in parse.c or expression.c, sh clients/php-extension/scripts/build.sh publishes the library, and make dbspec-php-extension-check/vectors, make dbspec-php-extension-check/plan and make dbspec-php-extension-check/declarations pass. | — | — | [o] |
| G5.104 | Write bool and null properties of the PHP extension objects in place. Cause: obj_put_bool and obj_put_null of clients/php-extension/src/convert.c set only the type of a local zval and pass it to obj_put, whose ZVAL_COPY_VALUE reads the value of that zval, which was never written; GCC 16 warns 'v.value.counted' is used uninitialized and may be used uninitialized where those functions are inlined into zv_result, out_document, out_table, out_setting, out_plan, out_effect and out_steps, so clients/php-extension/scripts/build.sh, which fails a build with compiler warnings, fails with that compiler. Change: the new released_slot releases the value of the property and returns its slot; obj_put copies its value there, and obj_put_bool and obj_put_null write the type into the slot with ZVAL_BOOL and ZVAL_NULL. Red: compiling the extension with GCC 16.2 and the flags of config.m4 gave 14 -Wuninitialized and -Wmaybe-uninitialized warnings from convert.c. Green: a clean GCC 16.2 build of every source has no compiler warning, sh clients/php-extension/scripts/build.sh publishes the library, and make dbspec-php-extension-check/vectors, make dbspec-php-extension-check/plan, make dbspec-php-extension-check/declarations, make dbspec-php-extension-check/mermaid and make dbspec-php-extension-check/apply-cleanup pass. | — | — | [o] |
| G5.105 | Generate the external document sets of the PHP schema_set test in the namespace that the test calls. Cause: externalSets of clients/php/tests/schema_set_test.php generates the models of the sets core, member, member_v2 and drifted in the namespaces ExtCore\Orm, ExtMember\Orm, ExtMemberV2\Orm and ExtDrifted\Orm, but externalDocuments creates the post as \Polyspec\Orm\Tests\ExtMember\ExtPost, so schema_set/external_documents failed on SQLite, MySQL and PostgreSQL in client-db-check and client-pooler-check with Class "Polyspec\Orm\Tests\ExtMember\ExtPost" not found. Change: externalSets generates and autoloads each set in Polyspec\Orm\Tests\<prefix>, like the other generated sets of the PHP tests. Red: php clients/php/tests/schema_set_test.php failed schema_set/external_documents/sqlite, /mysql and /postgres with that error. Green: php clients/php/tests/schema_set_test.php passes every case on the three databases. | — | — | [o] |
| G5.106 | Declare the PHP record fixture of the interface check in the namespace Polyspec\Orm. Cause: TestPHPRecordSourceMutationChangesExtractedWire of tests/interfaces/check/php_test.go writes the class Validator in namespace Orm; and reads its record under the key fixture.php::Polyspec\Orm\Validator::Request#wire, so the key is absent and go-test-check failed with PHP record declaration was not extracted: "". Change: the fixture declares namespace Polyspec\Orm;, the namespace of the PHP client's Validator. Red: go test ./tests/interfaces/check -run TestPHPRecordSourceMutationChangesExtractedWire failed with that message. Green: the same command passes. | — | — | [o] |
| G5.107 | Generate the PHP error codes in the namespace Polyspec\Orm. Cause: orm-gen errors --lang php (cmd/orm-gen/errors.go) writes namespace Orm;, while clients/php/src/Code.php, which it generates, declares namespace Polyspec\Orm;, so the feature command features/helpers/error-codes of feature-check failed with the diff -namespace Polyspec\Orm; +namespace Orm;. Change: the generator writes namespace Polyspec\Orm;, and the paths of the feature error-codes in contracts/features.json include clients/php/src/Code.php beside the Go and Rust outputs. Red: the command of error-codes failed with that diff. Green: the command of error-codes exits 0. | — | — | [o] |
| G5.108 | Match the case-insensitive name condition of the conformance vector conditions_values to the seeded author names. Cause: bench/sql/seed.*.sql name the authors author-<i>, and tests/conformance/vectors.json, vectors.postgres.json and vectors.sqlite.json expect 12 authors of service 7 for the case-insensitive condition lk of conditions_values, but the runners of Go, PHP, Rust and TypeScript (tests/conformance/runner_go/main.go, runner.php, runner_typescript.mjs, clients/rust/tests/src/conformance.rs) pass the substring attle-10, which no seeded name contains, so all four clients count 0 and conformance-check and the conformance_verification commands of feature-check failed on MySQL, PostgreSQL and SQLite with expected [1714,0,1000,700,100,12,0,0,100,10], got [1714,0,1000,700,100,0,0,0,100,10]. Change: the four runners pass uthor-10, which matches author-10 and Author-10 in any letter case, and the three vector files bind %uthor-10%; the expected count 12 and the case-sensitive condition lb with Author-10, which matches no lower-case seeded name, are unchanged. Red: make run-databases TARGETS=conformance-check failed with conditions_values go:DIFF php:DIFF rust:DIFF typescript:DIFF and conformance: 4 mismatches. Green: make run-databases TARGETS=conformance-check passes with conditions_values go:ok php:ok rust:ok typescript:ok on MySQL, PostgreSQL and SQLite. | — | — | [o] |
| G5.109 | Select the schema crate by its package name in the preempted timing case. Cause: the case rust: the orm-schema vector tests pass while preempted of tests/timing/preempted.test.mjs runs cargo test -p orm-schema, but the crate of clients/rust/orm-schema is the package polyspec-orm-schema, so make timing-check and the feature command features/performance_gate/preempted-timing of feature-check failed with "package ID specification orm-schema did not match any packages"; the unit cases of tests/cargo-test.test.mjs give cargo-test the package orm and the library target orm, names of no crate of the workspace. Change: the case is rust: the polyspec-orm-schema vector tests pass while preempted and runs cargo test -p polyspec-orm-schema; the cargo-test cases use the package polyspec-orm and the library target polyspec_orm. Red: make timing-check failed that case with "package ID specification orm-schema did not match any packages". Green: make timing-check passes every case, and node --test tests/cargo-test.test.mjs passes. | — | — | [o] |
| G5.110 | Name the Rust crates by their package names in the documentation and the test fixtures. Cause: the Rust crates are the packages polyspec-orm and polyspec-orm-build, but README.md, docs/operations/packaging.md, docs/operations/usage.md, docs/spec/interface-implementation.md, examples/complex/README.md, examples/thin-slice/README.md, tests/conformance/README.md, tests/interfaces/README.md, their Korean pages, the description of catalog_connection in contracts/features.json and docs/features.md, and the description generated_models of contracts/interfaces.json name the crates orm and orm-build, and the cargo error lines of scripts/check/full-run.test.mjs name orm-build, so a dependency written from those pages names packages that do not exist. Change: those pages and descriptions name polyspec-orm and polyspec-orm-build, docs/features.md and docs/features.ko.md are regenerated with scripts/features/build.mjs, and the cargo error lines name polyspec-orm-build. Green: node scripts/features/build.mjs --check and node --test scripts/check/full-run.test.mjs pass. | — | — | [o] |
| G5.111 | Run the CI suite as parallel jobs, one per CI group. Cause: the job test of .github/workflows/ci.yml ran every target of CHECK_TARGETS in sequence in one job for about 55 minutes, 25 of them in feature-check, whose helper check dbspec-apply-stress alone took 12 minutes, and every setup step ran whatever the targets needed. Change: the Makefile declares the CI groups CI_GROUPS (static, clients, conformance, dbspec, features, stress) with their targets CI_TARGETS_<group>, balanced on the target times of one CI run; the job test runs one job per group of its matrix with make check GROUP=<group>, which only GitHub Actions accepts, while a local make check runs every target; make ci-group-needs writes the needs that contracts/check-inputs.json declares for the targets of the group as step outputs, and each setup step runs only under the condition that scripts/check/ci-setup.mjs declares for it; the runner ignores a skipped setup step whose need no target of the run declares, records a target that needs a skipped step as not-run, and starts no servers or databases when no target needs them; feature-check runs the coverage and the verification commands of the features (scripts/features/check.mjs --features), and the new targets feature-helper-check and feature-stress-check of CHECK_TARGETS run the helper checks, the latter dbspec-stress and dbspec-apply-stress; the summary, the report artifact and the run id carry the group; make repo-check requires that the matrix equals CI_GROUPS, that the groups run every target of CHECK_TARGETS exactly once and no other target, the declared condition of each setup step, the step group-needs, and the summary and report steps of the group; the ruleset requires the check test (<group>) of each group. Red: with the earlier scripts/check/run.mjs the runner cases of a CI group job fail (a skipped step recorded as a failed setup step, databases created for a run without a database target, 3 of 37 cases), and the CI group cases of scripts/repo/check.test.mjs and the selection cases of scripts/features/select.test.mjs fail without the grouped rules and the selection module. Green: node --test scripts/check/full-run.test.mjs (37 cases), make repo-check (78 unit cases), node --test scripts/features/select.test.mjs (6 cases) and node --test scripts/github/ruleset.test.mjs (8 cases) pass. Expected wall time per group from the measured target times, with about 2 minutes of setup and 1.3 minutes of database creation: static 9, clients 12, conformance 12.5, dbspec 11, features 14.5 and stress 16 minutes; the case TestApplyStressPlan/mysql of dbspec-apply-stress takes 11 minutes and bounds the group stress. | — | — | [o] |
| G5.112 | Load the modules that the PHP extension requires in the declarations test that runs PHP without php.ini. Cause: clients/php-extension/tests/declarations_test.php runs php -n -d extension=<orm_dbspec library> declarations.php extension; -n also drops the shared modules that the php.ini of the CI runner's PHP package loads, among them pdo, which src/orm_dbspec.c requires with ZEND_MOD_REQUIRED, so PHP did not load the extension and the cases php-extension/declarations and php-extension/client of make dbspec-php-extension-check (feature command schema_definition/schema-php-extension of feature-check) failed with the extension orm_dbspec is not loaded, while the same library loaded without -n in the next part of the same make run and directory; the reason PHP printed at startup went to stdout, which the failure did not show. The library path holds the make process id of that run, the build publishes it by rename before the parts run, and only the end of the target removes its directory. Change: the test reads the required modules from src/orm_dbspec.c, probes php -n for each, and loads each missing one from the extension_dir of the running PHP; a failed run states its stderr and its stdout; the new case php-extension/required-modules checks the arguments for a PHP whose pdo is a shared module and for one with every module built in, and that a failed run keeps its stdout. Red: with the arguments of the earlier test (none) the case php-extension/required-modules fails with a PHP without a built-in pdo gets the arguments [], and the failure of php-extension/declarations on a library PHP cannot load showed only stderr. Green: make dbspec-php-extension-check/declarations passes both runs (5 cases each), and a library PHP cannot load fails with the PHP startup warning after stdout:. | — | — | [o] |
| G5.113 | Add the CI completion job, the tag release workflow and the changelog convention of tag releases. Cause: main required one check per CI job, no workflow released a tag, the changelogs put unreleased changes under version sections, and no check compared a go.mod with its directory. Change: G5.113-1 to G5.113-5. Green: the checks of each sub-item pass. | — | — | [o] |
| G5.113-1 | Keep the changes that no tag released under ## Unreleased. Cause: CHANGELOG.md and CHANGELOG.ko.md put every entry under the version sections ## 0.0.2 and ## 0.0.1, while no tag has released a version of the repository, and a release takes its notes from the section of the tagged version. Change: both changelogs keep every entry under ## Unreleased at the top, in the existing entry format; make version-check requires ## Unreleased as the first and only unreleased section of both files, every other section a version X.Y.Z not above VERSION, newer first, and the same sections in both languages, so the release PR that sets VERSION to X.Y.Z and renames ## Unreleased to ## X.Y.Z below a new empty ## Unreleased passes; VERSION and every package file keep 0.0.2, the version that the first release PR releases; AGENTS.md states that an entry goes under ## Unreleased. Red: the new case of scripts/version/check.test.mjs fails on the earlier changelogs with the first section is ## 0.0.2 for both files. Green: make version-check passes (5 cases). | — | — | [o] |
| G5.113-2 | Require the check push-gate and one CI completion check on main. Cause: the ruleset of main required the check of every job of ci.yml, test (<group>) for each CI group and docs, so a new or renamed group or job changed the required checks. Change: the last job ci-passed of .github/workflows/ci.yml runs under if: ${{ always() }} with needs: [test, docs] and runs make ci-passed (scripts/check/ci-passed.mjs), which passes only when every needed job's result is success and names each other job with its result; make repo-check requires that the job exists, is the last job, runs under always(), needs every other job of ci.yml and has that one step; .github/ruleset.json requires exactly the checks push-gate and ci-passed, which scripts/github/ruleset.test.mjs reads from the workflows; AGENTS.md states it. Red: the ci-passed case of scripts/repo/check.test.mjs fails on the earlier ci.yml with ci.yml has no job ci-passed, and scripts/github/ruleset.test.mjs fails on the earlier declaration. Green: node --test scripts/check/ci-passed.test.mjs (2 cases), make repo-check (79 unit cases) and node --test scripts/github/ruleset.test.mjs (8 cases) pass. | — | — | [o] |
| G5.113-3 | Create the GitHub Release of a version tag from the commit that CI passed. Cause: the repository had no release workflow, so a tag vX.Y.Z of main produced no release and no assets, and nothing checked that the tagged commit passed CI or that its version matched the manifests. Change: .github/workflows/release.yml runs on push: tags: ['v*', '**/v*'] with contents: write, pinned actions, the runner of .github/runner, no timeout and the tag as TAG, and after the Node, Go and Node dependency setup runs four make steps in order, each a step of scripts/release/release.mjs that the recipe gives "$$TAG": release-verify requires the tagged commit on origin/main and the latest runs of its checks push-gate and ci-passed (gh api repos/<repo>/commits/<sha>/check-runs) successful; release-versions requires VERSION, clients/typescript/package.json, clients/php/composer.json and the Cargo.toml of polyspec-orm-schema, polyspec-orm and polyspec-orm-build to declare the version of the tag (clients/php-extension/composer.json has no version and takes the tag's), go.mod to declare github.com/polyspec/orm, and CHANGELOG.md and CHANGELOG.ko.md to have its section, naming each problem with its file and both values; release-assets builds polyspec-orm-<version>.tgz (npm pack after make typescript-build) and polyspec-orm-<version>.zip and polyspec-orm-dbspec-<version>.zip (git archive of the package directory) in .runtime/release/assets, and no .crate, as a Cargo dependency names the crates by git and tag; release-publish runs only after the three steps succeeded and runs gh release create <tag> --verify-tag --title <tag> --notes-file <the section> with them; a tag <dir>/vX.Y.Z requires <dir>/go.mod to declare github.com/polyspec/orm/<dir> and builds and attaches nothing; every tracked manifest is either released or listed as not released with its reason; make release-check (scope owner, CI group static) runs the unit cases, and make repo-check requires the triggers of release.yml. Red: scripts/release/release.test.mjs fails without scripts/release/release.mjs, the case a manifest version that differs from the tag failed while the Cargo manifest reader stopped at the first line of [package], and the trigger case of scripts/repo/check.test.mjs fails on the earlier rules with release.yml runs on push; only ci.yml, push-gate.yml and docs-pages.yml run on these events. Green: make release-check (8 cases: tag forms, asset names, the manifest list, version mismatch, missing changelog section, failed and missing checks and a commit off main, the four steps with fakes of git, gh, make and npm, and a Go module tag) and make repo-check (79 unit cases) pass. | — | — | [o] |
| G5.113-4 | State the release procedure in AGENTS.md. Cause: AGENTS.md and AGENTS.ko.md described no release, so the version-bump pull request, the tag of the merged commit and the workflow that publishes the release were unwritten. Change: the section Releases of AGENTS.md and its Korean pair state the pull request chore(release): Release X.Y.Z (#<checklist ID>) that sets VERSION and every package file to X.Y.Z and renames ## Unreleased to ## X.Y.Z below a new empty ## Unreleased, the tag vX.Y.Z (or <dir>/vX.Y.Z) of the merged commit of main, which no pull request raises, and the four make steps of .github/workflows/release.yml that publish the release. Red: AGENTS.md has no ## Releases. Green: make docs-rules-check passes with the section in both languages. | — | — | [o] |
| G5.113-5 | Require every go.mod to declare the module path of its directory. Cause: go get finds a module by its path, the repository and the directory under it, and a Go module in a subdirectory is released by the tag <dir>/vX.Y.Z, but no check compared the path that a go.mod declares with its directory. Change: make repo-check requires every tracked go.mod to declare github.com/polyspec/orm at the root and github.com/polyspec/orm/<dir> in a subdirectory, naming the declared and the required path; the repository has one go.mod, at the root, which declares github.com/polyspec/orm. Red: the new case of scripts/repo/check.test.mjs fails without the rule, and the rule refuses a root go.mod that declares github.com/polyspec/orm/v2, a go.mod of packages/orm-go that declares github.com/polyspec/orm and a go.mod without a module line. Green: make repo-check passes (80 unit cases). | — | — | [o] |
| G5.114 | Match the runner unit cases to the grouped make check recipe and the new part of feature-unit-check. Cause: G5.111 put two make functions that check GROUP before the guard of the check recipe, ran $(CHECK_RUN_TARGETS) in place of $(CHECK_TARGETS) and added the part feature-unit-check/select, while the cases make check and make rerun-failed start with the guard and a target of independent commands runs each as a part of its own of scripts/check/full-run.test.mjs still expected the earlier recipe and parts, so make full-run-check failed 2 of 65 cases. Change: the first case requires the two $(if $(GROUP),...) lines, then the guard and the runner over $(CHECK_RUN_TARGETS), and that make -n check without GROUP prints the guard as its first command; the parts of feature-unit-check include select. Red: make full-run-check fails these 2 of 65 cases. Green: make full-run-check passes 65 cases. | — | — | [o] |
| G5.115 | Require the grouped make check of CI in the generated-check case of the interface check. Cause: since G5.111 ci.yml runs make check GROUP=${{ matrix.group }} once per CI group, while TestCIRequiresGeneratedChecks of tests/interfaces/check/prohibited_test.go required the line run: make check, so go-test-check failed in the CI group clients with CI must run make check. Change: the case requires the step run: make check GROUP=${{ matrix.group }} and that every target of CHECK_TARGETS, go-model-check and ts-model-check included, is a target of a CI group of CI_GROUPS; new mutations remove the grouped step and remove go-model-check or ts-model-check from its group, and each fails the case. Red: the earlier case fails on the grouped ci.yml with CI must run make check. Green: go test ./tests/interfaces/check -run '^TestCIRequiresGeneratedChecks$' passes with its 7 mutations refused. | — | — | [o] |
| G5.116 | Build the programs of decimal-physical-check before its cases, without a deadline. Cause: scripts/decimal-physical-check.mjs runs each owner process, among them cargo test ... --bin decimal_physical and go test -tags decimalphysical, and the state reader go run ./tests/conformance/check, under a deadline of 120 s per process, so these processes also compiled under that deadline; in the CI group conformance no earlier target had built the Rust tests, and the helper check features/helpers/decimal-physical of feature-helper-check failed with decimal/mysql/rust/1 ... cargo timed out after 120000 ms; the servers and DECIMAL_ENV of the run were present, and the Go and PHP cases before it passed. Change: the recipe of decimal-physical-check builds the Rust test binary decimal_physical with cargo test --no-run under the lease of the Rust target directory, the Go test of clients/go/decimalmodel with go test -c and the state reader with go build, both published to .runtime/bin through $(PUBLISH) so the Go build cache holds them, each through RUN_LONG without a deadline, before the cases, which then run what is built. Red: the CI log of the group conformance shows cargo timed out after 120000 ms for decimal/mysql/rust/1. Green: the three build steps pass locally (rust-build/decimal-physical 43.9 s, go-build/decimal-physical 0.7 s, go-build/conformance-state 0.7 s), and make repo-check passes. | — | — | [o] |
| G5.117 | Name the checks that the ruleset requires in the header of scripts/github/ruleset.mjs. Cause: since G5.113-2 .github/ruleset.json requires the checks push-gate and ci-passed, while the header of scripts/github/ruleset.mjs still named push-gate and every job of ci.yml. Change: the header names push-gate and ci-passed, the last job of ci.yml that passes only when every other job succeeded. Red: the header names every job of ci.yml. Green: node --test scripts/github/ruleset.test.mjs passes (8 cases). | — | — | [o] |
| G5.118 | Build the state reader of the group-rows and unselected-column physical checks before their cases, without a deadline. Cause: scripts/group-rows-physical-check.mjs and scripts/unselected-column-physical-check.mjs run the state reader go run ./tests/conformance/check state under a deadline of 120 s per process, so a cold Go build of the reader ran under that deadline, the cause that G5.116 removed from decimal-physical-check. Change: the Makefile variable BUILD_CONFORMANCE_STATE builds the reader through RUN_LONG and $(PUBLISH) into .runtime/bin without a deadline, and group-rows-physical-check, unselected-column-physical-check and decimal-physical-check run it before their cases. Red: the recipes of group-rows-physical-check and unselected-column-physical-check start the cases with no build step. Green: make -n group-rows-physical-check prints go-build/conformance-state before the script, the build passes locally (0.7 s), and make repo-check passes (80 unit cases). | — | — | [o] |
| G5.119 | Run the child processes of the JavaScript tests with only the variables of the case. Cause: the case make check and make rerun-failed start with the guard of scripts/check/full-run.test.mjs ran make -n check with the environment of its caller, so in the CI group static it received GROUP=static of make check GROUP=static without GITHUB_ACTIONS, which the case set empty, and make stopped at the line of check that reads GROUP with GROUP runs one CI group of make check on GitHub Actions only. Twenty-two other child processes of the tests received the same environment: the stub runs of full-run.test.mjs read ORM_CHECK_RUN_ID and GITHUB_ACTIONS of the CI job, scripts/check/push-gate.mjs, run by push-gate.test.mjs, appended to the GITHUB_STEP_SUMMARY of the job, and the make runs of push-gate.test.mjs, scripts/git/check.test.mjs, scripts/github/ruleset.test.mjs and tests/cargo-test.test.mjs read the MAKEFLAGS and variables of a calling make. Change: tests/environment.mjs exports isolatedEnvironment(variables), an environment of PATH, HOME and TMPDIR and the given variables; every child process of the JavaScript tests runs with it, the guard case runs make -n check without GROUP and with GROUP=static GITHUB_ACTIONS=true, both printing the guard first, and with GROUP=static alone and GROUP=nope GITHUB_ACTIONS=true, both refused with their messages; the cargo case of tests/cargo-test.test.mjs gives cargo the channel of rust-toolchain.toml and CARGO_NET_OFFLINE, and tests/timing/preempted.test.mjs gives its programs the toolchain variables that the Makefile exports. callerEnvironmentErrors of scripts/repo/testcases.mjs, run by make repo-check, fails a line of a .test.mjs file that spreads process.env into a child environment, passes env: process.env, or runs make without env: isolatedEnvironment(. AGENTS.md states the rule. Red: GROUP=static GITHUB_ACTIONS=true make full-run-check fails the guard case with Makefile:171: *** GROUP runs one CI group of make check on GitHub Actions only, and callerEnvironmentErrors returns 23 errors over the test files of the tree before the change. Green: make full-run-check passes its 67 cases without CI variables, with GROUP=static GITHUB_ACTIONS=true ORM_CHECK_RUN_ID=1-1-static ORM_CI_SETUP={} ORM_GIT_RANGE=HEAD~1..HEAD GITHUB_STEP_SUMMARY=<file> in the environment, which leaves the summary file unwritten, and with GROUP=static on the command line; make git-check/unit (7), make github-check (8), make testcase-check/node (6) and make testcase-check/runners (11) pass with and without those variables; make test-servers-check passes (13); the preempted cases rust: the polyspec-orm-schema vector tests pass while preempted and php: dbspec_rules_test passes while preempted pass; make repo-check passes (81 unit cases). | — | — | [o] |
| G5.120 | Run each helper check once in make check. Cause: feature-helper-check ran every helper check except the stress helpers, and four of them repeated work that make check does elsewhere: conformance-runners runs make conformance-check, a target of CHECK_TARGETS; conformance-result runs make conformance-result-check, a prerequisite of conformance-check; case-database runs make case-database-check, a target of CHECK_TARGETS; bench-database runs sh scripts/bench-db.sh, which the setup step databases/create runs through scripts/check/databases.sh. In the CI group conformance they took 212 s, 84 s, 209 s and 107 s of the 379 s of feature-helper-check. Change: the Makefile variable FEATURE_SUITE_HELPERS lists the four helpers, and feature-helper-check leaves them out with --without-helper next to FEATURE_STRESS_HELPERS; make owner-check still runs a helper check when the files of that helper change. helperRunErrors of scripts/repo/ci.mjs, run by make repo-check, counts for every helper of contracts/features.json where make check runs its check: each target of CHECK_TARGETS whose recipe selects it with node scripts/features/check.mjs --run (after expanding the make variables and $(addprefix)), a helper command make ... <target> whose target is a target of make check or one of its prerequisites, and a helper command sh <script> that scripts/check/databases.sh runs; it fails on a helper checked twice or never, naming the places. AGENTS.md states the rule. Red: over the Makefile before the change, helperRunErrors returns 4 errors, one for each of bench-database, case-database, conformance-result and conformance-runners, each run twice. Green: make -n feature-helper-check passes --without-helper for the 2 stress helpers and the 4 suite helpers, and selectChecks of scripts/features/select.mjs selects the other 11 helpers; the new case make check runs the check of every helper once of scripts/repo/check.test.mjs passes with its mutations (a helper run by a target and by feature-helper-check, by the setup step and by feature-helper-check, a stress helper also run by feature-helper-check, and a helper that nothing runs); make repo-check passes (82 unit cases). | — | — | [o] |
| G5.121 | Run the 2000 table apply of MySQL in a CI group of its own. Cause: the CI group stress ran feature-stress-check, which ran the helpers dbspec-stress (56 s and 64 s in the two measured CI runs) and dbspec-apply-stress; make dbspec-apply-stress-bench applied the 2000 table plan to MySQL (652 s and 675 s), PostgreSQL (43 s and 44 s) and SQLite (41 s and 38 s) one after another, so the job took 937 s and 962 s. Change: the Makefile variable APPLY_STRESS_DIALECTS (default mysql|postgres|sqlite) selects the subtests of TestApplyStressPlan with -run '^TestApplyStressPlan$$/^($(APPLY_STRESS_DIALECTS))$$', and the recipe stops before any command on an empty value or a name that is no database, which would select no subtest and pass; the helper dbspec-apply-stress becomes dbspec-apply-stress-mysql (APPLY_STRESS_DIALECTS=mysql) and dbspec-apply-stress-pg-sqlite (APPLY_STRESS_DIALECTS='postgres|sqlite'), both owning tests/dialects/apply_stress_physical_test.go; FEATURE_STRESS_MYSQL_HELPERS and FEATURE_STRESS_PG_SQLITE_HELPERS (with dbspec-stress) replace the one list, and the targets feature-stress-mysql-check (needs go and databases) and feature-stress-pg-sqlite-check replace feature-stress-check in CHECK_TARGETS, contracts/check-inputs.json and the CI groups stress-mysql and stress-pg-sqlite of CI_GROUPS and the matrix of ci.yml; ciGroups of scripts/repo/ci.mjs reads group names with -. make bench still applies the plan to the three databases. Red: ciGroups reads no CI_TARGETS_stress-mysql, so make repo-check fails the new groups with Makefile CI group stress-mysql declares no CI_TARGETS_stress-mysql, and make dbspec-apply-stress-bench has no way to run one database. Green: make dbspec-apply-stress-bench APPLY_STRESS_DIALECTS='postgres|sqlite' runs TestApplyStressPlan/postgres and TestApplyStressPlan/sqlite only and passes locally (38.0 s); make -n dbspec-apply-stress-bench runs -run '^TestApplyStressPlan$/^(mysql|postgres|sqlite)$', and APPLY_STRESS_DIALECTS= and APPLY_STRESS_DIALECTS='postgres|sqlit' stop with their messages; make ci-group-needs GROUP=stress-mysql sets up only the servers and their databases; node scripts/features/check.mjs validates the manifest; make repo-check (82) and make feature-unit-check (16, 19 and 6 cases) pass. Expected CI time from the measured runs: stress-mysql about 15 min (the MySQL apply alone), stress-pg-sqlite about 5 min. | — | — | [o] |
| G5.122 | Split feature-check of make check into feature-coverage, feature-verify-rust and feature-verify-other, each in a CI group of its own. Cause: the CI group features ran feature-check alone (726 s and 793 s in the two measured CI runs; the jobs took 902 s and 978 s): about 400 s of coverage, of which the cargo build of the coverage test binaries took 97 s, and about 380 s of 147 verification commands in 4 lanes, whose cargo builds run one at a time under the lease of the Rust target directory. Change: every verification command of contracts/features.json declares shard, rust for the 31 commands that build and run Rust with cargo (tests/cargo-test.mjs, cargo build, and the interface self-test, whose checker builds its Rust symbols with cargo) and other for the other 116; scripts/features/check.mjs fails on a command without one of the shards of SHARDS and on two features that declare the same command (cwd and text) in different shards, since that command runs once; --features --shard <shard> of scripts/features/select.mjs selects the commands of that shard, and an unknown shard is an error. The targets feature-coverage (scripts/features/coverage.mjs), feature-verify-rust and feature-verify-other (check.mjs --run --features --shard rust|other), each after feature-check/build, replace feature-check in CHECK_TARGETS, contracts/check-inputs.json and the CI groups of the same names, which replace features in CI_GROUPS and the matrix of ci.yml; make feature-check runs the three. AGENTS.md and docs/spec/feature-coverage.md name the targets. Red: one CI job ran the coverage and all verification commands one after another. Green: selectChecks selects 31 commands (30 distinct) for rust and 116 (102 distinct, 1 of the Linux runner) for other, 147 together; node scripts/features/check.mjs passes, and with the shard of one command removed and the shard of model_writes/styled-write-typescript changed it fails with dsn_connection: verification 0 declares the shard undefined; declare one of rust, other and model_writes/styled-write-typescript: shard rust differs from shard other of model_queries/styled-query-typescript, which runs the same command; make -n prints the command of each new target; the new case --features --shard runs the feature commands of that shard only and four refused argument sets of scripts/features/select.test.mjs pass (7 cases); make feature-unit-check (16, 19, 7), make repo-check (82), make full-run-check (67) and TestCIRequiresGeneratedChecks pass. | — | — | [o] |
| G5.123 | Run the coverage cases that use no cargo test binary while the Rust coverage build runs. Cause: scripts/features/coverage.mjs built every native program before the first case, so all four database lanes waited for the cargo build of the coverage test binaries (97 s in the measured CI run) and then ran; the longest lane, sqlite, ran 219 s of cases without Rust and 76 s of Rust cases, so the coverage took about 97 s + 295 s. Change: buildNative returns after the state reader and the Go builds and starts the cargo build as buildCargo, whose promise builds.cargoReady fills builds.cargo and never rejects (a failed build stays the error of the cases that use it); each lane runs its cases without a cargo test binary first and then, after builds.cargoReady, its Rust cases in manifest order; every case still compares the database state before and after each of its two runs, so the order changes no result, and the temporary directory is removed only after the build ends. Red: the new case coverage runs without cargo proceed while the Rust test build runs of scripts/features/coverage.test.mjs, whose cargo test --no-run waits until the PHP case of the same lane writes a FIFO and whose Rust owner comes first in the manifest, fails on scripts/features/coverage.mjs without this change with deadline 2m0s exceeded and the case did not stop. Green: the case passes (1.2 s), and make feature-unit-check/coverage passes (17 cases). Expected CI time: the coverage drops by the build time to about 300 s, the longest lane. | — | — | [o] |
| G5.124 | Split the CI group clients in two. Cause: the group clients ran rust-driver-check (32 s and 50 s in the two measured CI runs), example-check (15 s and 41 s), client-db-check (221 s and 235 s), client-pooler-check (180 s and 189 s), dialect-facts-check (4 s and 5 s) and go-test-check (5 s and 6 s), 519 s and 603 s of make check after 60 s of databases/create, and the jobs took 615 s and 697 s. Change: CI_GROUPS and the matrix of ci.yml replace clients with clients-db (rust-driver-check, example-check, client-db-check, dialect-facts-check, go-test-check, 277 s and 337 s measured) and clients-pooler (client-pooler-check); in a job of its own client-pooler-check builds the Rust workspace tests that client-db-check built before it (rust-build/workspace-tests 136 s and 143 s in client-db-check, 17 s and 19 s in client-pooler-check), so it is expected to take about 330 s. The comment of CI_GROUPS lists what each group runs and the 10 minute bound of a job. Red: one job ran both client database suites, 615 s and 697 s. Green: make repo-check passes (82), so every target of CHECK_TARGETS runs in exactly one group; make ci-group-needs sets up Node, Rust, PHP and the servers for both groups; TestCIRequiresGeneratedChecks, make full-run-check (67) and make github-check (8) pass. Expected CI time with about 110 s of job setup and 60 s of databases/create: clients-db about 8.5 min, clients-pooler about 8.5 min. | — | — | [o] |
| G5.125 | Split the CI group dbspec in two. Cause: the group dbspec ran case-database-check and 17 dbspec targets, 414 s and 483 s of targets in the two measured CI runs after 60 s of databases/create, and the jobs took 584 s and 673 s. Change: CI_GROUPS and the matrix of ci.yml add dbspec-apply, which runs the six apply targets (dbspec-apply-check, dbspec-apply-php-check, dbspec-apply-php-extension-check, dbspec-apply-rust-check, dbspec-apply-ts-check, dbspec-apply-pairs-check; 251 s and 248 s measured), and dbspec keeps case-database-check and the DDL, introspection and plan targets (163 s and 235 s measured); the builds of the apply targets are their own (the Rust apply runner, the PHP extension of each run directory and the TypeScript build), so the split adds no build to either group but the Rust dbspec build of dbspec-apply-rust-check. The comment of CI_GROUPS names both groups. Red: one job ran all 18 targets, 584 s and 673 s. Green: make repo-check passes (82), so every target of CHECK_TARGETS runs in exactly one group; TestCIRequiresGeneratedChecks, make full-run-check (67) and make github-check (8) pass. Expected CI time with about 110 s of job setup and 60 s of databases/create: dbspec about 7 min, dbspec-apply about 7.5 min. | — | — | [o] |
| G5.126 | Run feature-helper-check in a CI group of its own. Cause: the group conformance ran ts-check (47 s and 51 s in the two measured CI runs), ts-min-check (50 s and 54 s), conformance-check (141 s and 189 s) and feature-helper-check (379 s and 353 s), and the jobs took 771 s and 839 s. Without the four helper checks that G5.120 leaves to their targets, feature-helper-check still runs rust-test-helpers (159 s and 154 s) and decimal-physical (180 s and 129 s), whose cargo builds hold the lease of the Rust target directory one after the other, and nine short helper checks (under 25 s each), so it is expected to take about 250 s to 310 s, which with the 294 s of the other three targets, 60 s of databases/create and about 110 s of job setup would pass 10 minutes. Change: CI_GROUPS and the matrix of ci.yml add feature-helpers, which runs feature-helper-check, and conformance keeps ts-check, ts-min-check and conformance-check; the comment of CI_GROUPS names the group. Red: one job ran the TypeScript, conformance and helper checks, 771 s and 839 s. Green: make repo-check passes (82), so every target of CHECK_TARGETS runs in exactly one group; TestCIRequiresGeneratedChecks, make full-run-check (67) and make github-check (8) pass. Expected CI time: conformance about 8 min, feature-helpers about 8 min. | — | — | [o] |
| G5.127 | Set up for a CI group what the setup step databases/create needs. Cause: make ci-group-needs derived the setup steps of a group only from the needs that its targets declare in contracts/check-inputs.json; feature-stress-mysql-check declares go and databases, so the group stress-mysql skipped the steps php, php-sqlite and composer, while databases/create (scripts/check/databases.sh) installs the decimal database with scripts/decimal-db-setup.php through the Composer autoload of clients/php; in CI run 37594907271 it failed with PHP Fatal error: Uncaught Error: Failed opening required '.../clients/php/vendor/autoload.php', and feature-stress-mysql-check was recorded as not-run. Change: SETUP_NEEDS of scripts/check/ci-setup.mjs declares that databases needs go, php, composer and server-programs, and expandNeeds adds these needs to the needs of a target; groupOutputs and the runner (scripts/check/run.mjs) use the expanded needs, so a skipped or failed step of such a need makes the runner skip the servers and databases/create and record the database targets as not-run with that step; groupSetupErrors, run by make repo-check, fails a CI group whose outputs skip a setup step that installs a need of its targets or of the setup steps of those needs. AGENTS.md states the rule. Red: with the groupOutputs before this change, groupSetupErrors over the CI groups of the Makefile returns 3 errors, the steps php, php-sqlite and composer of stress-mysql; the new runner case a run without the setup step that databases/create needs records the database targets as not-run fails on the runner before this change, which creates the databases and runs the targets. Green: the new case every CI group runs the setup steps of its needs and of the setup step databases/create of scripts/repo/check.test.mjs passes over all 12 groups of CI_GROUPS, with a control output of declared needs only that skips php, php-sqlite and composer; make ci-group-needs GROUP=stress-mysql prints php=true; make repo-check (83), make full-run-check (68) and make feature-unit-check (17, 19, 7) pass. | — | — | [o] |
| G5.128 | Name the CI groups so that each job uploads the report its runner writes. Cause: the runner names the report directory .runtime/check/<runName(ORM_CHECK_RUN_ID)>/report, and runName of scripts/check/report.mjs turns every character other than a lowercase letter or digit into _, while the upload step of ci.yml reads .runtime/check/ci_<run id>_<attempt>_<matrix group>/report/; the groups that G5.121 to G5.126 named with - (clients-db, clients-pooler, dbspec-apply, feature-coverage, feature-verify-rust, feature-verify-other, feature-helpers, stress-mysql, stress-pg-sqlite) therefore uploaded a directory that does not exist, and in CI run 37594907271 every target of eight of them passed while the job failed with No files were found with the provided path: .runtime/check/ci_37594907271_1_feature-verify-other/report/. Change: the nine groups are clients_db, clients_pooler, dbspec_apply, feature_coverage, feature_verify_rust, feature_verify_other, feature_helpers, stress_mysql and stress_pg_sqlite in CI_GROUPS, CI_TARGETS_<group>, the comment of CI_GROUPS, the matrix of ci.yml and the changelog entries of G5.121 to G5.126; ciGroups of scripts/repo/ci.mjs reads _ in a group name; ciReportPathErrors, run by make repo-check, fills ORM_CHECK_RUN_ID and the upload path: of each job of ci.yml with run id 1, attempt 1 and every group of CI_GROUPS and fails when the path is not the report directory that runName gives. Red: over the ci.yml and Makefile before this change, ciReportPathErrors returns 9 errors, one for each group with -, starting with ci.yml job test group clients-db uploads .runtime/check/ci_1_1_clients-db/report/, but the runner writes the report of ORM_CHECK_RUN_ID 1-1-clients-db to .runtime/check/ci_1_1_clients_db/report/. Green: the new case every CI group uploads the report directory that the runner writes of scripts/repo/check.test.mjs passes, with a group stress-x added to the Makefile failing; make repo-check (84), make full-run-check (68), make github-check (8) and TestCIRequiresGeneratedChecks pass, and make ci-group-needs prints the setup of each of the 12 groups. | — | — | [o] |
| G5.129 | Make the process cases of scripts/check/step.test.mjs wait until the background child runs sleep. Cause: a step started sleep 37 & and exited at once, and the forked child kept the command line of its shell until it ran exec; in CI run 37597456980 the process check of the step ran first, so the report read left 1 processes: 6764 sh -c sleep 37 … and the case a passed step that leaves a process in its group fails and the process is killed, which expects left 1 processes: <pid> sleep 37, failed in make full-run-check. The case of sleep 38 had the same order. Change: both steps wait, after writing the pid, until ps -o args= of the child starts with sleep ; the expected report lines are unchanged. Red: CI run 37597456980, group static, 67 passed and 1 failed. Green: the five process cases of step.test.mjs and make full-run-check pass. | — | — | [o] |
| G5.130 | Release 0.0.2. Cause: VERSION and every package file declare 0.0.2, while CHANGELOG.md and CHANGELOG.ko.md keep all their entries under ## Unreleased, so the release step make release-versions of the tag v0.0.2 fails. Change: both changelogs rename ## Unreleased to ## 0.0.2 below a new empty ## Unreleased; the versions stay 0.0.2. Red: TAG=v0.0.2 make release-versions fails with CHANGELOG.md has no section ## 0.0.2 and CHANGELOG.ko.md has no section ## 0.0.2. Green: TAG=v0.0.2 make release-versions passes, and make version-check, make release-check, make checklist-check and make docs-rules-check pass. | — | — | [o] |
| G5.131 | Check out the repository and install node in the CI job ci-passed. Cause: the job ran make ci-passed as its only step, without checking out the repository, so make found no Makefile; in CI runs 37597456980 and 37598892543 it failed in 3 s with make: *** No rule to make target 'ci-passed'. Stop., also when every other job had passed, and ciPassedErrors of scripts/repo/ci.mjs required exactly that single step. Change: the job runs actions/checkout@v5 and actions/setup-node@v7 with .node-version before make ci-passed, which runs under if: ${{ !cancelled() }} as every step after a setup step, and CI_PASSED_STEPS of scripts/repo/ci.mjs declares the three steps, so make repo-check fails a ci-passed job without them. Red: the case the job ci-passed runs last under always() and needs every other job fails on the ci.yml before this change with ci.yml job ci-passed has the steps - name: every job passed | … instead of - uses: actions/checkout@v5 || …. Green: the case passes, a minimal job without the checkout is refused, and make ci-passed in a fresh clone exits 0 for two succeeded jobs and fails for a failed one. | — | — | [o] |
| G5.132 | Keep the release notes within the GitHub body limit. Cause: make release-publish passed the whole CHANGELOG.md section of the version as the release body, and GitHub refuses a body over 125000 characters; in the release run 37603500083 of v0.0.2 (a section of 173807 characters) gh release create failed with body is too long (maximum is 125000 characters). Change: releaseNotes of scripts/release/release.mjs keeps the section when it has at most NOTES_LIMIT (125000) characters and otherwise returns The changes of X.Y.Z are listed in [CHANGELOG.md](https://github.com/polyspec/orm/blob/<tag>/CHANGELOG.md#XYZ)., with the tag URL-encoded per path segment and the anchor of the heading ## X.Y.Z; AGENTS.md and AGENTS.ko.md state the rule. Red: scripts/release/release.test.mjs fails to load with does not provide an export named 'NOTES_LIMIT'. Green: the case the notes are the changelog section up to the GitHub body limit and otherwise one line that links the section passes: a section of exactly 125000 characters is kept whole, one character more gives the one-line notes with the right URL for v0.0.2 and for a Go module tag, and make release-publish writes that line to notes.md. | — | — | [o] |
| G5.133 | Release 0.0.3. Cause: the release of v0.0.2 failed at make release-publish (G5.132), and an existing tag is never moved, so the fix is released as the next version. Change: VERSION, the nine Rust manifests and their =0.0.3 path dependencies, the orm package entries of the three Cargo.lock files, clients/php/composer.json and the content hash of its composer.lock, the TypeScript package and lockfile root, contract_version of contracts/features.json, ORM_DBSPEC_VERSION of the PHP extension and the README, SECURITY, AGENTS and plan pairs and the PHP README declare 0.0.3; both changelogs rename ## Unreleased to ## 0.0.3 below a new empty ## Unreleased. Red: TAG=v0.0.3 make release-versions fails with VERSION declares 0.0.2, the tag declares 0.0.3 and the sections missing from both changelogs. Green: TAG=v0.0.3 make release-versions passes, and make version-check, make release-check, make checklist-check and make docs-rules-check pass. | — | — | [o] |
| G5.134 | Take ordered-json 0.0.2 from its release in all four clients. Cause: clients/typescript/package.json named github:polyspec/ordered-json#v0.0.1, and npm resolved it in clients/typescript/package-lock.json to a git+ssh://git@github.com/polyspec/ordered-json.git URL with a commit, so npm ci with git disabled or without SSH access to GitHub failed with EALLOWGIT Fetching packages of type "git" have been disabled; the dependency rule of the polyspec packages until 0.1 takes the other polyspec packages by GitHub tag and release archive. Change: clients/typescript/package.json names https://github.com/polyspec/ordered-json/releases/download/v0.0.2/polyspec-ordered-json-0.0.2.tgz, and npm regenerated the lockfile entry with that resolved and its integrity; go.mod requires github.com/polyspec/ordered-json/go v0.0.2 (go.sum by go mod tidy), clients/rust/orm/Cargo.toml takes =0.0.2 at the tag v0.0.2 (clients/rust/Cargo.lock and bench/rust/Cargo.lock by cargo update -p polyspec-ordered-json), and clients/php/composer.json requires 0.0.2 (composer.lock by composer update polyspec/ordered-json); ordered-json changed between v0.0.1 and v0.0.2 only its versions, changelogs and release tooling. npmGitSourceErrors of scripts/repo/node.mjs, run by make repo-check, fails a tracked package.json whose dependency spec starts with git+, git:, ssh: or github: or is a GitHub shorthand owner/repo#ref, and a tracked package-lock.json with a resolved that starts with git+, git:, ssh: or github:; docs/spec/codec.md and docs/codec.ko.md name the version and source of each client. Red: node scripts/repo/check.mjs on the package.json and lockfile before this change fails with clients/typescript/package-lock.json resolves node_modules/@polyspec/ordered-json from git+ssh://git@github.com/polyspec/ordered-json.git#…, a git source; use the release archive URL of the GitHub tag and clients/typescript/package.json dependencies @polyspec/ordered-json is github:polyspec/ordered-json#v0.0.1, a git source; …, and npm ci --allow-git=none of that lockfile fails with EALLOWGIT. Green: make repo-check passes (87 cases, with the three new cases of scripts/repo/check.test.mjs for the release archive, the git specs and the git resolved URLs), npm ci --allow-git=none in clients/typescript installs @polyspec/ordered-json 0.0.2 with an empty npm cache, and the ordered-json tests pass: TypeScript codec-vector.mjs and styled-value.mjs, Go go test ./clients/go/orm of the codec, styled and JSON tests on SQLite, Rust polyspec-orm codec, value and model tests and model_output, and PHP styled_value_test.php and runtime_model_test.php. | — | — | [o] |
| G5.135 | Import OrderedJSON by the entry point of its package in the TypeScript conformance runners. Cause: tests/conformance/result_typescript.mjs and result_typescript.test.mjs imported clients/typescript/node_modules/@polyspec/ordered-json/js/index.js, a path of the repository tree; the release archive of OrderedJSON 0.0.2 holds the package itself, whose entry point is index.js at the package root, so in CI run 37606293644 conformance-result-check/typescript and the TypeScript runs of feature-coverage failed with ERR_MODULE_NOT_FOUND for .../@polyspec/ordered-json/js/index.js. Change: both files import .../@polyspec/ordered-json/index.js, the import entry of the package's exports. Red: node --test tests/conformance/result_typescript.test.mjs fails with ERR_MODULE_NOT_FOUND for the js/index.js path. Green: make conformance-result-check/typescript passes 5 cases. | — | — | [o] |
| G5.136 | Declare the polyspec dependencies of the release assets by exact version. Cause: polyspec publishes to no registry until 0.1, so a project installs the release assets it downloads together, and the assets find each other by name and exact version; the npm tarball of make release-assets named @polyspec/ordered-json by the URL of its release tarball, the Composer zip polyspec-orm-<version>.zip carried the vcs repository of clients/php/composer.json, which Composer reads only from the root package, and polyspec-orm-dbspec-<version>.zip had no version, which an artifact repository needs; nothing checked the packed manifests. Change: buildAssets of scripts/release/release.mjs packs the npm package with the package.json of packedPackage, which turns a release tarball URL of an @polyspec/* dependency into the version of its tag, and archives each Composer directory with the composer.json of packedComposer, which drops repositories and sets version to the release version; the manifests of the repository tree stay as they are. It reads the packed manifest back from each asset and refuses the release with packedManifestErrors for an @polyspec/* or polyspec/* dependency that is not an exact version (a URL, file:, link:, workspace:, a git source, @dev, dev-* or a range), a Composer repositories entry and a Composer version other than the release version. scripts/release/install-check.mjs, run by make package-check, builds the assets of HEAD with buildAssets and installs them in a temporary directory outside the repository: npm with every tarball as a file: dependency and the @polyspec registry set to an unreachable address, Composer with an artifact repository of the zips; docs/operations/packaging.md and docs/packaging.ko.md describe this install. Red: packedManifestErrors on the release assets of v0.0.3 reports polyspec-orm-0.0.3.tgz declares dependencies @polyspec/ordered-json as "https://github.com/polyspec/ordered-json/releases/download/v0.0.2/polyspec-ordered-json-0.0.2.tgz", a URL, polyspec-orm-0.0.3.zip declares repositories and polyspec-orm-dbspec-0.0.3.zip declares the version (none). Green: make release-check passes with the two new cases of scripts/release/release.test.mjs, and node scripts/release/install-check.mjs installs @polyspec/orm with @polyspec/ordered-json 0.0.2 by npm and polyspec/orm with polyspec/ordered-json 0.0.2 by Composer, and loads both. | — | — | [o] |
| G5.137 | Release 0.0.4. Cause: the release assets of v0.0.3 declare ordered-json by a URL in the npm tarball, carry a vcs repository in the polyspec/orm zip and no version in the polyspec/orm-dbspec zip (G5.136), and an existing tag is never moved, so the fix is released as the next version. Change: VERSION, the nine Rust manifests and their =0.0.4 path dependencies, the orm package entries of the three Cargo.lock files, clients/php/composer.json and the content hash of its composer.lock (composer update --lock), the TypeScript package and lockfile root, contract_version of contracts/features.json, ORM_DBSPEC_VERSION of the PHP extension and the README, SECURITY, AGENTS and plan pairs and the PHP README declare 0.0.4; both changelogs rename ## Unreleased to ## 0.0.4 below a new empty ## Unreleased. Red: TAG=v0.0.4 make release-versions fails with VERSION declares 0.0.3, the tag declares 0.0.4 and the sections missing from both changelogs. Green: TAG=v0.0.4 make release-versions passes, and make version-check, make release-check, make checklist-check and make docs-rules-check pass. | — | — | [o] |
| G5.138 | Release the manifests of the tree unchanged. Cause: buildAssets rewrote the packed copies (packedPackage turned the release tarball URL of @polyspec/ordered-json into 0.0.2, packedComposer dropped repositories and added version), so a released package differed from the manifest of the tree; the tree's clients/typescript/package.json named the URL, clients/php/composer.json carried a vcs repository, and clients/php-extension/composer.json had no version. Change: buildAssets of scripts/release/release.mjs packs the manifests unchanged and refuses a packed manifest that differs from its source at the commit and one that breaks manifestErrors (a polyspec dependency other than an exact version, a Composer repositories entry, a Composer version other than the release version); a case of scripts/release/release.test.mjs runs manifestErrors on the released manifests of the tree, and make release-versions requires version in every released composer.json. clients/typescript/package.json requires @polyspec/ordered-json 0.0.2; the private root package.json lists clients/typescript under workspaces, takes ordered-json from the tarball of the release v0.0.2 through overrides and drops typescript and @types/node, .npmrc sets install-strategy=nested so the workspace dependencies stay in clients/typescript/node_modules, the root package-lock.json is the one lockfile with the earlier versions, and clients/typescript/package-lock.json is removed. clients/php/composer.json has no repositories, clients/php-extension/composer.json declares 0.0.4, and the private root composer.json installs clients/php from a path repository and ordered-json from a package repository of the zip of the release v0.0.2 into vendor-php (vendor at the root is the vendor directory of the Go module); composer.lock moves to the root, the PHP tests load vendor-php/autoload.php and run vendor-php/bin/orm-gen, make install runs npm ci and composer install at the root, scripts/check/downloads.mjs reads the root lockfiles and the vendor-dir, and scripts/version/check.mjs reads the workspace entry of the root package-lock.json and clients/php-extension/composer.json. scripts/release/install-check.mjs installs the assets offline with an empty npm cache, COMPOSER_HOME and Composer cache: npm from the orm tarball and the tarballs packed from the installed dependencies, Composer from an artifact repository of the zips and path repositories of the locked packages. docs/operations/packaging.md and docs/packaging.ko.md describe the install and the development layout, and docs/spec/codec.md and docs/codec.ko.md the source of ordered-json. Red: node --test scripts/release/release.test.mjs before the manifest change fails with clients/typescript/package.json declares dependencies @polyspec/ordered-json as "https://github.com/polyspec/ordered-json/releases/download/v0.0.2/polyspec-ordered-json-0.0.2.tgz", a URL, and node scripts/release/install-check.mjs on the manifests of the commit before refuses polyspec-orm-0.0.4.zip declares repositories and polyspec-orm-dbspec-0.0.4.zip declares the version (none). Green: make release-check (12 cases), make version-check, make repo-check, make client-unit-check, node scripts/check/downloads.mjs, clients/typescript/tests/package-declarations.mjs and node scripts/release/install-check.mjs with npm_config_offline=true and COMPOSER_DISABLE_NETWORK=1 pass. | — | — | [o] |
| G5.139 | Install the release assets from committed consumer locks. Cause: scripts/release/install-check.mjs packed the third-party npm packages from the installed node_modules of the repository and gave the Composer packages from its vendor-php directory, so the install used the local install instead of what a consumer downloads. Change: tests/release-install/npm (package.json with the polyspec packages as file: dependencies on the release tarball names, package-lock.json) and tests/release-install/composer (composer.json with the artifact repository assets and a package repository of the ordered-json 0.0.2 zip with its shasum, composer.lock) are the consumer projects; install-check.mjs builds the assets of HEAD, copies a fixture and the assets into a temporary directory outside the repository, downloads the tarball of @polyspec/ordered-json from its GitHub release, and runs npm ci with an empty --cache and --@polyspec:registry=http://127.0.0.1:9/ and composer install with an empty COMPOSER_HOME and COMPOSER_CACHE_DIR, then loads the packages. make install-release-fixtures writes the fixture locks (install-check.mjs lock); the integrity and shasum of the packages this repository builds stay out of the locks, because their content changes with every commit. scripts/version/check.mjs reads the orm version of the fixtures, AGENTS.md and AGENTS.ko.md state that a check makes no registry query whose result depends on time and that a download pinned by a lock is installation, and docs/operations/packaging.md and docs/packaging.ko.md describe the check. Red: the install check of the previous commit packs the tarballs from clients/typescript/node_modules and the path repositories from vendor-php. Green: npm_config_offline=true COMPOSER_DISABLE_NETWORK=1 node scripts/release/install-check.mjs passes (npm ci and composer install from the fixtures with empty caches), and make release-check, make version-check, make repo-check and make docs-rules-check pass. | — | — | [o] |
| G5.140 | Load typescript from the workspace in the interface check, and declare the scope of the private roots. Cause: G5.138 removed typescript from the devDependencies of the root package.json, so tests/interfaces/typescript.mjs, which imported typescript from the repository root, failed with ERR_MODULE_NOT_FOUND Cannot find package 'typescript' in conformance-check, feature-coverage and feature-verify-rust of CI run 37612782635, and feature-unit-check failed with every tracked file selects a behaviour test or declares its scope for the tracked files .npmrc and composer.json. Change: tests/interfaces/typescript.mjs loads typescript through createRequire of clients/typescript/package.json, and contracts/check-inputs.json declares .npmrc and composer.json with the scope suite. Red: CI run 37612782635 fails with those errors. Green: node tests/interfaces/typescript.mjs . clients/typescript/src prints the declarations, and make feature-unit-check, make repo-check and make release-check pass. | — | — | [o] |
| G5.141 | Take ordered-json 0.0.3 in all four clients and install the release assets from an artifact repository alone. Cause: the ordered-json 0.0.2 zip declared no version, so a Composer artifact repository could not read it and the install fixture of tests/release-install/composer gave ordered-json through a package repository; ordered-json 0.0.3 is released with zips that declare version. Change: clients/typescript/package.json and clients/php/composer.json require 0.0.3, go.mod requires github.com/polyspec/ordered-json/go v0.0.3 (go.sum by go mod tidy), clients/rust/orm/Cargo.toml takes =0.0.3 at the tag v0.0.3 (clients/rust/Cargo.lock and bench/rust/Cargo.lock by cargo update -p polyspec-ordered-json), the root overrides take the tarball of the release v0.0.3 (package-lock.json by npm install), and the root composer.json takes its zip with the shasum (composer.lock by composer update polyspec/ordered-json). The Composer fixture declares only the artifact repository assets; scripts/release/install-check.mjs puts into it the built zips and the zip of each polyspec package that they require, downloaded from its GitHub release, and make install-release-fixtures wrote both fixture locks. docs/spec/codec.md, docs/codec.ko.md, docs/operations/packaging.md and docs/packaging.ko.md name ordered-json 0.0.3. The G5.139 row of docs/checklist.ko.md is reworded for the writing rules. Red: the artifact repository rejects the ordered-json 0.0.2 zip with Package polyspec/ordered-json has no version defined. Green: npm_config_offline=true COMPOSER_DISABLE_NETWORK=1 node scripts/release/install-check.mjs installs both fixtures with empty caches, and the TypeScript, PHP, Go and Rust ordered-json tests listed in the commit pass. | — | — | [o] |
| G5.142 | Install the npm workspace with the default hoisted layout. Cause: .npmrc set install-strategy=nested, which kept the dependencies of clients/typescript in clients/typescript/node_modules so that paths into that directory resolved. Change: .npmrc and its entry in contracts/check-inputs.json are removed, and package-lock.json records the hoisted layout with the same package versions. scripts/typescript/build.mjs resolves typescript/bin/tsc through createRequire of clients/typescript/package.json; clients/typescript/tests/package-declarations.mjs finds @polyspec/ordered-json and @types/node with findPackageJSON from clients/typescript/package.json and tsc through createRequire; tests/conformance/result_typescript.mjs and its test import @polyspec/ordered-json by package name; the decimal-typescript-types commands of contracts/features.json run npm exec --workspace clients/typescript -- tsc -p tsconfig.json --noEmit. docs/operations/packaging.md and docs/packaging.ko.md no longer name .npmrc. Red: under the hoisted layout node scripts/typescript/build.mjs exits with clients/typescript/node_modules/typescript/bin/tsc is missing, node --test tests/conformance/result_typescript.test.mjs fails with ERR_MODULE_NOT_FOUND for clients/typescript/node_modules/@polyspec/ordered-json/index.js, and the decimal-typescript-types command ends with FAIL typescript-types. Green: the build exits 0, the result test passes 5 of 5, the command ends with PASS typescript-types, node clients/typescript/tests/package-declarations.mjs passes, and make repo-check and node scripts/features/build.mjs --check pass. | — | — | [o] |
| G5.143 | Keep the TypeScript client clock from going back while the wall clock does not. Cause: wallMicros of clients/typescript/src/clock.ts adds the monotonic clock to an anchor and, when the reading lay more than one millisecond from Date.now(), moved the anchor to Date.now() in whole milliseconds; when the monotonic clock ran ahead of the wall clock, a reading 1.9 ms past the wall millisecond was followed by one at the start of the next millisecond, 0.9 ms earlier. In CI run 37640344084 the SQLite now condition read right after an insert was earlier than the stored created_ts, so clock_now_condition/sqlite failed with row 11 with created_ts <= now: 0 rows, while secondsLater(0) of the next statement matched. Go, PHP and Rust read the wall clock of the system, as docs/spec/protocol.md requires. Change: wallMicros moves a reading outside the millisecond that Date.now() reports to the nearest end of that millisecond and moves the anchor with it, so every reading lies within the wall clock millisecond and does not go back while the wall clock does not. The case clock_wall_micros_order of clients/typescript/tests/clock.mjs runs without a database under a wall clock and a monotonic clock that it sets, and contracts/features.json declares clock.ts an input of clock-typescript. Red: before the change the case fails with reading 1791472402032900 at wall 1791472402031 and monotonic 5001.9 lies within the millisecond of the wall clock and reading 1791472402032000 is not earlier than the previous reading 1791472402032900. Green: the case passes, and clock-typescript, clock-php, clock-rust (9 tests) and writes-orm-go pass on SQLite, MySQL and PostgreSQL. | — | — | [o] |
| T43 | Add the Python client. Cause: the repository ships Go, PHP, Rust and TypeScript clients; Python programs need the same model DSL, codecs, dbspec surface and MySQL, PostgreSQL and SQLite drivers from a client library of their own. | Acceptance: T43.1 to T43.6 are complete; the shared codec vectors and the conformance vectors of the three databases pass in Python, and every declaration that names the client languages names python. The polyspec-ordered-json dependency of clients/python/pyproject.toml names the tag v0.0.4 of the ordered-json repository and activates when that tag exists; until then the Python tests import the package from a sibling checkout of ordered-json at ../ordered-json/python/src. History: commits a6183322 and feaf90ba have empty bodies; T43.4 moved from waiting in a6183322 to done in d988a0f0 with no state commit between, and its implementation is in a6183322, 9ed5dddc and f8d2981d. Retry: every child of T43 is done or bypassed, and an ordered-json tag with python exists so that T43.6 runs in CI. | Evidence (local): every child is done; the CI jobs of the push check the database rows, and a failure there becomes a new row. | [o] |
| T43.1 | Lay out the Python package and its generator. Cause: the Python client lives in clients/python with the PEP 420 namespace package polyspec.orm, the distribution polyspec-orm, requires-python >= 3.11, a py.typed marker, unit tests in unittest, and the model generator orm-gen that reads dbspec documents and writes the model modules of clients/python/src/polyspec/orm/models/. | Acceptance: orm-gen gen --schema <document.dbs> --out <directory> writes the model modules from a dbspec document set, --check reports a missing or differing file, and the Python unit tests pass. | Evidence: polyspec.orm.dbspec parses, validates and emits dbspec documents (parse_dbspec, emit_dbspec, dbspec_manifest, read_dbspec_file), polyspec.orm.engine builds the runtime model of a document set (model_of_documents, parse_document_set, model_of_manifest), polyspec.orm.names resolves the chain grammar (parse_chain, parse_order, split_pair, op_allowed), and clients/python/bin/orm-gen gen --schema schema/bench.dbs --out <dir> writes models.py with the 12 models and the manifest hash of the TypeScript generation; --check reports a missing file with exit 1 and passes on the generated file; the scan validates the method names that the scanned sources call on a model (Author.not_a_model_method fails the generation). python clients/python/tests/dbspec_cases.py passes the shared dbspec cases (10 canonical, 10 normalize, 100 invalid, 5 hashes, 7 sets, 4 files) and python -m unittest discover -s clients/python/tests -p 'test_*.py' passes 26 cases. The query execution of the chain (get, gets, transactions) is T43.3; the fixed column methods and the chain conditions build the query state that T43.3 executes. Red: orm-gen ended with ModuleNotFoundError: No module named 'polyspec.orm' before the package existed, and the chain resolver rejected or_gt_read_count (the Or prefix check read two characters) with CONFIG: GtReadCount is not a column of author. Green: both commands pass. | [o] |
| T43.2 | Implement the value functions and the codec. Cause: queries name clock and date expressions through the value functions of orm, and styled columns decode and encode their stored cells through the codec stages. | Acceptance: the value functions and the column functions build the same IR as the other clients; encode, decode, hostEncode, hostDecode, blindIndex and CodecError follow docs/spec/codec.md; the shared codec vectors of tests/codec/vectors.json and the AES vectors pass. | Evidence: polyspec.orm.values, polyspec.orm.styled_value, polyspec.orm.codec and polyspec.orm.aes implement the surface of the TypeScript client (orm.now() to orm.months_later(), orm.day_of_week() to orm.date(), the gz, base64, serialize, yaml and ordered_json stages, the aes, hex and ip host styles, the PHP serialize decoder, the YAML 1.2 core reading of PyYAML behind the codec value model, and the AES-256-GCM stored bytes ORM-AES2\0); python clients/python/tests/codec_vector.py decodes all 80 codec vectors (16 json, 16 serialize, 16 serialize+base64, 16 serialize+gz, 16 yaml), re-encodes the 30 deterministic ones byte-identically, round-trips every vector, holds the 14 decode and encode rejections and the AES fixed vector, and python -m unittest discover -s clients/python/tests -p 'test_*.py' passes 20 cases. Red: the vector run failed before polyspec.orm.codec existed with ModuleNotFoundError. Green: both commands pass. | [o] |
| T43.3 | Implement Db and the model chain. Cause: a Python program connects by one DSN URI whose scheme selects the database and queries through the generated model chain. | Acceptance: Db.connect and Db.connectSchema accept the mysql, postgres and sqlite schemes; the abstract model chain builds the same requests as the other clients; transactions take isolation, readOnly, timeoutMs, retry and audit. | Evidence: polyspec.orm.database runs the planned statements on a SQLite, MySQL or PostgreSQL connection behind one DSN URI (parse_dsn rejects an unknown parameter, a timezone other than UTC, and the sqlite _txlock), Db.transaction takes isolation, readOnly, timeoutMs, retry and audit, numbers transactions, saves a nested call as a savepoint, and polyspec.orm.events publishes a statement event (sql, binds, kind, tables, elapsed, transaction, error) of every statement to every subscriber; the model chain executes get, gets, getCount, getSum, getAvg, getsCount of a group, getsPage, create, creates, update(true), save, delete(true), restore and getQuery, a join assembles its columns, relations read their steps, a relation with its own connection attaches after the main rows, an audited write binds the audit record of its transaction, and forUpdate takes the SQLite row lock inside a transaction. clients/python/.venv/bin/python -m unittest discover -s clients/python/tests -p 'test_*.py' passes 39 cases including the SQLite end-to-end tests of clients/python/tests/test_sqlite.py (write cycle, group and page, creates with duplication and save, transaction rollback and its events, relations with a recursive delete, a relation of its own connection, restore, the row lock, and the audit record of a transaction). Red: the first run failed with AttributeError: 'NoneType' object has no attribute 'length' because snake did not split the Pascal words of getServiceMemberModels, and creates, restore and the statement events did not exist. Green: the command passes. | [o] |
| T43.4 | Implement the dbspec surface. Cause: schema documents are read, rendered, compared and planned in the process that calls the library. | Acceptance: parseDbspec, emitDbspec, renderDbspec, introspectDbspec, diffPlan and the rest of the dbspec surface of the other clients exist in Python and agree on the shared cases of tests/dbspec. | Evidence: polyspec.orm.dbspec exports parse_dbspec, emit_dbspec and render_dbspec; polyspec.orm.dbspec.introspect exports introspect_dbspec; polyspec.orm.dbspec.plan_diff exports diff_plan; polyspec.orm.dbspec.apply exports apply_plans and finalize_plans; polyspec.orm.dbspec.mermaid exports export_mermaid and import_mermaid; polyspec.orm.dbspec.compare exports compare_schemas. python clients/python/tests/dbspec_cases.py runs the canonical, normalize, invalid, hashes, sets and files cases of tests/dbspec/cases.json (10, 10, 100, 5, 7 and 4 cases) and passes. clients/python/tests/dbspec_ddl.py agrees on 6 cases and 131 statements, dbspec_plans.py on 50 cases, dbspec_introspect.py on the 3 sqlite cases, dbspec_apply.py on 4 sqlite scenarios and dbspec_mermaid.py on 13 cases. Red: importing polyspec.orm.dbspec.mermaid failed with re.PatternError: missing ), unterminated subpattern at the _TRIM_SPACE pattern, and the Mermaid driver compared the diagnostics with a message field and the round trip with the text of the dropped-object list, which the shared file does not hold. Green: the pattern closes its group, the driver compares [rule, line, column], the dropped objects and the table, column and key skeleton as the TypeScript driver does, and every command above passes. | [o] |
| T43.5 | Register python in every language declaration. Cause: the language declarations, the checkers and the CI of the repository name the client languages, and python joins them as one more. | Acceptance: contracts/symbols/python.json, the language tables of contracts/validate.go and contracts/interfaces.json, the python runner of the conformance and dbspec comparisons, the features of contracts/features.json, the version declaration, the check inputs and the CI group hold python; every related check passes. Retry: T43.5-5 is done: the php extension loads on an arm64 machine or in CI, and make dbspec-compare-check passes. | Evidence (local): every child is done; the CI jobs of the push check the database rows, and a failure there becomes a new row. | [o] |
| T43.5-1 | Run the Python client in the dbspec comparison. Cause: tests/dbspec/compare/runners.mjs runs each client on the shared dbspec vectors and requires every output to equal the Go output, and no Python runner exists. | Acceptance: tests/dbspec/compare/python.py prints the line format of tests/dbspec/compare/check.mjs for the shared cases, the stress document, the ddl, plan and Mermaid vectors; its output equals the Go runner's output byte for byte; it rejects the same malformed inputs as the TypeScript runner; runners.mjs registers it as python. Red: before the runner existed, python3 tests/dbspec/compare/python.py ... exited 2 with can't open file. Green: the runner's output on the shared cases and a 20-table stress document equals the Go runner's output byte for byte (3385 lines, cmp reports no difference); ORM_PYTHON=clients/python/.venv/bin/python node --test --test-name-pattern=python tests/dbspec/compare/runners.test.mjs passes 21 tests, including the rejection of every mutated input. | — | [o] |
| T43.5-1-1 | Keep the Python interpreter setup out of the checks. Cause: the dbspec-compare-check preparation downloaded the Python client dependencies with python3 -m venv and pip install, which a check may not do; the download belongs to make install. | Acceptance: make install creates clients/python/.venv with the pinned dependencies, the preparation of dbspec-compare-check only checks that clients/python/.venv/bin/python exists and fails with run make install when it is missing. Red: make -n dbspec-compare-check/prepare listed python3 -m venv and pip install. Green: the preparation lists no pip install; make python-venv-check exits 0 with the venv present and 2 with PYTHON_VENV=/nonexistent/venv, naming run make install. | — | [o] |
| T43.5-2 | Run the Python client in the conformance comparison. Cause: the conformance checker runs the Go, PHP, Rust and TypeScript runners on the bench database and compares their results, and no Python runner exists. | Acceptance: tests/conformance/runner_python.py runs every vector of tests/conformance/vectors*.json with the output shape of the Go runner, the checker registers it as python, and the conformance result check passes. | Evidence: T43.5-2-1 to T43.5-2-4 are closed; the Python conformance runner over the SQLite bench exits 0 with 26 vectors and matches the Go output byte for byte (T43.5-2-2), make conformance-result-check/python exits 0, and the python checker is registered (T43.5-2-4). The MySQL and PostgreSQL runs are CI-only (T43.6) and are not claimed here. | [o] |
| T43.5-2-1 | Seed a SQLite bench database without the MySQL and PostgreSQL servers. Cause: scripts/bench-db.sh requires BENCH_MYSQL_DSN, BENCH_POSTGRES_DSN and BENCH_SQLITE_DSN and seeds all three through the Go model, so a local SQLite run has no seeded file. | Acceptance: a documented command seeds the SQLite bench file with the same rows as the other databases, or the row states that the SQLite vectors run only in CI. Red: env -u BENCH_MYSQL_DSN -u BENCH_POSTGRES_DSN BENCH_SQLITE_DSN=sqlite://<dir>/bench.sqlite sh scripts/bench-db.sh sqlite exited 1 with BENCH_MYSQL_DSN is required. Green: the same command exits 0 and writes the tables of schema/bench.dbs to the file (100000 rows in author, 101 in service); an unknown dialect exits 2 and a missing SQLite DSN exits 1. MySQL and PostgreSQL seeding stay available by name, and the no-argument run still seeds all three, as scripts/check/databases.sh calls it. | — | [o] |
| T43.5-2-2 | Generate the Python bench models for the conformance runner. Cause: the runner needs the models of schema/bench.dbs at run time, and clients/python/bin/orm-gen gen writes them into a directory of its own. | Acceptance: a Makefile step writes the models into the run directory of conformance-check and the runner imports them from there. Bypass: the make target and the model generation are committed (300897e2; make conformance-python-models exits 0, and the 12 models import under Python), and the runner does not import them yet. Cause: the coordinator reordered the work so that audit items T43.5-5 to T43.5-9 close first. Retry: resume after T43.5-9 is done, then import the models from the runner. | Evidence: make conformance-python-models exits 0 and writes models.py into .runtime/run/conformance-python-models; tests/conformance/runner_python.py load_models imports those models from the directory given by --models (the value of ORM_PYTHON_MODELS in the conformance check, which tests/conformance/check/main.go requires); run on a SQLite bench database seeded by scripts/bench-db.sh sqlite, the runner exits 0 with 26 vectors and writes 65,854 bytes that are byte-identical to the saved Go output (go-full.json). The cause recorded in the blocked state (the runner did not import the models yet) no longer holds. | [o] |
| T43.5-2-3 | Port the 26 vectors to the Python runner. Cause: tests/conformance/vectors.json names 26 vectors whose chains tests/conformance/runner_typescript.mjs writes by hand; the Python runner must print the output shape of runner_go. | Acceptance: tests/conformance/runner_python.py runs every vector of each database expectation file with output equal to the Go runner's output for the same database, and every behavior a vector needs exists in the Python client. | Evidence: the Python runner is tests/conformance/runner_python.py. On a freshly seeded SQLite bench database (scripts/bench-db.sh sqlite), python tests/conformance/runner_python.py --models .runtime/run/conformance-python-models --dsn sqlite://<file> runs all 26 vectors in one process with exit code 0, and its output is byte-identical to go run ./tests/conformance/runner_go -dsn sqlite://<file> (65,854 bytes, cmp reports no difference). The sub-items T43.5-2-3-1 to T43.5-2-3-9 each carry their Red and Green evidence. MySQL and PostgreSQL vectors run in CI only, and the checker registration is T43.5-2-4. | [o] |
| T43.5-2-3-1 | Provide db.utils() in the Python client. Cause: the vectors aes_status and transactions call utils().lock, utils().set_local, utils().local and utils().aes().status, which the Python client does not have. | Acceptance: the four calls exist with the behaviour of the Go client, and the vectors aes_status and transactions agree with the Go output. Red: python -m unittest test_utils in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1 with 5 errors, each AttributeError: 'Db' object has no attribute 'utils'. Green: the same run exits 0 with 5 tests OK and 0 entries left in TMPDIR. The vectors aes_status and transactions run in one process on a freshly seeded SQLite bench database: aes_status agrees with the Go output; transactions agrees on its result, and its statements differ only by the orm__row_lock table creation, which appears in the first transaction of the process (the full run places it as the Go run does). MySQL GET_LOCK and PostgreSQL pg_advisory_xact_lock and their set_config run in CI only. | — | [o] |
| T43.5-2-3-2 | Keep the declared name of an alias column. Cause: add_column_start_dt_alias_start_year(orm.year()) makes the result key StartYear with value null, and the Go output has start_year with the year. | Acceptance: the key is the declared alias and the value is the function result, for the vectors columns and functions. Red: the test tests/test_alias_columns.py first asserted the alias key list and failed with AssertionError: Lists differ: ['StartYear', 'name', 'seq', ...]. Green: after the add-column function stores the alias under snake(alias), python -m unittest test_alias_columns in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 0 with 1 test OK and 0 entries left. On the bench database, the results of the vectors columns and functions agree with the Go output: the alias key is start_year, its value is 2026, and months is [6, 6]. The functions statements still differ only in the $NOW binds, which are the item T43.5-2-3-6. | — | [o] |
| T43.5-2-3-3 | Build a subquery column. Cause: add_column_read_total(lambda u: Author().sum_read_count()...) fails with KeyError 'columns' in _subquery (core.py), which deletes a key that is not there. | Acceptance: the vector subqueries agrees with the Go output. Red: python -m unittest test_subqueries in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1 with KeyError: 'columns' from _subquery for an aggregate subquery that has no columns. Green: after the pop of a missing columns key and the snake_case name of the subquery column (read_total, not ReadTotal), the same run exits 0 with 1 test OK and 0 entries left. On the bench database the vector subqueries agrees with the Go output in its result and its statements. | — | [o] |
| T43.5-2-3-4 | Join a terminal-by condition with the existing conditions by AND. Cause: get_count_by_is_close(True) after service_seq(7) raises CONFIG 'condition without and/or after another condition', and the Go output joins it with AND. | Acceptance: the vector terminal_reuse agrees with the Go output. Red: python -m unittest test_terminal_by in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1 with CONFIG: condition without and/or after another condition. Green: the terminal-by method joins its condition with AND to the existing conditions and runs on a copy of the query, so the query keeps its own conditions; the same run exits 0 with 1 test OK and 0 entries left. On the bench database the vector terminal_reuse agrees with the Go output in its result and its statements. | — | [o] |
| T43.5-2-3-5 | Put joined and related models into the result. Cause: to_array of a joined row (service_model) and of a relation row (service_model, service_member_models) omits the nested models that the Go output contains. | Acceptance: the vectors joins and relations agree with the Go output. Red: python -m unittest test_related_output in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1 with KeyError: 'service_model', because the row output leaves out the related model. Green: the row output writes each related model as its own row output and each collection as a list of them, and the same run exits 0 with 1 test OK and 0 entries left. The Python unit suite (48 tests) passes. On the bench database the vectors joins and relations agree with the Go output in their results and statements. | — | [o] |
| T43.5-2-3-6 | Write $NOW and $SECRET in statement events. Cause: docs/operations/usage.md says an executor clock value is $NOW and a secret is $SECRET, but the Python client writes <now> and <secret> (database.py). | Acceptance: the events of now_defaults agree with the Go output, and the message of the spec is followed in code. Red: python -m unittest test_event_placeholders in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: AssertionError: '$NOW' not found in [...], because the insert event shows the clock bind as <now>. Green: the clock bind is shown as $NOW and the bound secret as $SECRET, as docs/operations/usage.md specifies; the same run exits 0 with 1 test OK and 0 entries left. On the bench database the vector functions agrees with the Go output. The vector now_defaults agrees in its result, and its statements differ only by the orm__row_lock table creation that this single process runs first; the full run places it as the Go run does. | — | [o] |
| T43.5-2-3-7 | Keep values attached with new_label on the model. Cause: new_label('created') attaches a value named label that the Go client keeps and writes in the row output; the Python client drops it. | Acceptance: the vector write_cycle agrees with the Go output. Red: python -m unittest test_new_values in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1 with KeyError: 'label', because the created model does not write the value attached with new_label. Green: the output writes the attached values of a model, and the same run exits 0 with 1 test OK and 0 entries left. The Python unit suite (50 tests) passes. On the bench database the vector write_cycle agrees with the Go output in its result and its statements. | — | [o] |
| T43.5-2-3-8 | Drop the default columns on remove_all_columns(). Cause: Author().connect(db).remove_all_columns().get_by_seq(42) returns seq, user_seq, service_seq, service_region_seq, service_member_seq and name in the Python client, while the Go client returns only the selected columns; the vectors pick their columns, so the difference is hidden in them. | Acceptance: remove_all_columns().add_column_name().get_by_seq(42) returns exactly seq and name, as the Go client does, with a unit test that checks the exact key set. Correction: the cause above is wrong. docs/spec/dsl.md states that removeAllColumns() keeps only the primary and foreign keys, the Go planner keeps the columns with PrimaryKey || ForeignKey (engine/planner/planner.go), and the Go output of the relations vector keeps service_seq, user_seq and the other key columns of author. The Python planner already does the same, so no client change is needed. | Evidence: python -m unittest test_remove_columns in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 0 with 1 test OK and 0 entries left; the test is a regression guard for the documented rule. | [o] |
| T43.5-2-3-9 | Name orm__row_lock in the tables of the SQLite row lock statement. Cause: the row lock insert of transactions (INSERT INTO "orm__row_lock" ... ON CONFLICT) has tables [] in the Python events and ["orm__row_lock"] in the Go events, and the full 26-vector comparison finds it as the only difference (Python 65802 bytes, Go 65854 bytes). | Acceptance: the event of that statement names orm__row_lock in tables, as the Go client does, and transactions agrees with the Go output in its statements. Red: python -m unittest test_row_lock_event in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: AssertionError: Lists differ: [] != ['orm__row_lock'], because the SQLite row lock insert event names no table. Green: the statement event names orm__row_lock, and the same run exits 0 with 1 test OK and 0 entries left. The Python unit suite (52 tests) passes. The full run of the 26 vectors on a freshly seeded SQLite bench database is byte-identical to the Go output (65,854 bytes, cmp reports no difference). | — | [o] |
| T43.5-2-4 | Register python in the conformance checker. Cause: tests/conformance/check/main.go requires the four languages go, php, rust and typescript, and conformance-result-check names no Python step. | Acceptance: requiredLanguages and runOne name python, the runner interpreter comes from ORM_PYTHON with a fix message when it is unset, conformance-result-check has a python step, and the conformance result check passes. Red: make conformance-result-check/python before the step existed exits 2 with "No rule to make target". After python is a required language, go test ./tests/conformance/check -run TestCompleteLanguageEvidence fails with conformance requires 5 language outputs, got 4, because the test listed four languages. Green: make conformance-result-check/python exits 0 (result_python.py: 7 tests OK); go vet ./tests/conformance/check exits 0; go test ./tests/conformance/check -count=1 exits 0; make -n conformance-check/run and make -n conformance-result-check exit 0. The checker runs the Python runner with ORM_PYTHON and ORM_PYTHON_MODELS, which conformance-check/run sets and builds with conformance-python-models. The full conformance run needs MySQL and PostgreSQL, so it runs in CI. | — | [o] |
| T43.5-5 | Register python in the dbspec comparison runner list (audit item 1). Cause: the audit found that tests/dbspec/compare/runners.mjs on branch feat/python-T43 at d988a0f0 lists go, php, typescript, rust and php-extension without python; commit be5903f1 on the T43.5 worktree branch adds the python runner with the five INPUTS. | Acceptance: every branch that carries T43.5 lists python among the runners, tests/dbspec/compare/check.mjs runs it, and the six runners produce equal output. Red: git show d988a0f0:tests/dbspec/compare/runners.mjs lists 0 python runners, and HEAD lists 1 (commit be5903f1). Bypass: five of the six runners agree locally, each equal to the Go output (3385 lines, cmp): go, python, php, typescript and rust. The php-extension runner is not verified: a local build with the default flags loads as x86_64 in an arm64 PHP, and a rebuild with an explicit arm64 flag was refused by the permission system. Cause: the php-extension build on this machine. Retry: run make dbspec-compare-check on a machine where the extension builds for the PHP binary, or in CI, then close T43.5-5. Resumed: the retry condition holds. CI run 37803770504 (commit fcebb854), job feature_verify_other (113403210727), ran make dbspec-compare-check with the Python runner and the php-extension build, and its step schema-compare passed (PASS equal outputs agree, PASS the unchanged vectors run in every runner). The job of the final head of this branch repeats that run. | Evidence (local): make dbspec-compare-check exits 0: tests/dbspec/compare/check.test.mjs passes and the runners go, php, typescript, rust, php-extension and python produce equal output (dbspec-compare/agreement: 12 runs agree). | [o] |
| T43.5-6 | Remove the temporary files that the Python tests create (audit item 2). Cause: clients/python/tests/test_orm_gen.py, test_sqlite.py and dbspec_apply.py create temporary files and directories, and the tearDownClass of test_sqlite.py only closes the database. | Acceptance: each temporary path is created by tempfile.TemporaryDirectory or removed by addCleanup, and after one run of the Python unit tests under a fresh TMPDIR that directory holds no entry. Red: after one run with PYTHONPATH set to the sibling ordered-json, the temporary directory held 5 entries for test_orm_gen.py, 11 for test_sqlite.py and 3 for dbspec_apply.py. Green: the same runs exit 0 (test_orm_gen.py 6 tests OK, test_sqlite.py OK, dbspec_apply.py prints 4 sqlite scenarios agree) and leave 0 entries. The subprocess import problem is a separate item, T43.5-6-1. | — | [o] |
| T43.5-6-1 | Pass the sibling ordered-json to the orm-gen subprocess in the Python tests. Cause: test_orm_gen.py and test_sqlite.py run orm-gen as a subprocess that imports polyspec.ordered_json, and the tests add that path only to their own sys.path; without PYTHONPATH the subprocess fails with ModuleNotFoundError. | Acceptance: the subprocess environment of those tests contains the sibling ordered-json source path, and both files pass with PYTHONPATH unset. Red: in a clean environment without an external PYTHONPATH (env -u PYTHONPATH), python -m unittest test_orm_gen and test_sqlite exit 1 with CalledProcessError whose subprocess fails with ModuleNotFoundError: polyspec.ordered_json. Green: the same runs exit 0 (test_orm_gen 6 tests OK, test_sqlite 13 tests OK) and leave 0 entries in a fresh TMPDIR. The orm-gen subprocess receives the sibling ordered-json source path in its PYTHONPATH (ORM_GEN_ENV in both files). | — | [o] |
| T43.5-7 | Compare the Python Mermaid results exactly (audit item 3). Cause: clients/python/tests/dbspec_mermaid.py compares the Mermaid text without the trailing line end, does not require empty export diagnostics, does not check that invalid cases have no document, and does not check version 1, while clients/typescript/tests/dbspec-mermaid.mjs checks all four. | Acceptance: dbspec_mermaid.py checks each of those four, matching dbspec-mermaid.mjs, and raises an explicit error for a failed check instead of using assert, which python -O removes. Red: the committed driver compares the export lines with splitlines, so the trailing line end is not compared; it checks neither version, nor empty export diagnostics, nor a missing document for invalid cases; and it uses assert. Green: clients/python/.venv/bin/python clients/python/tests/dbspec_mermaid.py exits 0 and prints mermaid cases: 13 (2 export, 6 import, 4 invalid, 1 round trip); a wrong version raises CheckFailed (mermaid vectors version: expected 1, got 2); the run under python -O exits 0, because the checks do not use assert. | — | [o] |
| T43.5-8 | Declare the Python client in docs/features.md (audit item 4). Cause: T43.5-4 names contracts/features.json and does not name docs/features.md, which has no Python column or row for the client. | Acceptance: docs/features.md and docs/features.ko.md name python where the other clients are named, and make docs-check exits 0. Bypass: docs/features.md and docs/features.ko.md are generated by scripts/features/build.mjs from contracts/features.json, and the generator lists only go, php, rust and typescript. A Python column would state a per-feature result that no evidence yet supports, and a hand edit would be overwritten by the next build. Cause: the per-feature Python status needs the conformance evidence of T43.5-2 and T43.6. Retry: after T43.5-2 and T43.6 record that evidence, add python to the clients of contracts/features.json and to the generator, then run node scripts/features/build.mjs. | Evidence: the generator lists python (scripts/features/build.mjs), and docs/features.md and docs/features.ko.md name python in each of the 18 rows as planned; make docs-check exits 0 on the regenerated documents (commit 7fe76490). | [o] |
| T43.5-9 | Record the T43 history facts (audit item 5). Cause: the audit found that the empty commit bodies of a6183322 and feaf90ba lack the what and why, and that T43.4 changed from waiting in a6183322 to done in d988a0f0 with no working state commit between. | Acceptance: the commits are not changed; these facts are recorded here as facts. | Evidence (recorded facts; the commits are not changed): commit a6183322 has an empty commit body; commit feaf90ba has an empty commit body; T43.4 is waiting in commit a6183322 and done in commit d988a0f0, and d988a0f0 is the only checklist commit between them, so no state commit of T43.4 exists; the T43.4 implementation is in commits a6183322, 9ed5dddc and f8d2981d, and its completion is in d988a0f0. | [o] |
| T43.5-3 | Declare the Python client in the interface contracts. Cause: tests/interfaces/check requires a symbol inventory and a signature for every rule, record and owner of each declared language. | Acceptance: a Python extractor reads the public symbols of clients/python/src; contracts/symbols/python.json and the symbol_hashes entry of contracts/interfaces.json hold its inventory; every rule, record and owner names a Python symbol; contracts/validate.go lists python in languages and outputs; make interface-check passes. | Evidence: every child (T43.5-3-1 to T43.5-3-9 and their sub-items) is closed; make interface-check exits 0 with five languages, python included, and the Python interface tests (12 tests) pass. | [o] |
| T43.5-3-1 | Add the Python symbol extractor and its snapshot. Cause: tests/interfaces/check extracts each language's public symbols with a native extractor, and no Python extractor or contracts/symbols/python.json exists. | Acceptance: tests/interfaces/python.py prints the symbol JSON of clients/python/src in the form of contracts/symbols/typescript.json, and contracts/symbols/python.json holds that inventory; the checker stays green because python is not yet a checked language. Red: python -m unittest tests/interfaces/test_python_extractor.py before the extractor existed exits 1 with can't open file ... tests/interfaces/python.py. Green: after the extractor, the same run in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 0 with 1 test OK and 0 entries left. The extractor reports the public declarations of clients/python/src in the form of tests/interfaces/typescript.mjs: 524 symbols, saved as contracts/symbols/python.json. The checker does not check python yet (T43.5-3-6). | — | [o] |
| T43.5-3-2 | Name python in contracts/validate.go. Cause: languages and outputs in contracts/validate.go name the four client languages and give each common result its native return type. | Acceptance: every outputs and inputs entry gives a python type; go test ./contracts exits 0. languages lists python in T43.5-3-6, because the rule loop requires a python native for every rule of each listed language. Red: the baseline before the change: git show HEAD:contracts/validate.go has 0 "python" entries in its outputs and inputs tables. Green: after the change every one of the 42 outputs entries and 36 inputs entries gives a python type (grep -c '"python"' contracts/validate.go counts 78 lines with a python entry), gofmt -l contracts/validate.go prints nothing, go vet ./contracts exits 0 and go test ./contracts -count=1 exits 0. languages is unchanged here and lists python in T43.5-3-6, because the rule loop requires a python native for every rule of each listed language. The acceptance text was changed to match this split. | — | [o] |
| T43.5-3-3 | Give every rule a python native entry. Cause: the 57 rules of contracts/interfaces.json have a native entry per language, and python has none. | Acceptance: each rule names a python symbol and signature that the Python extractor reports; the checker stays green because python is not yet a checked language. | Evidence: the sub-items T43.5-3-3-1 (entity rules, 22), T43.5-3-3-2 (connection and tool rules, 13) and T43.5-3-3-3 (dbspec rules, 22) are closed, so all 57 rules of contracts/interfaces.json have a python native that names a Python symbol of the snapshot, with a signature equal to it and parameters and return types equal to the common tables. python -m unittest test_python_natives in tests/interfaces exits 0 with 5 tests OK. The records (T43.5-3-4) and owners (T43.5-3-5) are not done. | [o] |
| T43.5-3-3-1 | Give the 22 entity rules a Python native entry. Cause: each entity rule names a Python Model method, and the interface rules need a native entry for python with the symbol and signature the snapshot records. | Acceptance: each entity rule has a python native whose symbol is in contracts/symbols/python.json and whose signature equals that snapshot value (compared without spaces), checked by tests/interfaces/test_python_natives.py. Red: python -m unittest test_python_natives in tests/interfaces before the python natives existed exits 1 with AssertionError: unexpectedly None : Model.connect has no python native; after the natives were added, the test with the validate.go tables exits 1 with AssertionError: '' != 'Self' (17 entity rules had no return annotation or a return type of dict). Green: the Model methods carry the common table types (Self, Collection[Self], GroupRows, Page[Self] with Page a dict subclass returned by gets_page, None for delete), the snapshot contracts/symbols/python.json is re-extracted (1201 symbols, including the generated models), the 22 entity rules have python natives equal to it, and python -m unittest test_python_natives exits 0. Regression: the full Python suite (70 tests) exits 0, the 26-vector SQLite output is byte-identical to the Go output (65,854 bytes), and make python-model-check exits 0. | — | [o] |
| T43.5-3-3-2 | Give the 13 Db, Utils, SchemaUtils and AesUtils rules a Python native entry. Cause: each rule names a Python method of the connection, the utilities or the schema or AES tools. | Acceptance: each rule has a python native whose symbol and signature match the snapshot, checked by tests/interfaces/test_python_natives.py. Red: python -m unittest test_python_natives.ConnectionAndToolRuleNativesTest in tests/interfaces before the natives existed exits 1 with AssertionError: unexpectedly None : Db.connect has no python native. Green: the 13 connection, utility, schema and AES tool rules have python natives that name the methods of Db, Utils, SchemaUtils and AesUtils, their signatures equal the re-extracted snapshot, and their parameters and return types match the common tables (the annotations are the common table types, for example Db, Utils, Callable[[], T], list[str]); python -m unittest test_python_natives (both test classes) exits 0. Regression: the full Python suite (70 tests) exits 0, the 26-vector SQLite output is byte-identical to the Go output (65,854 bytes), and make python-model-check exits 0. | — | [o] |
| T43.5-3-3-3 | Give the 22 Dbspec rules a Python native entry. Cause: each rule names a Python dbspec function. | Acceptance: each rule has a python native whose symbol and signature match the snapshot, checked by tests/interfaces/test_python_natives.py. | Evidence: the three groups T43.5-3-3-3-1, T43.5-3-3-3-2 and T43.5-3-3-3-3 are closed, so all 22 dbspec rules have python natives that name Python functions, with signatures equal to the snapshot and parameters and return types equal to the common tables. python -m unittest test_python_natives in tests/interfaces exits 0 with 5 tests OK. | [o] |
| T43.5-3-3-3-1 | Give the 8 dbspec rules for the files, the parse, the emit, the manifest, the render, the introspection and the external documents a Python native entry. Cause: Dbspec.readFile, readBytes, parse, emit, manifest, render, introspect and externalDifferences name Python functions whose signatures carry no annotations for the common table types. | Acceptance: each rule has a python native whose symbol and signature equal the snapshot, and whose parameters and return type equal the common tables, checked by tests/interfaces/test_python_natives.py. Red: python -m unittest test_python_natives.DbspecRuleNativesTest in tests/interfaces before the natives existed exits 1 with AssertionError: unexpectedly None : Dbspec.readFile has no python native. Green: the eight dbspec rules for the files, parse, emit, manifest, render, introspection and external documents have python natives that name read_dbspec_file, read_dbspec_bytes, parse_dbspec, emit_dbspec, dbspec_manifest, render_dbspec, introspect_dbspec and external_differences; the signatures equal the snapshot and the parameters and return types equal the common tables (the introspection result of the table is dict, which introspect_dbspec returns); python -m unittest test_python_natives exits 0 with 3 tests OK. Regression: the Python suite (70 tests) exits 0, the 26-vector SQLite output is byte-identical to the Go output (65,854 bytes), make python-model-check exits 0, and go test ./contracts exits 0. | — | [o] |
| T43.5-3-3-3-2 | Give the 8 dbspec rules for the plans, the comparison and the upgrade steps a Python native entry. Cause: Dbspec.parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps and planSteps name Python functions whose signatures carry no annotations for the common table types. | Acceptance: as T43.5-3-3-3-1, for these 8 rules. Red: python -m unittest test_python_natives.DbspecRuleNativesTest.test_plans_comparison_upgrade_group in tests/interfaces before the natives existed exits 1 with AssertionError: unexpectedly None : Dbspec.parsePlan has no python native. Green: the eight dbspec rules for the plans, the comparison and the upgrade steps have python natives that name parse_plan, emit_plan, chain_plans, diff_plan, compare_schemas, installed_differences, add_tables_and_columns_steps and plan_steps; the signatures equal the snapshot, and the parameters and return types equal the common tables (the six plan results are dicts, as these functions return them; the table entries are set to dict). python -m unittest test_python_natives exits 0 with 4 tests OK. Regression: the Python suite (70 tests) exits 0, the 26-vector SQLite output is byte-identical to the Go output (65,854 bytes), make python-model-check exits 0, and go test ./contracts exits 0. | — | [o] |
| T43.5-3-3-3-3 | Give the 6 dbspec rules for the apply, the recovery, the rollback, the finalization and the Mermaid conversion a Python native entry. Cause: Dbspec.apply, recover, rollback, finalize, exportMermaid and importMermaid name Python functions whose signatures carry no annotations for the common table types. | Acceptance: as T43.5-3-3-3-1, for these 6 rules. Red: python -m unittest test_python_natives.DbspecRuleNativesTest.test_apply_recovery_rollback_finalize_mermaid_group in tests/interfaces before the natives existed exits 1 with AssertionError: unexpectedly None : Dbspec.apply has no python native. Green: the six dbspec rules for apply, recovery, rollback, finalization and the Mermaid conversion have python natives that name apply_plans, recover_plans, rollback_plans, finalize_plans, export_mermaid and import_mermaid; the signatures equal the snapshot, and the parameters and return types equal the common tables (the apply handler is an alias of Callable[[dict], None]); python -m unittest test_python_natives exits 0 with 5 tests OK. Regression: the Python suite (70 tests) exits 0, the 26-vector SQLite output is byte-identical to the Go output (65,854 bytes), make python-model-check exits 0, and go test ./contracts exits 0. | — | [o] |
| T43.5-3-4 | Give every record a python native entry. Cause: the 19 records of contracts/interfaces.json have a native entry per language, and python has none. | Acceptance: each record names a python file, symbol and fields; the checker stays green. | Evidence: T43.5-3-4-1 and T43.5-3-4-2 are closed, so each of the 19 records of interfaces.json names a python file, symbol and fields (clients/python/src/polyspec/orm/ir.py::<id>, with the #wire fields checked by test_python_natives.RecordNativesTest); make interface-check exits 0. | [o] |
| T43.5-3-4-1 | Read Python TypedDict records in the interface extractor. Cause: the record check reads the #wire symbol of each record (the wire type of each field and the @flatten base), and tests/interfaces/python.py emits no #wire symbol. | Acceptance: a TypedDict class emits #wire with the wire type of each field (int as integer, str as text, bool as bool, a list of X as list<X>, a dict of X as map<X>, a name as the name, X | None and NotRequired of X as X) and @flatten for its base, checked by tests/interfaces/test_python_extractor.py. | Evidence: Red: python -m unittest test_python_extractor in tests/interfaces exits 1 with KeyError: 'pkg/mod.py::Base#wire' before the extractor emitted #wire, and after a TypedDict base was emitted, the derived class Request(Base) exits 1 with KeyError: 'pkg/mod.py::Request#wire' because it derives from a record and not from TypedDict. Green: a class that derives from TypedDict or from a record of the same module emits #wire with @flatten for its base, and python -m unittest test_python_extractor exits 0 with 2 tests OK. The client has no TypedDict yet, so the snapshot contracts/symbols/python.json is unchanged. Regression: python -m unittest discover in tests/interfaces exits 0 with 7 tests OK, and make checklist-check, make docs-rules-check and make version-check exit 0. | [o] |
| T43.5-3-4-2 | Declare the 19 IR records as TypedDicts with Python natives. Cause: the 19 records of contracts/interfaces.json have a native entry per language, and python has none. | Acceptance: polyspec/orm/ir.py declares each record as a TypedDict with its fields and base, and each record has a python native whose #wire matches its fields, checked by tests/interfaces/test_python_natives.py. | Evidence: Red: python -m unittest test_python_natives.RecordNativesTest in tests/interfaces exits 1 with AssertionError: unexpectedly None : IRRequest has no python native before the records had python natives. Green: ir.py declares the 19 records as TypedDicts (IRGroup in the functional form, because its field not is a keyword; a base such as IRQuery is expanded by @flatten), each record of interfaces.json names clients/python/src/polyspec/orm/ir.py::<id>, and its #wire equals the fields with the base expanded; python -m unittest test_python_natives exits 0 with 7 tests OK. Regression: python -m unittest discover in tests/interfaces exits 0 with 9 tests OK; make interface-check, go test ./contracts, make python-model-check, make checklist-check, make docs-rules-check and make version-check exit 0. The snapshot changed only by 118 added keys, all from ir.py. | [o] |
| T43.5-3-5 | Give every owner a python native entry. Cause: the 16 owners of contracts/interfaces.json have a native entry per language, and python has none. | Acceptance: each owner names a python symbol and fields; the checker stays green. | Evidence: T43.5-3-5-1 is closed, so each of the 16 owners of interfaces.json names a python symbol and fields; make interface-check exits 0 (the checker still checks four languages, python joins in T43.5-3-6). | [o] |
| T43.5-3-5-1 | Declare the 16 owners with Python types and natives. Cause: the 16 owners of contracts/interfaces.json name a class and its fields per language, and python has none. | Acceptance: each owner has a Python class or TypedDict whose field names equal the owner's python fields, and a python native that names it, checked by tests/interfaces/test_python_natives.py. | Evidence: T43.5-3-5-1-1, T43.5-3-5-1-2 and T43.5-3-5-1-3 are closed, so each of the 16 owners of interfaces.json has a Python class or TypedDict whose fields equal the owner's python fields, and a python native naming it (checked by test_python_natives.OwnerNativesTest, 12 tests OK in the interfaces suite). | [o] |
| T43.5-3-5-1-1 | Declare Page and AESRotationStatus as Python owners with their fields. Cause: these two of the 16 owners of contracts/interfaces.json have no python entry. | Acceptance: each has a Python class or TypedDict whose field names equal the owner's python fields, and a python native that names it, checked by tests/interfaces/test_python_natives.py. | Evidence: Red: python -m unittest test_python_natives.OwnerNativesTest in tests/interfaces exits 1 with AssertionError: unexpectedly None : Page has no python native before the natives existed. Green: Page (model.py) declares the fields items, totalCount, totalPages, page and perPage as class annotations, which match its dict keys without changing the runtime; AESRotationStatus (utils.py) is a dataclass with current, total, pending and versions; both owners of interfaces.json name these python symbols and fields, and the test checks that the #field symbols of the snapshot equal those fields; the test module exits 0 with 8 tests OK. Regression: the interfaces suite exits 0 with 10 tests OK, make python-model-check, make interface-check and go test ./contracts exit 0; the snapshot changed only by 5 added keys. | [o] |
| T43.5-3-5-1-2 | Declare the 7 plan owners as Python owners with their fields. Cause: DbspecDiagnostic, DbspecManifest, DbspecPlan, DbspecPlanStep, DbspecEffect, DbspecNullCheck and DbspecChange have no python entry. | Acceptance: each has a Python class or TypedDict whose field names equal the owner's python fields, and a python native that names it, checked by tests/interfaces/test_python_natives.py. | Evidence: Red: python -m unittest test_python_natives.OwnerNativesTest in tests/interfaces exits 1 with AssertionError: unexpectedly None : DbspecDiagnostic has no python native before the natives existed. Green: the seven owners name python symbols in dbspec/model.py (DbspecDiagnostic as the dataclass, DbspecPlan as a functional TypedDict because its field from is a keyword, DbspecPlanStep, DbspecEffect, DbspecNullCheck and DbspecChange as TypedDicts) and dbspec/manifest.py (DbspecManifestResult, whose fields are class annotations); the fields equal the snapshot #field symbols, which the extractor now reads for functional TypedDicts; the test module exits 0 with the group tests, the interfaces suite exits 0, and make python-model-check, make interface-check, go test ./contracts and make dbspec-compare-check/unit exit 0. The snapshot changed only by 45 added keys. | [o] |
| T43.5-3-5-1-3 | Declare the 7 difference and apply owners as Python owners with their fields. Cause: DbspecDifference, DbspecUnsupported, DbspecTableRename, DbspecColumnRename, DbspecColumnName, DbspecApplyEvent and DbspecApplyError have no python entry. | Acceptance: each has a Python class or TypedDict whose field names equal the owner's python fields, and a python native that names it, checked by tests/interfaces/test_python_natives.py. | Evidence: Red: python -m unittest test_python_natives.OwnerNativesTest in tests/interfaces exits 1 with AssertionError: unexpectedly None : DbspecDifference has no python native before the natives existed. Green: the seven owners name python symbols whose fields are the dict keys the dbspec code writes: DbspecDifference, DbspecUnsupported (which replaces the Unsupported = dict alias, and Unsupported now names this class), DbspecTableRename, DbspecColumnRename, DbspecColumnName and DbspecApplyEvent are TypedDicts of dbspec/model.py, and DbspecApplyError (apply.py) declares its fields as class annotations; the fields equal the snapshot #field symbols, and test_python_natives exits 0 with 12 tests OK in the interfaces suite. Regression: make python-model-check, make interface-check, go test ./contracts and make dbspec-compare-check/unit exit 0, and the dbspec modules import. The snapshot changed only by 35 added keys. | [o] |
| T43.5-3-6 | Check python in the interface manifest and the checker. Cause: tests/interfaces/check/main.go requires exactly four languages and four symbol hashes, and its extractor dispatch and language list name only the four. | Acceptance: contracts/interfaces.json lists python with its roots and symbol hash, the checker requires five and dispatches the Python extractor, and make interface-check exits 0. | Evidence: Red: before the checker knew python, make interface-check exits 2 with FAIL interfaces/python: unsupported language python; once the checker dispatched python, its source mutation step failed with python parser returned too few fixture declarations; and go test ./contracts exits 1 with a matching native snapshot could silently change Collection to scalar for python, because the drift test had no python old signature. Green: validate.go lists python, the manifest lists python with its roots and the hash of contracts/symbols/python.json, and the checker requires five languages, dispatches tests/interfaces/python.py, and has the python prohibited-call pattern, its extension and a mutation fixture; make interface-check exits 0 with PASS interfaces/python; go vet and go test for ./contracts and ./tests/interfaces/check exit 0. Regression: the Python interfaces suite exits 0 with 12 tests OK, and make python-model-check and make docs-rules-check exit 0. | [o] |
| T43.5-3-7 | Add Utils.schema() to the Python client. Cause: the TypeScript and Go clients expose the schema tools as utils().schema(), and the rules name Utils.schema; the Python Utils has no schema(). | Acceptance: db.utils().schema() returns a SchemaUtils object, and the unit test of the accessor passes. Red: python -m unittest test_schema_utils in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: ImportError: cannot import name 'SchemaUtils' from 'polyspec.orm.utils'. Green: after Utils.schema() returns the SchemaUtils of the connection, the same run exits 0 with 1 test OK and 0 entries left. | — | [o] |
| T43.5-3-8 | Add SchemaUtils.register(schema) to the Python client. Cause: the TypeScript client checks that the manifest text hashes to its declared hash before it registers the set, and fails with CONFIG otherwise, registering nothing; the Python Db.register does not check. | Acceptance: a mismatched hash fails with CONFIG and registers nothing, and registering the same set again changes nothing. Red: python -m unittest test_schema_utils.SchemaRegisterTest in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1 with AttributeError: 'SchemaUtils' object has no attribute 'register' in both tests. Green: after register checks the hash (a mismatch raises CONFIG and registers nothing) and leaves a set that is registered already unchanged, the same run exits 0 with 3 tests OK (the accessor test included) and 0 entries left. | — | [o] |
| T43.5-3-9 | Add SchemaUtils.install(schema) to the Python client. Cause: the TypeScript client renders the document set for the dialect, creates the owned tables when none exists, creates nothing when all exist, fails with CONFIG when only some exist, reads the database back and reports every difference with CONFIG, and refuses MySQL inside a transaction. | Acceptance: the SQLite behaviour of these cases matches the TypeScript client on the same schema, and a unit test covers each case. Red: python -m unittest test_schema_install in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: AttributeError: 'SchemaUtils' object has no attribute 'install' in each test. Green: after install renders the set for the driver, creates the owned tables on an empty database, creates nothing when they all exist, refuses a partial set and a database that differs (the message is the TypeScript client's, unsupported_column author.junk: declared type INTEGER with CHECK "" has no dbspec type), the same run exits 0 with 3 tests OK and 0 entries left, and the full Python suite (64 tests) exits 0. The MySQL path (CONFIG inside a transaction and the session statements) runs in CI only. | — | [o] |
| T43.5-3-13 | Commit the generated Python bench models and check them. Cause: the Go and TypeScript clients commit the models generated from schema/bench.dbs (clients/go/model, clients/typescript/src/models), and the Python client does not; the plan places them in clients/python/src/polyspec/orm/models/. | Acceptance: clients/python/src/polyspec/orm/models/models.py is the orm-gen gen output of schema/bench.dbs, and make python-model-check runs orm-gen gen --check and exits 0. Red: make python-model-check before the target existed exits 2 ("No rule to make target"). Green: the generated bench models are committed at clients/python/src/polyspec/orm/models/models.py (12 models, manifest sha256:74501d5f..., the same manifest hash as the TypeScript client), and make python-model-check exits 0. A tampered models.py makes it exit 2, and the restored file exits 0 again. | — | [o] |
| T43.5-3-14 | Read the Python signatures in contracts/validate.go. Cause: the rule validation reads the parameter list and the return type of each native signature, and it trims : for PHP and TypeScript and -> for Rust; a Python signature has self as its receiver and -> for its return type, which the validation does not read. | Acceptance: contracts/validate.go reads a Python signature with a self receiver and a -> return type as the same inputs and outputs as the other clients, and go test ./contracts exits 0. Red: go test ./contracts -run TestPythonSignatureReadsLikeTheOtherClients -count=1 before pythonParts existed exits 1 with undefined: pythonParts. Green: pythonParts reads self as the receiver and "->" as the return marker, the validation loop uses it for python, and go test ./contracts -count=1 exits 0; go vet ./contracts exits 0 and gofmt -l contracts/ prints nothing. | — | [o] |
| T43.5-3-15 | Define Model.and and Model.or explicitly in the Python client. Cause: the rule list names Model.and and Model.or, and the Python Model builds them only through __getattr__, which the static symbol snapshot does not show. | Acceptance: and_ and or_ exist as methods of Model with the same condition behaviour, and contracts/symbols/python.json lists them. Correction: the cause above is wrong. The snapshot lists clients/python/src/polyspec/orm/model.py::Model.and_ and Model.or_ (the Python names of and and or), so the Model methods exist already, and the earlier check compared the camelCase names. | Evidence: a script maps each name of the rule list to its snake_case Python name and finds all 22 entity rules in contracts/symbols/python.json; no client change is needed. | [o] |
| T43.5-3-16 | Define Model.getCount, getSum, getAvg, getsCount and getsPage explicitly in the Python client. Cause: the rule list names them, and the Python Model dispatches them only through __getattr__. | Acceptance: get_count, get_sum, get_avg, gets_count and gets_page exist as methods of Model with the same results, and the snapshot lists them. Correction: the cause above is wrong. The snapshot lists clients/python/src/polyspec/orm/model.py::Model.get_count (-> int), get_sum, get_avg, gets_count and gets_page (-> dict) as explicit methods, so the names of the rule list are snake_case in Python and the methods exist. | Evidence: a script maps the rule names to the snake_case names and finds each one in contracts/symbols/python.json; no client change is needed. | [o] |
| T43.5-3-17 | Define Model.getQuery and Model.toArray explicitly in the Python client. Cause: the rule list names them, and the Python Model dispatches get_query through __getattr__ and writes its row output through to_array without the snapshot listing it for the rule. | Acceptance: get_query and to_array exist as methods of Model with the same results, and the snapshot lists them. Correction: the cause above is wrong. The snapshot lists clients/python/src/polyspec/orm/model.py::Model.get_query (-> dict) and Model.to_array (-> dict) as explicit methods, which the entity rules find under their snake_case names. | Evidence: a script maps the rule names to the snake_case names and finds both in contracts/symbols/python.json; no client change is needed. | [o] |
| T43.5-3-18 | Define AesUtils.rotate in the Python client. Cause: the rule list names AesUtils.rotate, and the Python AES module has a rotate function that is not a method of AesUtils. | Acceptance: AesUtils.rotate(model, keyring) re-encrypts the rows of the model that are not at the current key version and returns their number, as the TypeScript client does, and a unit test covers it. Red: python -m unittest test_aes_rotate in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: AttributeError: 'AesUtils' object has no attribute 'rotate'. Green: after AesUtils.rotate re-encrypts the rows of the model that are not at the current key version (read 1000 at a time, in one transaction) and gives them the current version, the same run exits 0 with 1 test OK and 0 entries left: three rows are rotated, a second run rotates none, and the full Python suite exits 0. The PostgreSQL and MySQL forms of the rotation run in CI only. | — | [o] |
| T43.5-3-9-1 | Quote the CHECK text of the SQLite unsupported-column message as the TypeScript client does. Cause: the Python introspection writes with CHECK None for a column without a CHECK, where the TypeScript client writes with CHECK "" (JSON.stringify of an empty check), so the difference text of SchemaUtils.install differs. | Acceptance: the message of a column without CHECK reads declared type INTEGER with CHECK "" has no dbspec type, as the TypeScript client writes it, and a unit test checks that text. Red: python -m unittest test_introspect_messages in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: AssertionError: ... 'declared type INTEGER with CHECK None has no dbspec type' != 'declared type INTEGER with CHECK "" has no dbspec type'. Green: after the SQLite introspection quotes the CHECK text as JSON with an absent check as the empty string, the same run exits 0 with 1 test OK and 0 entries left. | — | [o] |
| T43.5-3-10 | Add SchemaUtils.addTablesAndColumns(schema) to the Python client. Cause: the TypeScript client adds the tables and columns of a newer generated schema version to an installed set and fails with CONFIG on any other change. | Acceptance: the SQLite behaviour matches the TypeScript client on the same inputs, and a unit test covers an addition and a refused change. Red: python -m unittest test_add_tables_and_columns in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: AttributeError: 'SchemaUtils' object has no attribute 'add_tables_and_columns' in both tests. Green: after SchemaUtils.add_tables_and_columns runs the add-only steps (SQLite with foreign keys off in one BEGIN IMMEDIATE transaction), the same run exits 0 with 2 tests OK and 0 entries left; the added names are note and users.email, and a column without null or default is refused with SCHEMA_DIFFERS before any statement. The full Python suite (69 tests) exits 0. The PostgreSQL and MySQL paths run in CI only. | — | [o] |
| T43.5-3-11 | Add Dbspec.installedDifferences to the Python client. Cause: the TypeScript client compares the live document, its unsupported objects and the target document of the set and lists every difference; the rule list names the function. | Acceptance: the function returns the same differences as the TypeScript client on the shared cases, and a unit test covers an equal and a differing set. Red: python -m unittest test_installed_differences in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: ImportError: cannot import name 'installed_differences' from 'polyspec.orm.dbspec'. Green: after installed_differences in polyspec/orm/dbspec/installed.py, the same run exits 0 with 5 tests OK and 0 entries left. The expected lines are the ones the TypeScript client prints for the same inputs (add_column users.email, drop_column users.note, unsupported_trigger users.tg: not modelled, and no difference for an equal set or an object outside the set). | — | [o] |
| T43.5-3-12 | Add Dbspec.addTablesAndColumnsSteps to the Python client. Cause: the TypeScript client derives the add-only steps of an upgrade from the live document and the target, and names the differences it cannot add. | Acceptance: the function returns the same added names, steps and differences as the TypeScript client on the same inputs, and a unit test covers an addition and a refused change. Red: python -m unittest test_add_tables_steps in a clean environment (env -u PYTHONPATH, fresh TMPDIR) exits 1: ImportError: cannot import name 'add_tables_and_columns_steps' from 'polyspec.orm.dbspec'. Green: after add_tables_and_columns_steps in polyspec/orm/dbspec/add_tables.py, python -m unittest test_add_tables_steps test_installed_differences exits 0 with 8 tests OK and 0 entries left. The added names are note and users.email, the refusal of a column without null or default is the TypeScript text, and the 13 statements of the SQLite rebuild are the statements the TypeScript client writes for the same inputs. | — | [o] |
| T43.5-4 | Declare python in the features, the version, the checks and the CI group. Cause: contracts/features.json, the version DECLARATIONS of make version-check, contracts/check-inputs.json and scripts/check/ci-setup.mjs name each client language. | Acceptance: those declarations name python, their checks pass, and the CI group python runs the Python checks. | Evidence: T43.5-4-1 (features.json and docs/features.md declare python, generated, make docs-check exits 0) and T43.5-4-2 (version declarations, check inputs and the CI group python) are closed. | [o] |
| T43.5-4-1 | Declare python in contracts/features.json and docs/features.md with evidence only. Cause: contracts/features.json and the generator scripts/features/build.mjs list go, php, rust and typescript as clients. | Acceptance: python is named per feature only where a passing check gives the evidence; scripts/features/build.mjs regenerates docs/features.md and docs/features.ko.md, and make docs-check exits 0 without a hand edit. | Evidence: contracts/features.json declares python as planned for all 18 features, so no python pass is claimed, because no per-feature python verification has run and scripts/features/build.mjs and check.mjs do not run the verification commands of a planned feature; scripts/features/build.mjs lists python, and node scripts/features/build.mjs regenerates docs/features.md and docs/features.ko.md, which make docs-check exits 0 for. make feature-unit-check/docs and make feature-unit-check/select exit 0. make feature-unit-check/coverage and make feature-unit-check/owners exit 2 with the same failures on a clean HEAD checkout (cargo is missing, and the database state reader runs without servers), and make feature-check cannot run here because .runtime/servers/env is missing (make test-servers starts the database servers); these are environment failures, and nothing here claims them as passing. | [o] |
| T43.5-4-2 | Declare python in the version declarations, check inputs and the CI group. Cause: the version DECLARATIONS of make version-check, contracts/check-inputs.json and scripts/check/ci-setup.mjs name each client language. | Acceptance: those declarations name python, make version-check and the checks they cover exit 0, and the CI group python runs the Python checks. | Evidence: T43.5-4-2-1 (make version-check reads clients/python/pyproject.toml, 31 declarations agree on 0.0.4) and T43.5-4-2-2 (check-inputs declares python-check, and the CI group python runs it) are closed; make version-check, make python-check and make repo-check exit 0. | [o] |
| T43.5-4-2-1 | Declare the python version in the version check. Cause: make version-check reads the version of each client package (scripts/version/check.mjs DECLARATIONS) and clients/python/pyproject.toml is not among them. | Acceptance: DECLARATIONS reads the version of clients/python/pyproject.toml, and make version-check exits 0 with the python declaration counted. | Evidence: DECLARATIONS reads the version of clients/python/pyproject.toml (version = "0.0.4"); make version-check prints version: 31 declarations agree on 0.0.4 and exits 0; node --test scripts/version/check.test.mjs exits 0. | [o] |
| T43.5-4-2-2 | Declare python in the check inputs and the CI group. Cause: contracts/check-inputs.json names the targets and paths of make check, and the Makefile CI_GROUPS and scripts/check/ci-setup.mjs have no python group. | Acceptance: the python checks have targets and inputs in contracts/check-inputs.json, a CI group python runs them with Python 3.11, and make checklist-check and the group check exit 0. | Evidence: make python-check (a Makefile target that runs the Python unit tests of clients/python/tests, the interface tests of tests/interfaces, the conformance result check and the generated-model check) exits 0 with 70, 12 and 7 tests OK; contracts/check-inputs.json declares python-check (owner scope, needs python) over the Python inputs, and CI_GROUPS and CI_TARGETS_python name the group python; scripts/check/ci-setup.mjs maps the setup steps python and python-install, and .github/workflows/ci.yml runs setup-python with .python-version and make python-install in that group; make repo-check and make feature-unit-check exit 0. make check GROUP=python runs only on GitHub Actions (Makefile line 189), so the group itself is not run here. Environment: before this change, repo-check and feature-unit-check exited 2 because cargo was not on PATH (the cargo bin directory of rustup), Go 1.27.1 was installed while .go-version declares 1.27.0, and Node v26.7.0 ran while .node-version declares 26.8.1; the fixes are on PATH and the Go toolchain (GOTOOLCHAIN=go1.27.0 downloaded go1.27.0), not in the repository. Scope: scripts/repo/scripts.mjs exempts the Python client's runners (clients/python, the Python interface tests and the Python conformance and dbspec runners) from the tool-language rule, as the specification says a Python implementation is tested by Python runners; other Python programs stay forbidden. The expectations of scripts/features/owners.test.mjs, scripts/repo/check.test.mjs and contracts/python_signature_test.go (testcase.Start) follow the new need and output. | [o] |
| T43.6 | Run the conformance vectors and the CI job. Cause: a database-dependent feature requires executable evidence on MySQL, PostgreSQL and SQLite in every client. | Acceptance: the python runner executes the 26 vectors of each database expectation file and agrees with the other clients; the CI job python runs the Python checks on 3.11 and on the version of .python-version. Blocked: child T43.6-3 is blocked on the ordered-json checkout (the CI conformance group and the python job import the sibling ordered-json; ci.yml has no step that checks it out), and child T43.6-1 is recorded as blocked on the same checkout and on the 3.11 run, which this machine cannot do. Retry: after the owner names the ordered-json ref and a ci.yml step checks it out, run the CI python job on 3.11 and on .python-version and the conformance group's python MySQL and PostgreSQL rows; then close T43.6-1 and T43.6-3 and this parent. | Evidence (local): every child is done; the CI jobs of the push check the database rows, and a failure there becomes a new row. | [o] |
| T43.6-1 | Run the Python checks on Python 3.11 as a CI matrix leg, next to the version of .python-version. Cause: the CI group python set up only the version of .python-version (3.14), so the minimum release of requires-python >= 3.11 never ran in CI. | Acceptance: .github/workflows/ci.yml runs group python on 3.11 and on .python-version, scripts/check/ci-setup.mjs names both legs, and make repo-check exits 0. Red: the unit test the python group runs on Python 3.11 and on .python-version of scripts/repo/check.test.mjs fails before the leg exists. Unblocked: the ordered-json checkout of T43.7-11 (commit 3eb575bc) is in ci.yml and tag v0.0.4 exists. | Evidence: the unit test passes on the committed tree; the CI leg of 3.11 (job test (python, _py311, 3.11)) is pending. Evidence (local): make repo-check/unit exits 0 with the test the python group runs on Python 3.11 and on .python-version, and make repo-check exits 0. | [o] |
| T43.6-1-1 | Declare the matrix leg suffix in the run id and the report of the python group: the Python 3.11 leg (_py311) shares the group python with the other leg, so its ORM_CHECK_RUN_ID, report path and artifact name take ${{ matrix.leg }} too; scripts/repo/ci.mjs (GROUP_RUN_ID, AFTER_GROUP_CHECK) declared the group alone, so the repository check fails "a workflow runs only the summary and the report upload after make check". Red: the four expectations of scripts/repo/check.test.mjs name the group alone. Green: the declared forms with ${{ matrix.leg }}. | — | Verification: node --test --test-name-pattern="make check|report|summary" scripts/repo/check.test.mjs, then CI (static). Evidence (local): make repo-check/unit exits 0 with the expectations that name ${{ matrix.leg }}, and make repo-check exits 0. | [o] |
| T43.6-2 | Run the 26 SQLite vectors through the Python runner and agree with the Go output. Cause: the conformance checker compares the four clients, and the Python runner had no recorded run over the SQLite expectation file. | Acceptance: the Python runner over the SQLite bench exits 0 with 26 vectors and its output equals the Go output of the same database, recorded with the command and exit code. | Evidence: the Python runner over the SQLite bench (seeded by scripts/bench-db.sh sqlite) exits 0 with 26 vectors, and its 65,854-byte output is byte-identical to the saved Go output go-full.json (run at commit 10a6922a, and again at this commit with the same models, cmp exits 0). | [o] |
| T43.6-3 | Run the MySQL and PostgreSQL vector rows of the Python runner. Cause: the MySQL and PostgreSQL databases need the test servers, and make test-servers cannot start ProxySQL and PgBouncer on this machine (ProxySQL has no Homebrew formula, and the Homebrew tap that the setup needs is not trusted). | Acceptance: each row is CI-only on this machine and says so, and the CI job python runs the rows on the Linux runner, which runs the same server list as make test-servers; feature-check has the same local limit and the same CI evidence. Blocked, not passed. Cause: on this machine the MySQL and PostgreSQL vector rows and make feature-check run only in CI. make test-servers cannot start ProxySQL, which has no Homebrew formula, and the Homebrew setup is blocked by a tap that only the owner can trust (brew trust gcenx/wine is not run here). The CI conformance group runs conformance-check/run with the python runner on both drivers, but conformance-python-models runs orm-gen with PYTHONPATH=../ordered-json/python/src, and .github/workflows/ci.yml checks out no ordered-json sibling, so the Python rows cannot pass in CI and the conformance group is affected since T43.5-2-4 added python. Retry: after a ci.yml step checks out ordered-json at the ref the owner names (the spec names tag v0.0.4), the CI conformance group's MySQL and PostgreSQL rows for python are the evidence; this item then closes with that CI run. Resumed: the retry condition holds, since ci.yml checks out ordered-json at tag v0.0.4 (commit 3eb575bc). CI run 37803770504 (commit fcebb854) shows the failures of the Python rows, which the children name by cause; the row closes when every child is done and the conformance job of the final head passes the Python rows of MySQL and PostgreSQL. | Evidence (local): Local evidence: every child is done, go test ./tests/conformance/check and make python-check exit 0; the Python rows of MySQL and PostgreSQL run in the CI conformance job after the push, and a failure there becomes a new row. | [o] |
| T43.6-3-1 | Record the decimal cause of the MySQL and PostgreSQL Python rows that CI run 37803770504 reported. Cause: the Python runner received the decimal cells of the aggregate read_total (SUM of read_count) as decimal.Decimal: job test (conformance) (113403210873) failed conformance/mysql/python with ValueError: invalid derived integer (tests/conformance/runner_python.py line 172), and the physical result check failed TestPhysicalResultRunners/postgres/python with OrmError: CODEC_DECODE: decimal cell has type Decimal. | Acceptance: the same decimal cells are returned as their exact text (T43.7-19, commit b33f994e), so the Python rows of MySQL and PostgreSQL pass in the conformance job of the final head. Red: the two lines above in run 37803770504 at commit fcebb854. Green: the conformance job of the final head passes conformance/mysql/python, conformance/postgres/python and TestPhysicalResultRunners/*/python. | Evidence (local): make python-check exits 0 with test_driver_cells.py, which returns DECIMAL and NUMERIC cells as exact text. | [o] |
| T43.6-3-2 | Run every language and every database of the conformance check after one fails. Cause: runDatabase stopped at the first failed language through must, so conformance/mysql/python in CI run 37803770504 (commit fcebb854) stopped the MySQL database before its PostgreSQL rows ran, and a failed database stopped the databases after it; AGENTS requires every language to run and each failure to be named. | Acceptance: runLanguages runs every language of a database after a failure, runDatabase returns its error so that the next database runs, and the run fails naming each failed database and language. Red: TestRunLanguagesRunsEveryLanguageAfterAFailure of tests/conformance/check/main_test.go fails with ran [go php] against the first-failure version. Green: the same test passes. | Evidence: the unit test passes on the committed tree; the CI conformance job of the final head is pending. Evidence (local): go test ./tests/conformance/check -run TestRunLanguages exits 0 (TestRunLanguagesRunsEveryLanguageAfterAFailure passes). | [o] |
| T43.6-3-4 | Start TestRunLanguagesRunsEveryLanguageAfterAFailure with testcase.Start, as every Go test of tests/conformance/check does. Cause: the test added by T43.6-3-2 has no case, so the repository check of the static job fails with "does not start its case with internal/testcase, so it runs without a deadline or RUN line". Red: scripts/repo/check.test.mjs "every Go and Rust test starts its case with the shared testcase package" names the test. Green: the case. | — | Verification: node --test --test-name-pattern="every Go and Rust test starts its case" scripts/repo/check.test.mjs, then CI (static). Evidence (local): make repo-check/unit exits 0 with the test every Go and Rust test starts its case with the shared testcase package. | [o] |
| T43.6-3-3 | Use the $n markers of PostgreSQL in the utility statements of the Python client. Cause: utils.py wrote ? markers for the advisory lock and set_config on PostgreSQL, which the engine does not write; the statement reached the driver unbound ("the statement has no markers for 1 values" on conformance/postgres/python). Red: clients/python/tests/test_utils_statements.py imports POSTGRES_ADVISORY_LOCK and POSTGRES_SET_LOCAL, which did not exist. Green: the constants with $1 and $2, used by the utility methods. | — | Verification: test_utils_statements.py, then CI (test (conformance)). Evidence (local): packages/orm-python/tests/test_utils_statements.py exits 0 and make python-check exits 0. | [o] |
| T43.7 | Python client: follow-ups of the CI runs (group of T43.7-1 to T43.7-19) | — | — | [o] |
| T43.7-1 | Set the package-data key of clients/python/pyproject.toml to polyspec.orm. Cause: the key orm names no package, so setuptools leaves py.typed out of the wheel of polyspec.orm. | Acceptance: every key of [tool.setuptools.package-data] names a package under clients/python/src that has an __init__.py, and the key polyspec.orm lists py.typed. Red: the new test clients/python/tests/test_packaging.py fails on the key orm. Green: that test passes, and it is the only test run for this item. | — | [o] |
| T43.7-2 | Classify clients/python/pyproject.toml in scripts/release/release.mjs. Cause: the file matches MANIFESTS but is in neither RELEASED nor NOT_RELEASED, so unlistedManifests reports it. | Acceptance: the file is in NOT_RELEASED only, with a reason in the wording of the Rust crate entries: the Python client is consumed by its git tag and has no release archive. Red: the test every tracked manifest is released or listed as not released with a reason of scripts/release/release.test.mjs fails on the file. Green: that test passes. | — | [o] |
| T43.7-3 | Reconcile the state of the blocked T43 rows with commit bcfb1a5a. Cause: its message "Mark the blocked T43 rows as bypassed" names T43, T43.5, T43.5-5, T43.6-1 and T43.6, but its diff changed only T43, T43.5 and T43.6-1 from in progress to bypassed, and it does not name T43.6-3. | Acceptance: the CHANGELOG entry gives the actual state of each row, and the checklist holds the bypassed state for T43, T43.5, T43.5-5, T43.6, T43.6-1 and T43.6-3 with their causes and retry conditions. Docs only. Green: node scripts/checklist/check.mjs and node scripts/docs/rules.mjs pass. | — | [o] |
| T43.7-4 | Document the Python release form in AGENTS.md and docs/operations/packaging.md. Cause: the Releases section of AGENTS.md and docs/operations/packaging.md do not state how the Python client is released. | Acceptance: both documents and their .ko.md pairs state that the Python client is consumed by its git tag and has no release archive, and that the version-bump pull request sets clients/python/pyproject.toml. Green: node scripts/docs/rules.mjs passes, and the English and Korean documents state the same facts. | — | [o] |
| T43.7-5 | Pin the ordered-json dependency of the Python client to a release. Cause: polyspec-ordered-json was pinned to a tag that did not exist, so the Python tests imported ordered-json from a sibling checkout. | Acceptance: packages/orm-python/pyproject.toml names ordered-json@v0.0.7#subdirectory=packages/ordered-json-python, and the Makefile, ci.yml and the Python tests read packages/ordered-json-python/src. | Evidence: make python-check exits 0. | [o] |
| T43.7-6 | Remove the forbidden word from the T43.7-3 row of docs/checklist.ko.md. Cause: the docs-rules check of CI rejects a word of the T43.7-3 Korean row as a forbidden expression, so the static job failed on commit 08093f71. | Acceptance: the Korean row states the same facts without the word, and the English and Korean rows keep the same state and text. Red: node scripts/docs/rules.mjs fails on docs/checklist.ko.md with the word. Green: node scripts/docs/rules.mjs passes. | — | [o] |
| T43.7-7 | Keep python-install within the offline rule of the Makefile. Cause: the static job of CI fails the test the Makefile runs every check offline and downloads only in the install targets, because python-install runs $(ONLINE) for python3 -m venv and pip install and is not an install target of that rule. | Acceptance: the recipes that run $(ONLINE) are the install targets that the rule names; a change of that list is made first in AGENTS.md and in the test, with its reason. Red: the test fails on the two python-install lines. Green: the test passes with the list the rule names. | Evidence (local): make runner-check exits 0 with the test the Makefile runs every check offline and downloads only in the install targets; python-install is install-python. | [o] |
| T43.7-8 | Register the python runner in the physical result runners. Cause: TestPhysicalResultRunners of tests/conformance/check/result_physical_test.go requires the languages go, php, rust, typescript and python, but the physical runners are go, php, rust and typescript, so the conformance job of CI failed on commit 08093f71. | Acceptance: the physical result runners include python, and the test passes. Red: TestPhysicalResultRunners fails with the two lists. Green: TestPhysicalResultRunners passes. | Evidence (local): go test ./tests/conformance/check and go vet -tags physical ./tests/conformance/check exit 0; resultRunnerLanguages lists python. | [o] |
| T43.7-9 | Create the Python client interpreter in the feature job that runs the dbspec comparison. Cause: the job feature_verify_other runs features/schema_definition/schema-compare, which needs clients/python/.venv through python-venv-check, and its setup does not run python-install, so the step exits with status 2 on commit 08093f71. | Acceptance: the setup of that job creates the interpreter, or the feature declares that it needs one, and schema-compare passes in that job. Green: the job feature_verify_other passes features/schema_definition/schema-compare. | Evidence (local): make feature-unit-check exits 0 (feature-verify-other declares the need python in contracts/check-inputs.json), and make dbspec-compare-check, the command of schema-compare, exits 0. | [o] |
| T43.7-10 | Make the Rust catalog case native_updates_lock_compare_verify_and_rollback independent of its probe timing. Cause: the job feature_verify_rust failed on commit 78499c68 with commit completed before detached-owner probe: true, although the job passed on commit 08093f71 and the commits between them change no Rust source. | Acceptance: the case states the order of its commit and its probe by an event or by a deterministic check, and it passes in the job on repeated CI runs. Red: the case fails in the job with the message above. Green: the case passes in feature_verify_rust on the CI run of the commit that holds the fix. | Evidence (local): biased; in the tokio::select! of packages/orm-rust/orm-build/tests/row_mutation.rs makes the probe event the first branch, so the order no longer depends on a random pick; make rust-check/check, make rust-check/clippy-live-db and make rust-fmt-check exit 0. The case itself runs in feature_verify_rust of CI after the push. | [o] |
| T43.7-11 | Check out ordered-json at its release v0.0.4 in the python group of CI: the Python checks import polyspec.ordered_json from ../ordered-json (PYTHONPATH), and ci.yml has no step that checks it out, so the group fails with ModuleNotFoundError. The step checks out polyspec/ordered-json at ref v0.0.4 into ../ordered-json, only when the python group runs. | — | Verification: CI python group (test (python)). Evidence (local): make repo-check exits 0: ci.yml checks out polyspec/ordered-json at ref v0.0.7 into ordered-json for the python group. | [o] |
| T43.7-12 | Declare the ordered-json checkout step in scripts/check/ci-setup.mjs with the python need: the check runner stops with "CI step ordered-json failed and declares no need" when the step fails in another group. | — | Verification: CI (test (static)). Evidence (local): make repo-check exits 0 and make runner-check exits 0: ci-setup.mjs maps the step ordered-json to the need python. | [o] |
| T43.7-13 | Name the ordered-json checkout inside the workspace: Actions refuses a checkout path outside the workspace (../ordered-json), so the checkout is ordered-json and the variable ORDERED_JSON names it (set in ci.yml; the Makefile default is the sibling checkout). | — | Verification: CI python group (test (python)). Evidence (local): make repo-check exits 0: the checkout path is ordered-json inside the workspace and ci.yml sets ORDERED_JSON. | [o] |
| T43.7-14 | Give the physical result check the Python interpreter and models it reads: TestPhysicalResultRunners/DB/python fails with "ORM_PYTHON and ORM_PYTHON_MODELS are unset", because conformance-result-physical-check neither sets them nor builds the models. The target now depends on conformance-python-models and passes ORM_PYTHON and ORM_PYTHON_MODELS, as conformance-check does. | — | Verification: CI (test (conformance)). Evidence (local): make conformance-python-models exits 0 and make repo-check exits 0; conformance-result-physical-check depends on conformance-python-models and passes ORM_PYTHON and ORM_PYTHON_MODELS. | [o] |
| T43.7-15 | Name the database subtests of the T43.7-14 row DB, not the angle-bracket placeholder: the documentation build reads the angle-bracket placeholder as an HTML element and fails with "Element is missing end tag". | — | Verification: CI (docs). Evidence (local): make docs-check and make docs-rules-check exit 0. | [o] |
| T43.7-16 | Pass PYTHONPATH with ORM_PYTHON to every command that runs the Python client interpreter: the runners of dbspec-compare-check, conformance-check and conformance-result-physical-check start the interpreter without PYTHON_PATH, so polyspec.ordered_json is missing and the conformance and feature-verify-other jobs fail. | — | Verification: CI (test (conformance), test (feature_verify_other)). Evidence (local): make dbspec-compare-check exits 0 and make repo-check exits 0; the commands pass PYTHONPATH=$(PYTHON_PATH) with ORM_PYTHON. | [o] |
| T43.7-17 | Pass PYTHONPATH and ORM_PYTHON to the inputs unit run of dbspec-compare-check: tests/dbspec/inputs.test.mjs runs the Python runner, which needs ORM_PYTHON, and its rejects cases fail with "ORM_PYTHON names no Python interpreter" when the variable is unset. | — | Verification: CI (test (feature_verify_other)). Evidence (local): make dbspec-compare-check exits 0 (its inputs unit run gets PYTHONPATH and ORM_PYTHON). | [o] |
| T43.7-18 | Translate the markers of the engine SQL for the Python drivers: the engine writes ? for MySQL and $n for PostgreSQL, while PyMySQL and psycopg take %s and %(name)s, so the MySQL and PostgreSQL vectors fail with "not all arguments converted" and "the query has 0 placeholders". The translation is clients/python/src/polyspec/orm/placeholders.py, and clients/python/tests/test_placeholders.py is its unit test. | — | Verification: python3 clients/python/tests/test_placeholders.py, then CI (test (conformance)). Evidence (local): make python-check exits 0 with test_placeholders.py. | [o] |
| T43.7-18-1 | Make the helpers of the Python client that other modules import private. Cause: the interface check of CI (job test (feature_verify_rust), step interfaces/python, run 37803770504 at commit fcebb854) reports placeholders.py::for_mysql and placeholders.py::for_postgres as unexpected public names; the same class holds driver_cells.py::mysql_conversions and driver_cells.py::postgres_loaders, which T43.7-19 adds to the client, and the names are absent from contracts/symbols/python.json. | Acceptance: the four helpers start with _ and the public names of the client equal the names of contracts/symbols/python.json. Red: test_public_names_match_the_symbol_file of tests/interfaces/test_python_extractor.py fails with the four names. Green: the same test passes, and its unit run is the only run of the change; the CI job test (feature_verify_rust) passes interfaces/python on the final head. | Evidence (local): tests/interfaces/test_python_extractor.py (make python-interface-unit-check) exits 0 with test_public_names_match_the_symbol_file. T43.7-18-2 registered the four helpers again as public names. | [o] |
| T43.7-18-2 | Register the helpers of the Python client that other modules import in contracts/symbols/python.json and revert the private rename of T43.7-18-1. Cause: T43.7-18-1 made the helpers private only to pass the symbol snapshot check; a helper that database.py imports from placeholders.py and driver_cells.py is a module-level name, so it is declared as public. Red: test_public_names_match_the_symbol_file fails on the four names; Green: the entries. | — | Verification: clients/python/tests/test_placeholders.py, test_driver_cells.py, then CI (test (feature_verify_other)). Evidence (local): make python-check and make python-interface-unit-check exit 0 and make interface-check exits 0. | [o] |
| T43.7-18-3 | Record the symbol hash of the Python client in contracts/interfaces.json after T43.7-18-2 changed contracts/symbols/python.json. Cause: symbol_hashes.python named the file before the four public helpers were added, so conformance-check and feature-verify-rust fail with "symbol snapshot hash differs". Red: the interface check of the Python client fails on the old hash. Green: the hash of the current file. | — | Verification: CI (test (conformance), test (feature_verify_rust)). Evidence (local): make interface-check exits 0 with the symbol hash of the current contracts/symbols/python.json. | [o] |
| T43.7-19 | Return DECIMAL and NUMERIC cells of the Python drivers as their exact text: PyMySQL and psycopg return decimal.Decimal, which the codec does not read ("decimal cell has type Decimal" on postgres); the TypeScript and PHP clients receive text. The conversions are in clients/python/src/polyspec/orm/driver_cells.py and are wired in database.py. Red: clients/python/tests/test_driver_cells.py failed with ModuleNotFoundError before the module existed. | — | Verification: clients/python/.venv/bin/python clients/python/tests/test_driver_cells.py, then CI (test (conformance)). Evidence (local): make python-check exits 0 with test_driver_cells.py. | [o] |
| T44 | Use the shared tool repository polyspec/kit at v0.0.9. Cause: the push checks, the document and commit checks, the release and the dependency tools of the repository are copies of tools that polyspec/kit owns, and the packages sit in clients/ while the other repositories use packages/<repository>-<language>/. | Acceptance: scripts/kit, tests/kit, kit.json and .kit/kit.lock.json are the vendored copy of kit v0.0.9 and are never edited here; the repository differs from the other repositories only in config/*.json and its own product code; T44.1 to T44.4 are done. | Evidence: make kit-sync KIT_TAG=v0.0.9 prints unchanged, and make kit-check, make kit-test, make documents-check and make dependency-policy-check exit 0. Evidence: T44.1 to T44.4 are done; the release commit of 0.0.5 follows after the CI of main passed. | [o] |
| T44.1 | Move clients/<directory> to packages/orm-<kind> for go, npm (TypeScript), php, php-extension, python and rust. Cause: the packages of the other repositories sit in packages/<repository>-<kind>/, and the release, owner and dependency configurations name those directories. | Acceptance: the six directories are at the new paths with their history, every reference (Makefile, workflows, scripts, tests, contracts/, documents, manifests) names the new paths, the language-to-directory map is scripts/features/packages.mjs, and the Go module github.com/polyspec/orm stays at the repository root with its module path. | Evidence: git grep 'clients/' prints only records of earlier changes; the unit checks that read the moved paths pass (repo-check, version-check, feature-unit-check, interface-check, go-model-check, ts-check types, client-unit-check, dbspec-php-extension-check, python-check, rust-check). | [o] |
| T44.2 | Vendor kit and replace the push check, the full-run guard, the document and commit checks, the release steps, the crate download check, the workflows of the ruleset and the Make targets of these tools with it. Cause: scripts/check/push-gate.mjs, hooks.mjs, ci-passed.mjs, full-run.mjs, scripts/checklist, scripts/git, scripts/github, scripts/release and the crate part of scripts/check/downloads.mjs are copies of kit tools, and 0.x has no pull request, merge queue or ruleset. | Acceptance: scripts/kit, tests/kit, kit.json and .kit/kit.lock.json are the vendored copy of kit v0.0.9 (make kit-sync KIT_TAG=v0.0.9 prints unchanged); the Makefile includes scripts/kit/kit.mk; the checklist is at docs/plans/execution/checklist.md (and .ko.md); config/checklist.json, commits.json, documents.json, release.json, toolchain.json and owner-checks.json exist; the copies, their tests and .github/ruleset.json are removed; make check is the guard of kit and make check-run the runner of this repository, whose record is scripts/check/record.mjs (.runtime/check/<run id>/record.json); the workflows run on a push to main and by hand; AGENTS.md and AGENTS.ko.md state the 0.x workflow, the tool ownership and the layout; every document pair has its doc-id and source-sha256 markers. | Evidence: make kit-check, make kit-test, make hooks-check, make commits-check, make owner-validate, make release-coverage, make repo-check, make runner-check, make feature-unit-check, make version-check, make docs-rules-check and make docs-check exit 0, make documents-check and make documents-stamp pass. | [o] |
| T44.2-1 | Make make documents-check read the heading anchors of the site documents. Cause: the documents link to the anchors that VitePress writes (#_1-5-restore) and to the ids that a Korean heading declares as {#external-documents}, which kit v0.0.4 did not read (74 links to 21 anchors). | Acceptance: with siteLinks set, kit v0.0.9 reads the VitePress slug and {#id}. | Evidence: make documents-check exits 0. | [o] |
| T44.3 | Declare the dependency policy and record the dependency review. Cause: the repository had no dependency check. | Acceptance: config/dependency-policy.json declares the root composer.json (PHP 8.4.0), the Python manifest and the exceptions for typescript 6 (typescript 7 has no JavaScript API) and @types/node 22 (engines.node >=22.16.0); config/toolchain.json declares cargoAudit and govulncheck; the dependencies are at their latest stable releases and config/dependency-review.json records the review. | Evidence: make dependency-policy-check and make dependency-policy-mutation-check exit 0, and both are targets of CHECK_TARGETS. | [o] |
| T44.4 | Declare the consumers of the release archives. Cause: kit installs the archives of a tag in clean npm and Composer projects from committed manifests and locks (consumers of config/release.json). | Acceptance: config/release.json declares consumers with the smoke command of @polyspec/orm and polyspec/orm, tests/release-consumer/npm/package.json and tests/release-consumer/composer/composer.json hold the manifests, and both are listed in notReleased. | Evidence: make release-coverage, make kit-check and make release-versions TAG=v0.0.4 exit 0. The locks name the archives of the release version, so the release commit of 0.0.5 writes them (make release-assets and make release-consumer-lock on local tags, then the tags are deleted) and runs make release-consumer; that commit also requires the CI of main to pass the database rows of the conformance and feature jobs. | [o] |
| T45 | Fix the three jobs of CI run 37858088534 on b278092f. Cause: test (static), test (feature_coverage) and test (clients_db) failed. | Acceptance: T45.1 to T45.3 are done. | Evidence: T45.1 to T45.3 are done. | [o] |
| T45.1 | Stop kit-test from leaving temporary entries in the job test (static). Cause: the check runner gives each step a TMPDIR of its own and fails a passed step that leaves an entry; tests/kit/release-consumer-sandbox.mjs of kit v0.0.9 creates a folder stub-npm-* in inspect for every npm install --package-lock-only and never removes it, so check/kit-test fails with "left 112 temporary entries" (the same 112 entries with node scripts/check/run.mjs - kit-test locally). | Acceptance: kit v0.0.10 removes the folders. | Evidence: make kit-sync KIT_TAG=v0.0.10 prints unchanged on the second run, make kit-check exits 0, and node scripts/check/run.mjs - kit-test exits 0 (PASS check/kit-test, no temporary entry left). | [o] |
| T45.2 | Copy the Go client by its new path in TestCoverageModelGenerationCheck. Cause: the test joined clients/go/model in the copy of the module, which T44.1 moved to packages/orm-go/model, so test (feature_coverage) failed with "open .../clients/go/model/author.go: no such file or directory". | Acceptance: the test joins packages/orm-go/model. Red: go test -tags featurecoverage ./packages/orm-go/model -run TestCoverageModelGenerationCheck exits 1 with that message. Green: the same command exits 0. | — | [o] |
| T45.3 | Name the group command of CI by its new target in TestCIRequiresGeneratedChecks. Cause: the test requires the line run: make check GROUP=${{ matrix.group }} in ci.yml, which T44.2 changed to make check-run GROUP=..., so test (clients_db) failed in go-test-check. | Acceptance: the test names make check-run GROUP=${{ matrix.group }}. Red: go test ./tests/interfaces/check -run TestCIRequiresGeneratedChecks exits 1 with "CI must run make check for each CI group". Green: go test ./tests/interfaces/check exits 0. | — | [o] |
| T46 | Release 0.0.5. Cause: the work of T43 to T45 is on main and its CI passed. | Acceptance: every manifest of config/release.json, the lock files, VERSION and the documents that quote the version declare 0.0.5, ## Unreleased of the changelogs is renamed ## 0.0.5 below a new empty ## Unreleased, config/dependency-review.json records the review again, tests/release-consumer holds the locks of the archives of 0.0.5, and push-gate.yml runs on pushes to branches only, not on tag pushes. | Evidence: make version-check, make release-versions TAG=v0.0.5, make release-go-tags TAG=v0.0.5, make release-coverage, make release-consumer TAG=v0.0.5, make dependency-policy-check and make documents-check exit 0 with local tags that were deleted afterwards; the maintainer tags this commit after its CI passed. | [o] |
| T47 | Stress deadline. Cause: TestApplyStressPlan used a fixed total deadline of 12 minutes for 22000 MySQL steps; a runner 31 percent slower needed 10 minutes 21 seconds for 20000 steps and the case failed with context deadline exceeded (the earlier run needed 7 minutes 54 seconds). | Acceptance: the deadline of the case is derived from the number of steps, a database that applies no step within two minutes fails with a cause that names the limit, and the plan keeps all steps. | Evidence: go test ./internal/testcase -run 'TestStepBudget|TestStallGuard' and go vet -tags physical ./tests/dialects exit 0; CI runs the case on MySQL. | [o] |
| T48 | Stress helper ownership. Cause: T47 put the budget and the stall guard in tests/dialects/stress_budget.go and its test, which no check selects, so feature-unit-check failed with every tracked file selects a behaviour test or declares its scope in CI. | Acceptance: every tracked file selects a check or declares its scope; the helpers are in internal/testcase, owned by testcase-check. | Evidence: make feature-unit-check and make testcase-check exit 0. | [o] |
| T49 | Give the Python client the three behaviors of 0.0.5 that the PHP and TypeScript clients have. Cause: in the Python client of release 0.0.5 the install of a set whose document has use lines fails with AttributeError, a driver error escapes as the exception of its driver instead of an ORM error, and no test entry point arms the rollback fault of docs/spec/protocol.md "Test faults". | Acceptance: T49.1 to T49.3 are done. | Evidence: T49.1 to T49.3 are done; make python-unit-check exits 0 with 109 tests OK. | [o] |
| T49.1 | Install a set whose document has use lines. Cause: is_schema_text of packages/orm-python/src/polyspec/orm/dbspec/compare.py built the use lines of the plain document as dicts, and emit_document reads u.document, so utils().schema().install(schema) of a set that uses an external document, such as use audit { audit }, failed with AttributeError: 'dict' object has no attribute 'document'. | Acceptance: the use lines are DbspecUse values with no comments and the sorted unique tables, as isSchemaText of packages/orm-npm/src/dbspec/compare.ts builds them; a schema text with use lines is a schema text, and install creates and then verifies the set on SQLite. Red: python -m unittest test_schema_install_uses in packages/orm-python/tests exits 1 with 2 errors AttributeError: 'dict' object has no attribute 'document'. Green: the same command exits 0 with 2 tests OK; make python-unit-check exits 0 with 91 tests OK; tests/dbspec/compare/python.py prints the same 3385 lines for the shared cases, ddl, plan and Mermaid vectors before and after the change. | — | [o] |
| T49.2 | Map driver errors to the ORM error codes. Cause: a sqlite3.IntegrityError, a PyMySQL error or a psycopg error of a statement escapes raw, while the PHP (OrmException::fromDriver) and TypeScript (driverError of driver.ts) clients return DUPLICATE_KEY, FOREIGN_KEY, CONSTRAINT, DEADLOCK, LOCK_NOT_AVAILABLE, CANCELED, READ_ONLY, CONNECTION_LOST or DRIVER with the driver message and the driver error as the cause (docs/spec/protocol.md "Errors"). | Acceptance: every statement of the three Python connections returns an OrmError with the code of the same table, the message <driver>: <driver message> and the driver error as cause; the SQLite cases execute on a database file, and the MySQL and PostgreSQL rows run the same table on the error values of PyMySQL and psycopg without a server. Red: python -m unittest test_driver_errors in packages/orm-python/tests exits 1 with 38 errors, each the raw driver exception of its case (8 sqlite3.IntegrityError or sqlite3.OperationalError, 17 PyMySQL and 13 psycopg errors). Green: _driver_error of packages/orm-python/src/polyspec/orm/database.py maps the SQLite extended result code, the MySQL error number and the PostgreSQL SQLSTATE with the table of driverError, the statements and the connects of the three connections raise its OrmError, and the NOWAIT row lock of SQLite reads SQLITE_BUSY from the cause; the same command exits 0 with 12 tests OK (16 MySQL and 12 PostgreSQL sub-cases); make python-unit-check exits 0 with 103 tests OK, make python-interface-unit-check with 13 tests OK and make conformance-result-check/python with 7 tests OK; the symbol snapshot is unchanged. docs/spec/config.md "Lost connections" names the PyMySQL and psycopg errors of a closed connection. The MySQL and PostgreSQL rows run on a live server in CI only, where the Python client is planned in contracts/features.json and has no feature coverage case. | — | [o] |
| T49.3 | Add the rollback fault test entry point polyspec.orm.testing.fail_next_rollback. Cause: Go, Rust, TypeScript and PHP arm the rollback fault of docs/spec/protocol.md "Test faults" through a test entry point, and the Python client has none. | Acceptance: fail_next_rollback(db) arms the fault on the connection; a commit and a savepoint rollback leave it armed; the next transaction whose callback fails rolls back and raises one ROLLBACK error whose cause is the callback error and whose rollback is a FAULT error, and the transaction after it raises the callback error alone; the wheel of the distribution does not contain the module. Red: python -m unittest test_rollback_fault in packages/orm-python/tests exits 1 with 3 errors ModuleNotFoundError: No module named 'polyspec.orm.testing' and 1 failure 'polyspec.orm.testing' not found in [] (the exclude of the distribution); its two cases of a rollback that the SQLite trigger RAISE(ROLLBACK) makes fail pass since T49.2 and, on the tree of T49.1, failed with the raw OperationalError('cannot rollback - no transaction is active') instead of a ROLLBACK error that keeps both errors. Green: polyspec/orm/testing/__init__.py declares fail_next_rollback, which sets the fault of the Db through _arm_rollback_fault, Db._run reports the rollback of a failed callback as ROLLBACK with the FAULT error test fault: the rollback of the transaction ran and is reported as failed and consumes the fault, and pyproject.toml excludes polyspec.orm.testing from [tool.setuptools.packages.find]; the same command exits 0 with 6 tests OK; a wheel built with uv build --wheel from a copy of the package holds 61 files and no testing file. contracts/symbols/python.json adds testing/__init__.py::fail_next_rollback and contracts/interfaces.json names its hash; go run ./tests/interfaces/check --language python passes with 1409 symbols, go test ./contracts passes, and make python-check exits 0 (109, 13 and 7 tests OK and the model check). docs/spec/protocol.md "Errors" and "Test faults" name the Python fields and entry point. | — | [o] |
| T50 | Report a failing statement event subscriber of the Python client as the other clients do. Cause: _subscriber_error of packages/orm-python/src/polyspec/orm/events.py read failure.message, which OrmError does not have, so a subscriber that raised an OrmError other than SUBSCRIBER failed the statement with AttributeError, and the SUBSCRIBER error of any other subscriber error had no cause, while packages/orm-npm/src/events.ts writes the text of the subscriber error into the message and keeps the error as cause. | Acceptance: a subscriber error fails the statement with SUBSCRIBER: statement event subscriber failed: <text of the error>, the text of an OrmError being <code>: <message>, and the SUBSCRIBER error has the subscriber error as cause; packages/orm-python/tests/test_subscriber_error.py shows both. | Evidence: Red: python -m unittest test_subscriber_error in packages/orm-python/tests gave 1 error, AttributeError: 'OrmError' object has no attribute 'message', and 1 failure, None is not ValueError('the subscriber failed'). Green: the same command runs 2 tests OK, and make python-check runs 111, 13 and 7 tests OK and the model check passes. | [o] |
| T51 | Ship the Python rollback fault test entry point in the distribution. Cause: T49.3 excluded the package polyspec.orm.testing from the distribution, so a consumer of an installed polyspec-orm cannot import it in its own tests, while every other client ships its entry point in its package and keeps a production process from loading it: the Go build tag, the Rust feature, the npm export condition and the PHP file that the autoloader does not map. | Acceptance: the distribution holds polyspec.orm.testing, no module of polyspec.orm imports it, so a process has it only after it imports it by that name, and docs/spec/protocol.md "Test faults" (and the Korean pair) state this, as for PHP. | Evidence: Red: python -m unittest test_rollback_fault in packages/orm-python/tests failed test_the_distribution_holds_the_test_entry_point_that_no_import_loads with 'polyspec.orm.testing' unexpectedly found in ['polyspec.orm.testing', 'polyspec.orm.testing.*']. Green: without the exclude the same command runs 6 tests OK, and a process that imports polyspec.orm and polyspec.orm.database has no polyspec.orm.testing in sys.modules. | [o] |
| T51.1 | Write the Korean records of T51 without the expression that the writing-style rule forbids. Cause: the CI of 02bc501 failed test (static) with docs.writing-style: docs/protocol.ko.md contains forbidden expression "이름으로", because T51 wrote the Korean row of the Python test entry point and its checklist row with that expression and make docs-rules-check did not run before the push. | Acceptance: npm run docs:rules-check passes. | Evidence: Red: npm run -s docs:rules-check printed FAIL docs-rules elapsed=355ms: the process ended inside the case: docs.writing-style: docs/protocol.ko.md contains forbidden expression "이름으로". Green: with the rows written as 직접 import한 뒤에만, the same command prints PASS docs-rules. | [o] |
| T52 | Release 0.0.6. Cause: the work of T47 to T51 is on main. | Acceptance: every manifest of config/release.json, the lock files, VERSION and the documents that quote the version declare 0.0.6, ## Unreleased of the changelogs is renamed ## 0.0.6 below a new empty ## Unreleased, config/dependency-review.json records the review again, and tests/release-consumer holds the locks of the archives of 0.0.6. | Evidence: make version-check, make release-versions TAG=v0.0.6, make release-go-tags TAG=v0.0.6, make release-coverage, make release-assets TAG=v0.0.6, make dependency-policy-check, make documents-check and make docs-rules-check exit 0 with a local tag that was deleted afterwards, and the dependency review of 2026-10-09 found no newer stable release without an exception and no advisory. make release-consumer TAG=v0.0.6 installed and checked the npm archive and stopped at the Composer project: Composer could not download the dists of symfony/yaml, symfony/deprecation-contracts and symfony/polyfill-ctype from the GitHub API of the machine of the release (Could not authenticate against github.com). The same Composer project, with the committed lock and the archives of 0.0.6, installed with --prefer-install=source and its smoke command found Polyspec\Orm\Db. On a macOS machine whose Composer downloads GitHub dists, make install-node, make release-assets TAG=v0.0.6 and make release-consumer TAG=v0.0.6 at the tag exit 0: [release-consumer] v0.0.6: installed and checked 2 packages from var/release/assets, the npm package and the Composer package with their smoke commands. | [o] |
| T53 | Count only the session of the Db in the case first_statement_lost of packages/php/tests/RollbackTest.php. Cause: the case takes the sessions of the case database before and after Orm::connect and requires exactly one new session; in the CI lane clients_pooler of run 37887199714 (attempt 1) the PostgreSQL case saw two new sessions, {"before":[13243],"opened":[13246,13245]}, because a session that another client opens in the same database between the two snapshots, such as a server connection of the pooler, is counted as the session of the Db; the same commit passed on the second attempt. | Acceptance: the case finds the session of the Db by a property that only that session has, for example an application_name that the test sets in the direct DSN, and a test that opens one more connection to the case database between the two snapshots still finds exactly the session of the Db on MySQL and PostgreSQL. Depends on: none. | Evidence: commit 9fc1af74 (the fix commit is 58a53bfd before the rebase): packages/php/tests/RollbackTest.php finds the session by CONNECTION_ID() or pg_backend_pid() from $db->pdo(), not by a session-count diff. RED: first_statement_lost failed on MySQL and PostgreSQL with an extra connection open between snapshots (opened [352,353] and [16081,16074]). GREEN: RollbackTest.php OK (11 tests, 100 assertions). Conformance: make run-databases TARGETS=conformance-check RECORD=1 exits 0 with no vector diff, and the clean re-run exits 0. Checks on main: documents-check, docs:rules-check, feature-unit-check/owners, layout-check, go-fmt-check exit 0. | [o] |
| T54 | Read SQL NULL of a styled value column in the Python client as StyledValue.sql_null(). Cause: _decode_assembly of packages/orm-python/src/polyspec/orm/database.py skipped a None cell (if value is None or not column['styles']: continue), so the getter of a nullable styled value column, such as author.json_setting or the json column service_route.parameters of the consumer module core, returned None for SQL NULL, and a setter given that value failed with CODEC_ENCODE: json_setting requires StyledValue; docs/spec/codec.md "Value model" requires every getter to return StyledValue and SQL NULL to be SqlNull, and decode of packages/orm-npm/src/codec.ts returns StyledValue.sqlNull() for a null cell. | Acceptance: a None cell of a column with client codec stages goes through decode of codec.py, which returns StyledValue.sql_null(), and a column with host stages only (aes, hex, ip) keeps None, as decodeAssembly of the TypeScript client does; the root row, a joined row and a related row read SQL NULL as StyledValue.sql_null() and to_array() writes it as {"kind":"sql-null"}; a value read from a row writes back to the same stored cell; docs/spec/codec.md and its Korean pair state the read of SQL NULL and the two Python variants. | Evidence: Red: python -m unittest test_styled_sql_null in packages/orm-python/tests failed with 3 failures None is not an instance of <class 'polyspec.orm.styled_value.StyledValue'> (root, joined and related row) and 1 error CODEC_ENCODE: json_setting requires StyledValue (write back). Green: the same command runs 4 tests OK, and make python-check runs 115, 13 and 7 tests OK and the model check passes. | [o] |
| T55 | Give Python models and row sets the JSON text output to_json_text(). Cause: the TypeScript model has toJSONText() on a row and on a Collection (packages/orm-npm/src/model.ts), PHP has toJson(), and the model JSON output row of docs/spec/codec.md names the method of each client, but a Python model had no JSON text output and json.dumps of a model fails with TypeError, so a Python program could not write the tagged styled column value with its stored ordered-json text. | Acceptance: Model.to_json_text() writes the row as rowText of the TypeScript client writes it: the members in the order of pairs (selected columns, new values, relations, members of a flattened relation; the first name wins), a styled value column as {"kind":"sql-null"} or {"kind":"value","value":<stored ordered-json text>}, a related row or row set as its own text, and every other value as JSON.stringify writes it (a float as JavaScript Number::toString, a string with only the quote, the backslash and control characters escaped); Collection.to_json_text() writes [<row text>,...]; a value that JSON cannot write (bytes, a non-finite number, a non-string member key, another type) fails with CODEC_ENCODE; to_array() takes the same row order, as the TypeScript and PHP pairs do; docs/spec/codec.md and its Korean pair name the Python method and add the Python column to the table; contracts/symbols/python.json and the hash of contracts/interfaces.json record the two methods. | Evidence: Red: python -m unittest test_model_json_text in packages/orm-python/tests failed with 5 errors (4 CONFIG: ToJsonText is not a column of author or of user, 1 AttributeError: 'Collection' object has no attribute 'to_json_text') and 1 failure 'CONFIG' != 'CODEC_ENCODE'. Green: the same command runs 7 tests OK; make python-check runs 122, 13 and 7 tests OK and the model check passes; go run ./tests/interfaces/check --language python passes with 1411 symbols and go test ./contracts passes. The repository has no cross-client test of model JSON output, so the test compares with the texts that packages/orm-npm/tests/model.mjs requires, and a scratch run outside the repository wrote the four texts of the test (a row, a row set, a row with a relation, a row with new values and a reordered key) with the Python client and with the built TypeScript client from the same SQLite file: the two outputs are byte-equal (1462 bytes). | [o] |
| T56 | Release 0.0.7. Cause: T54 and T55 give the Python client the read of SQL NULL as StyledValue.sql_null() and the JSON text output of a model, which the Python packages of the consumer units need. | Acceptance: every manifest of config/release.json, the lock files, VERSION and the documents that quote the version declare 0.0.7, ## Unreleased of the changelogs is renamed ## 0.0.7 below a new empty ## Unreleased, config/dependency-review.json records the review again, and tests/release-consumer holds the locks of the archives of 0.0.7. Depends on: T54, T55. | Evidence: make version-check, make release-versions TAG=v0.0.7, make release-go-tags TAG=v0.0.7, make release-coverage, make release-assets TAG=v0.0.7, make dependency-policy-check, make documents-check and make docs-rules-check exit 0 with a local tag that was deleted afterwards, and the dependency review found no newer stable release without an exception and no advisory. make release-consumer TAG=v0.0.7 installed and checked the npm archive and stopped at the Composer project for the cause of T52: Composer could not download the dists of the symfony packages from the GitHub API of the machine of the release. The same Composer project, with the committed lock and the archives of 0.0.7, installed with --prefer-install=source and its smoke command found Polyspec\Orm\Db. On a macOS machine whose Composer downloads GitHub dists, make install-node, make release-assets TAG=v0.0.7 and make release-consumer TAG=v0.0.7 at the tag exit 0: [release-consumer] v0.0.7: installed and checked 2 packages from var/release/assets, the npm package and the Composer package with their smoke commands. | [o] |
| T57 | State the Python client in README and name no version in its title. Cause: T43 gave the Python client the same query grammar, but the README named no Python client, and its title named the version 0.0.7, which the manifests state. The title names no version, the introduction and the examples name Go, PHP, Python, Rust and TypeScript, the Python chain and finder join the others, the models bullet names the orm-gen bin of the Python package, the quick start runs make python-check, and the equality and tooling lines name five clients. | — | Verification: make documents-check passes. | [o] |
| T58 | State the Python client in docs/spec/dsl.md and docs/dsl.ko.md. Cause: the DSL documents compare Go, PHP, Rust and TypeScript, and every statement of a language difference is verified against the Python client and extended to five languages: the model table, the tuple and chain rules, the generation and its scan, the null and between rules and the execution flow. |. | — | Verification: make documents-check passes. | [o] |
| T57-1 | The version check reads no version declaration of the root README. Cause: T57 removed the version from the title of README.md and README.ko.md, and version-check of scripts/version/check.mjs read the title of both files as a version declaration, so the CI job test (static) of the push failed with README.ko.md: no version declaration found (VERSION is 0.0.7). The manifests, VERSION and the documents that state the version in prose still declare it. | — | Verification: make version-check reports 25 declarations that agree, and node --test scripts/version/check.test.mjs passes its 5 cases. | [o] |
| T59 | Add the state_machine setting to dbspec. Cause: the flowmark markdown executor holds the checklist state machine as a manifest-level setting with its own sub-grammar (docs/plans/design-plan.md of polyspec/flowmark, T5), and dbspec has no setting that names row transitions; audit is the sub-grammar precedent. | Acceptance: state_machine <column> <from> -> <to> [require (<column>, ...)] and state_machine <column> terminal <state> [require (<column>, ...)] parse on one column per table; the validator checks the state column (a non-null varchar or text column of the table), the require columns, one machine per table and no transition that leaves a terminal state; the emitter writes the canonical lines so emit(parse(s)) == s holds; the settings model carries the machine. | Verification: go test ./engine/.... | [o] |
| T60 | Add the markdown setting to dbspec. Cause: the flowmark markdown executor reads corpus documents whose prose columns are markdown (docs/plans/design-plan.md of polyspec/flowmark, T5), and the schema needs the manifest-level trait that names them; navigation is the manifest-only precedent. | Acceptance: markdown <column> marks a text or varchar column as markdown prose, repeatable per column; the validator checks the column kind, and nullability stays free (absent prose is NULL); the emitter writes the canonical line and the settings model carries the trait; the database renders nothing for it. | Verification: go test ./engine/.... | [o] |
| T61 | Add the IR executor section to the protocol and the markdown capability table to the dialects. Cause: the flowmark plan (docs/plans/markdown-database-plan.md of polyspec/flowmark, the ruling "flowmark is the engine; orm reaches it as an IR executor" and step T12.4) reaches orm clients through a DSN scheme that selects an IR executor instead of a SQL dialect, and the protocol has no such section, no write-conflict error and no capability table for it. | Acceptance: docs/spec/protocol.md has a section by which a DSN scheme selects an IR executor: the client validates the request with ir.Validate and does not plan it, sends {ir_version, manifest_hash, request, params, now} per request with params in the runtime value model JSON, and applies the codec stages itself; a read answers with the assembled rows and a write with {affected, keys}; the control messages begin, commit, rollback, savepoint, release and rollback_to are named; identity, key_prefix and order columns are executor-assigned, so an insert may omit them. docs/spec/errors.yaml has WRITE_CONFLICT (a target changed since it was read; the client never retries it) and states that a lock wait that ends is CANCELED; the generated error constants carry WRITE_CONFLICT. docs/spec/dialects.md has one capability table for the markdown executor with the surfaces of flowmark docs/spec/60-dialect.md. The Korean twins state the same, and make documents-check and make docs-rules-check exit 0. | — | [o] |
| T62 | Add the markdown storage settings to dbspec. Cause: the flowmark plan (the storage grammar and markdown settings of flowmark docs/spec/30-schema.md, and step T12.2, which requires these orm settings) maps each dbspec table onto a markdown corpus through manifest settings, after the precedent of the markdown setting (T60) and of state_machine (T59); the SQL backends ignore them. | Acceptance: store files, store document list|table and store block <fk> list|table parse, and a table with more than one store setting is a setting error; key_prefix <p>, title <column>, body <column>, order <column>, checkbox <column> <state> '<glyph>', state_machine <column> initial <state>, state_machine <column> history <table> row <fk column> from <column> to <column> at <column> and state_machine <column> limit <state> <n> parse; the emitter writes the canonical lines so that emit(parse(s)) == s holds; the validator rejects with a setting error: a state that checkbox, limit or initial names outside the machine's state set; a checkbox set that does not cover the state set exactly with distinct glyphs; a default of the state column that is not an initial state; an initial state that is also terminal; a history table whose columns differ from the required shape (the <fk column> foreign key with its leading index, from and to of the state column's type, at of datetime(6), one nullable column per required column, and no other column); a key_prefix on a column that is not a varchar primary key; an order column that is not a non-null i32 or i64 with no default, or that is in a key, index or check. The settings model carries each setting and the manifest hash covers them. Red-then-green rule tests exist in engine/dbspec, and the dbspec documents and their Korean twins state the rules. A corpus-level rejection (composite key, aes, audit, immutable, cascade or set_null, an external use document, a missing store) belongs to flowmark, not orm, and orm does not reject it. Requires T61. | — | [o] |
| T62-3 | Export the canonical literal function of dbspec. Cause: flowmark (polyspec/flowmark, a markdown database engine that uses engine/dbspec as a library) reads column values in dbspec's canonical literal form and copied the unexported canonicalDefault, and the copy can drift from orm (follows T62). | Acceptance: dbspec exports CanonicalLiteral(t Type, text string) (string, bool), which returns the canonical form of a literal written as a column default and whether the literal is valid for the type, implemented by the same code as canonicalDefault; its test covers every type with valid and invalid literals; the copy in flowmark can be removed. | — | [o] |
| T62-1 | The other dbspec implementations parse the state_machine, markdown and markdown storage settings. Cause: T59, T60 and T62 changed only the Go implementation, so a document set with these settings is a setting error in the TypeScript, PHP, Rust and PHP-extension implementations, while every implementation must read the same documents (docs/spec/dbspec.md "PHP extension" and "Verification"). | Acceptance: each implementation parses, validates (same rules, same rule names and positions) and emits these settings exactly as Go does, proven by shared cases in tests/dbspec/cases.json that every implementation runs; the dbspec check targets of every implementation pass (dbspec-go-check, dbspec-ts-check, dbspec-php-check, dbspec-rust-check, the PHP extension's check, dbspec-compare-check, and the manifest-hash equality checks). | — | [o] |
| T62-2 | The PHP, Rust and PHP-extension dbspec lexers keep a dotted name as one token, as Go does (follow-up of T62-1). Cause: they split a.b into two tokens where Go keeps one. | Acceptance: the shared dotted-name cases pass in every implementation. | — | [o] |
| T62-4 | Non-ASCII characters in names and symbols lex as Go lexes them in every implementation. Cause: Go classifies a rune by unicode.IsLetter and unicode.IsDigit. The Rust lexer (word_char of packages/orm-rust/orm-schema/src/dbspec/lexer.rs) treats every non-ASCII character except whitespace as a word character, and the PHP extension (dotword_ascii and the code point branch of packages/orm-php-extension/src/parse.c) does the same because C has no Unicode letter classification, so a symbol such as € is part of a name there and an illegal character in Go. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's behaviour and all six implementations pass dbspec-compare-check. | Evidence: make dbspec-compare-check exits 0 and prints 12 runs agree (before the fix, its red run failed at dbspec-compare/agreement on the new cases of tests/dbspec/cases.json); make dbspec-go-check, make dbspec-rust-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-ts-check, make python-unit-check and make python-check exit 0; make docs-rules-check and make documents-check exit 0. | [o] |
| T62-5 | The body of a table whose header fails is not validated further, in every implementation, as Go does. Cause: Go marks such a table failed (engine/dbspec/parse.go, the header and brace branches) and engine/dbspec/validate.go registers it and skips its body rules (if t.failed), so the body is parsed for syntax only; no shared case has a malformed table header that reaches a body, so the other implementations are not pinned. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's behaviour and all six implementations pass dbspec-compare-check. | Evidence: the Cause above is wrong for Go, and so is the title: Go validates the body of a table whose header failed in full, because validate.go has no if t.failed skip. A header failure (no name, no {, or words after {, parse.go openTable) sets failed and changes only five checks, each reading the failed table as the table itself or as a target: the repeated-name report of the table (skipped), the referenced columns, count, key and type checks of a foreign key to it (foreignKey; the child's own index check still runs), the checks of an audit history table that failed (audit, which returns) and of an audit record table that failed (auditRecord, which returns). The state_machine history table is checked as before. Shared cases failed-header-* (thirteen, in tests/dbspec/cases.json) pin these with Go's own output, with the body syntax errors Go still reports as syntax. Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 on a column line after a key line, a case that is not about headers; it was removed and is T62-5-1); the runner outputs of the other five implementations then differed from Go on the five cases of the target, duplicate and audit checks. Green: make dbspec-compare-check exits 0 and prints 12 runs agree; go test ./engine/dbspec -run TestSharedVectors/invalid/failed-header passes; make dbspec-go-check, make dbspec-rust-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-ts-check and make python-unit-check exit 0; make interface-check exits 0 after the Rust and TypeScript symbol snapshots were recorded (contracts/symbols/rust.json, contracts/symbols/typescript.json, their hashes in contracts/interfaces.json); make documents-check exits 0; make docs-rules-check exits 2 on one forbidden expression in the T62-4-1 line of the Korean checklist, which HEAD already contains. | [o] |
| T62-4-5 | Every negative owner sample that the feature-unit check runs is asserted, and a passing run prints no unasserted failure. Cause: T62-4-4 found that three sample failures are printed but asserted by no test — the second duplicate observed case first of sample/owner/typescript/none (run by the no executable command call of scripts/features/coverage.test.mjs) and the mysql and postgres lines of the second sqlite-only call — so a change to those samples would pass unnoticed, that every negative sample prints FAIL under a passing run, and that the go/none message reads "observed cases differ" with an empty list. | Acceptance: each printed sample failure is asserted by the test that runs it, the output of a passing run does not present asserted negative samples as failures, the empty-list message names the empty set, and make feature-unit-check exits 0. | Evidence: Red: before the message change, the tightened assertions of scripts/features/coverage.test.mjs failed at make feature-unit-check/coverage (exit 2); the go/none diff showed observed cases differ from TestFirst, and the native and database tests showed the errors and printed FAIL lines that the samples produce. At ab3e779a the same target exited 0 and printed eight FAIL lines (three for sample/owner/typescript/none, two each for typescript/mysql and typescript/postgres, one for go/none). Green: make feature-unit-check/coverage exits 0 with ℹ pass 17 and ℹ fail 0 and prints no FAIL line; make feature-unit-check exits 0 (coverage 17, owners 19 and select 7 passed, none failed); make repo-check, make interface-check, make documents-check and make docs-rules-check exit 0. | [o] |
| T62-4-6 | A CHECK whose string literal holds non-ASCII text round-trips through MySQL in every client. Cause: the plan case rename-column-check-non-ascii-literal of tests/dbspec/plans.json (a rename of users.mail to email in the check ck_users_mail (mail <> 'café')) fails make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check and make dbspec-plan-rust-check on the physical MySQL run: the applied schema reads back as check ck_users_mail (email <> 'café'), the UTF-8 bytes of é decoded as Latin-1 and encoded again (mojibake). The test server (.runtime/servers/env, mysql://root@127.0.0.1:33171/orm_test) reports character_set_server, character_set_client and character_set_connection as utf8mb4, and a plain SELECT 'café' = _utf8mb4'café' returns 1, so the server-side literal is fine; all four clients produce the same wrong text, and where the mojibake enters (the statement sent, or the CHECK_CLAUSE read by each client's MySQL catalog) is not isolated yet. The plan steps agree across the runners (make dbspec-compare-check prints 12 runs agree), so only the physical round trip differs. | Acceptance: a plan case with a non-ASCII CHECK literal round-trips through MySQL in every client, and make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check and make dbspec-plan-rust-check exit 0 with that case in tests/dbspec/plans.json. | Evidence: Injection point: the MySQL server's information_schema.CHECK_CONSTRAINTS.CHECK_CLAUSE, which the introspection of every client reads as stored; the DDL as sent and the connection charset are not the cause. Isolated with the mysql CLI (MySQL 8.4.11, HEX()): SHOW CREATE TABLE of a table whose CHECK is (mail <> 'café') returns 636166c3a9, the UTF-8 bytes of é, but CHECK_CLAUSE of the same constraint returns 636166c383c2a9 (café) over utf8mb4, utf8mb3 and binary connections alike, and the latin1 connection stores its own mojibake at DDL time; two scratch servers with character_set_server utf8mb3 and utf8mb4 show the same catalog text, and COLUMN_DEFAULT of 'café' reads correctly. Red: before the change, with the case added to tests/dbspec/plans.json, make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check and make dbspec-plan-rust-check exit 2 at mysql.plan.rename_column_check_non_ascii_literal with check ck_users_mail (email <> 'café') in the applied schema. Change: the MySQL introspection of engine/dbspec/introspect_mysql.go, packages/orm-php/src/Dbspec/MysqlCatalog.php, packages/orm-php-extension/src/introspect.c, packages/orm-npm/src/dbspec/introspect_mysql.ts, packages/orm-python/src/polyspec/orm/dbspec/introspect_mysql.py and packages/orm-rust/orm-schema/src/dbspec/introspect/mysql.rs reads the body of a CHECK whose CHECK_CLAUSE holds a byte of 0x80 or more from SHOW CREATE TABLE (once per table) and escapes it as CHECK_CLAUSE does, so the existing decoder is unchanged; the Rust driver (packages/orm-rust/orm/src/dbspec.rs, utils.rs, with catalog_followups of packages/orm-rust/orm-schema/src/dbspec/introspect/mod.rs) runs those follow-up queries after the catalog queries, and the trait CatalogQuerier takes &str (its test implementations changed too). The symbol snapshots contracts/symbols/{go,php,rust,typescript}.json were re-recorded with go run ./tests/interfaces/check --record --self-test, and their hashes in contracts/interfaces.json updated. docs/spec/dialects.md and docs/dialects.ko.md name the behaviour. Green: make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check, make dbspec-plan-rust-check, make dbspec-php-extension-check, make dbspec-introspect-check, make dbspec-introspect-php-check, make dbspec-introspect-ts-check, make dbspec-introspect-rust-check, make dbspec-introspect-php-extension-check, make dbspec-introspect-compare-check, make dbspec-apply-check, make dbspec-apply-php-check, make dbspec-apply-ts-check, make dbspec-apply-rust-check, make dbspec-apply-php-extension-check, make dbspec-ddl-check, make dbspec-compare-check (12 runs agree), make python-unit-check, make interface-check, make feature-unit-check, make rust-fmt-check/clients and make repo-check each exit 0; go test ./engine/dbspec -count=1 passes and its TestPlanVectors/rename-column-check-non-ascii-literal passes the vector. The Go, PHP and TypeScript parser helpers have no unit case of their own; the physical round trip of the case covers their path. | [o] |
| T62-4-7 | A generated column whose expression holds a non-ASCII string literal round-trips through MySQL in every client, or dbspec states that it cannot. Cause: T62-4-6 observed with the mysql CLI (MySQL 8.4.11) that information_schema.COLUMNS.GENERATION_EXPRESSION encodes the UTF-8 bytes of a non-ASCII literal a second time, as CHECK_CLAUSE does. The MySQL introspection of every client reads that column (engine/dbspec/introspect_mysql.go, GENERATION_EXPRESSION), and no plan case covers it. No client had run it when the item was registered. | Acceptance: either a plan case with such a generated column round-trips through MySQL in every client, with make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check and make dbspec-plan-rust-check exiting 0, or the dbspec grammar does not produce such an expression and docs/spec/dbspec.md says so with its grounding. | Evidence: Branch: the second, because dbspec cannot produce a generated column. The column line takes only a name, a type, null, identity and default (engine/dbspec/parse.go, the reads of null, identity and default at lines 841-842), and the renderers of Go, PHP, the PHP extension, Rust, TypeScript and Python write no generated column: grep of STORED, VIRTUAL and AS ( over those renderers finds no column DDL, and the only GENERATED they write is PostgreSQL's GENERATED BY DEFAULT AS IDENTITY. Why introspection reads GENERATION_EXPRESSION: it reports a column that a MySQL database defines with a generation expression as unsupported, because dbspec cannot represent it. A non-empty value makes every client report the column before it reads the type or the text, with the reason type <COLUMN_TYPE> <charset> <collation> has no dbspec type: engine/dbspec/introspect_mysql.go (generation != ""), packages/orm-php/src/Dbspec/MysqlCatalog.php:66, packages/orm-npm/src/dbspec/introspect_mysql.ts:58, packages/orm-python/src/polyspec/orm/dbspec/introspect_mysql.py:74, packages/orm-rust/orm-schema/src/dbspec/introspect/mysql.rs:79 and packages/orm-php-extension/src/introspect.c:1650. Observed on the test server of .runtime/servers/env (MySQL 8.4.11), in the scratch database orm_scratch_t62_4_7 (dropped afterwards), table greet with label varchar(128) AS (concat(name, 'café')) STORED: GENERATION_EXPRESSION is concat(`name`,_utf8mb4\'café\') (hex c383c2a9), and SHOW CREATE TABLE shows 'café'. Each client introspected that database through its public API with the same result, one table and the unsupported column greet.label with the reason type varchar(128) utf8mb4 utf8mb4_0900_bin has no dbspec type: Go dbspec.Introspect (go run of a scratch module outside the checkout that replaces github.com/polyspec/orm with it), TypeScript introspectDbspec of the built packages/orm-npm/dist (node), Python introspect_dbspec of packages/orm-python/src with PyMySQL, PHP Dbspec::introspect, PHP extension Native\Dbspec::introspect with orm_dbspec.so built by packages/orm-php-extension/scripts/build.sh into the scratchpad, and Rust examples/dbspec_introspect with mysql, which prints ! column<TAB>greet<TAB>label (the example prints no reason; the Rust reason has the same format string). Every run exited 0. Red: none, because no client decided differently or echoed the expression text, so no reader and no plan case changed. Change: docs/spec/dbspec.md and docs/dbspec.ko.md state that dbspec produces no generated column, with the grammar and the introspection as grounding; docs/spec/dialects.md and docs/dialects.ko.md record that no client reads the expression text; CHANGELOG.md and CHANGELOG.ko.md name the item; the ko source-sha256 markers of the changed twins are re-stamped. Green: make documents-check 0, make docs-rules-check 0, make version-check 0, make dbspec-compare-check 0 (12 runs agree), make interface-check 0 and make repo-check 0; the dbspec introspect targets were not run, since no reader changed. | [o] |
| T62-4-8 | The SHOW CREATE TABLE check readers of T62-4-6 have unit cases of their own in every client. Cause: T62-4-6 added a reader to each client that finds a CHECK body in SHOW CREATE TABLE by skipping string literals and matching parentheses, then escapes it as CHECK_CLAUSE does (mysqlShownCheck and escapeMySQLClause in engine/dbspec/introspect_mysql.go, and their counterparts in PHP, the PHP extension, TypeScript, Python and Rust). The only case is one physical round trip of 'café'. A literal holding ', \ or ), more than one non-ASCII check in one table, and a name with no CHECK in the statement are not exercised. | Acceptance: a shared set of SHOW CREATE TABLE texts and their expected CHECK_CLAUSE-form bodies is read by each client's reader in its unit check, and every client gives the same result. | Evidence: Shared vector tests/dbspec/show-create.json with nine cases: a non-ASCII literal, a literal with ', a literal with \, a literal with ( and ), two CHECKs in one table with the target first and second, a literal ending in \, a non-ASCII literal with ', and a name the statement lacks. Each create text is the output of SHOW CREATE TABLE on MySQL 8.4.11 (the test server of .runtime/servers/env), byte for byte. Observed quoting: a literal's ' is written \', its \ is written \\, the introducer _utf8mb4 stays, and the CHECK body is wrapped as CHECK ((...)). The expected body is the SHOW CREATE body with \ and ' escaped once more; for the ASCII cases it equals the CHECK_CLAUSE that MySQL stores (checked with HEX(CHECK_CLAUSE)), and for the non-ASCII cases it equals that clause with the second encoding undone. Readers: Go mysqlShownCheck (engine/dbspec/show_create_test.go, TestMySQLShownCheckVectors, target dbspec-go-check/engine), PHP MysqlCatalog::createChecks (packages/orm-php/tests/dbspec_show_create_test.php, target dbspec-php-check/show-create), TypeScript createChecks, exported for the test (packages/orm-npm/tests/dbspec-show-create.mjs, target dbspec-ts-check), Python _create_checks (packages/orm-python/tests/test_dbspec_show_create.py, target python-unit-check) and Rust shown_checks (module show_create_tests of packages/orm-rust/orm-schema/src/dbspec/introspect/mysql.rs, target dbspec-rust-check/show-create). The symbol snapshot contracts/symbols/typescript.json and its hash in contracts/interfaces.json were re-recorded for the export. Red: no reader disagreed with an expected body on the first run, so no reader changed. To show that the vectors can fail, escapeMySQLClause was broken to skip the \ escape; go test ./engine/dbspec -run TestMySQLShownCheckVectors then failed the five cases that hold a \ and exited 1, and the escape was restored. Green: make dbspec-go-check/engine, make python-unit-check, make dbspec-php-check/show-create, make dbspec-ts-check, make dbspec-rust-check/show-create, make dbspec-compare-check (12 runs agree), make interface-check (after the re-record), make rust-fmt-check/clients each exit 0. Extension: shown_check of packages/orm-php-extension/src/introspect.c is reached through Native\Dbspec::introspect over a PDO subclass, which dispatches query, errorCode, errorInfo and fetchAll by method name (pdo_call, zend_call_method), so a double can answer the catalog. packages/orm-php-extension/tests/dbspec_show_create_test.php (target dbspec-php-extension-check/show-create) answers the ten catalog queries of mysql_read from the catalog of tests/dbspec/show-create.json and each case, with rows of an in-memory SQLite statement, so shown_check reads each SHOW CREATE TABLE text. engine/dbspec/show_create_introspect_test.go (TestMySQLIntrospectionOfShownChecks, target dbspec-go-check/engine) runs Go's Introspect over the same answers through a fake Querier. Both compare the check lines of the introspected table and the unsupported objects (kind, name and rule) with the expectation of the case; a missing CHECK expects the error check t.ck_missing has no CHECK in SHOW CREATE TABLE from both. The first extension run found that the reason texts differ (backticks in C, quotes in Go), so reasons are compared by rule, as tests/dbspec/introspect.json does. The non-ASCII check name of the first case is unsupported in both (name.format), so its body is read but not visible in the document. Red (extension): the backslash escape of escape_mysql_clause in introspect.c was removed; make dbspec-php-extension-check/show-create then failed literal-with-backslash, two-checks-target-non-ascii and literal-ends-with-backslash and exited 2; the line was restored, and git diff -- packages/orm-php-extension/src/introspect.c is empty. Green: make dbspec-php-extension-check exits 0 with the nine cases run twice. | [o] |
| T62-4-9 | Introspection reports a generated column as generated, in every client. Cause: T62-4-7 observed that a MySQL column with a generation expression is reported as unsupported with the reason "type varchar(128) utf8mb4 utf8mb4_0900_bin has no dbspec type" (Go introspect_mysql.go, MysqlCatalog.php:66, introspect_mysql.ts:58, introspect_mysql.py:74, mysql.rs:79, introspect.c:1650). varchar(128) is a dbspec type, so the reason names the wrong cause; the column is unsupported because it is generated. | Acceptance: every client reports such a column with a reason that names the generation expression as the cause, the MySQL and PostgreSQL introspection cases pin it, and make dbspec-introspect-compare-check exits 0. | Evidence: Correction: before this item no shared case compared reasons. tests/dbspec/introspect.json compared only [kind, table, name], so the sentence of T62-4-8 "reasons are compared by rule, as tests/dbspec/introspect.json does" was untrue when it was written; T62-4-8 is terminal and is not edited. Shared change: an unsupported entry may carry a fourth element, a rule that the reason must contain; the Go (tests/dialects/introspect_physical_test.go), PHP (packages/orm-php/tests/dbspec_introspect_test.php), PHP extension (packages/orm-php-extension/tests/dbspec_introspect_test.php), TypeScript (packages/orm-npm/tests/dbspec-introspect.mjs) and Rust (packages/orm-rust/orm/tests/dbspec_introspect.rs) harnesses check it. Cases: mysql-generated-column (CREATE TABLE gen with label varchar(128) AS (concat(n, 'x')) STORED) and postgres-generated-column (label varchar(128) GENERATED ALWAYS AS ("n"::text || 'x') STORED), each expecting ["column", "gen", "label", "a generated column"]. Red: before the reader change, make dbspec-introspect-check exit 2 at TestIntrospectUnsupported/mysql.introspect.mysql-generated-column with reason of column gen label differs; want rule "a generated column", got type varchar(128) utf8mb4 utf8mb4_0900_ai_ci has no dbspec type, and the PostgreSQL case with got type character varying(128) with collation "default" has no dbspec type; make dbspec-introspect-php-check exit 2 (introspect/mysql/mysql-generated-column); make dbspec-introspect-ts-check exit 2 (mysql.introspect.mysql-generated-column, postgres.introspect.postgres-generated-column); make dbspec-introspect-rust-check exit 2 (introspect_unsupported, 2 failures); make dbspec-introspect-php-extension-check exit 2 (introspect/mysql/mysql-generated-column). A first fixture with name varchar(64) failed on every client with document differs, because a varchar column needs its renderer CHECK, so the reader correctly dropped it; the case was changed to a bigint column. Change: the MySQL and PostgreSQL readers check the generation before the type and report a generated column has no dbspec definition (the wording of the SQLite reader): engine/dbspec/introspect_mysql.go, engine/dbspec/introspect_postgres.go, packages/orm-php/src/Dbspec/MysqlCatalog.php, packages/orm-php/src/Dbspec/PostgresCatalog.php, packages/orm-npm/src/dbspec/introspect_mysql.ts, packages/orm-npm/src/dbspec/introspect_postgres.ts, packages/orm-php-extension/src/introspect.c (both readers), packages/orm-rust/orm-schema/src/dbspec/introspect/mysql.rs and postgres.rs. Docs: docs/spec/dialects.md and its ko twin say the rule element is compared when present; the note of tests/dbspec/introspect.json says so too. Green: make dbspec-introspect-check exit 0, make dbspec-introspect-php-check exit 0, make dbspec-introspect-ts-check exit 0, make dbspec-introspect-rust-check exit 0, make dbspec-introspect-php-extension-check exit 0, make dbspec-introspect-compare-check exit 0. Also exit 0: make python-unit-check (123 tests), make dbspec-compare-check (12 runs agree), make interface-check, make rust-fmt-check/clients, gofmt -l on the three Go files (no output). Python: packages/orm-python/tests/dbspec_introspect.py runs the 13 shared cases (5 MySQL, 3 PostgreSQL, 5 SQLite) through polyspec.orm.dbspec.introspect.introspect_dbspec with PyMySQL and psycopg of the venv, against the servers of TEST_ENV, creating and dropping a database or schema per case; make dbspec-introspect-python-check runs it. packages/orm-python/tests/dbspec_introspect_runner.py is the Python runner of make dbspec-introspect-compare-check, which now runs five runners. It sits under packages/orm-python/, the one prefix where scripts/repo/scripts.mjs allows a Python tool; a first copy under tests/dbspec/introspect/ was rejected by the repo-check language rule and moved. Red: with the Python generated-column change reverted, make dbspec-introspect-python-check exit 2 with 2 of 13 cases differ: mysql-generated-column: reason of ['column', 'gen', 'label'] differs: want rule 'a generated column', got 'type varchar(128) utf8mb4 utf8mb4_0900_ai_ci has no dbspec type' and postgres-generated-column: ... got "type character varying(128) with collation 'default' has no dbspec type". Green: the same target exit 0 with the change restored (introspect: 13 cases agree (5 mysql, 3 postgres, 5 sqlite)), and make dbspec-introspect-compare-check exit 0 with the Python runner (24849 bytes in each dialect, equal to the Go output). | [o] |
| T62-4-10 | Every test file and fixture that T62-4-8 added belongs to a feature and is an input of the check that runs it. Cause: T62-4-8 added engine/dbspec/show_create_test.go, engine/dbspec/show_create_introspect_test.go, packages/orm-php/tests/dbspec_show_create_test.php, packages/orm-php-extension/tests/dbspec_show_create_test.php, packages/orm-npm/tests/dbspec-show-create.mjs and tests/dbspec/show-create.json without listing them in contracts/features.json, and T62-4-9 was committed on that tree. The CI run of that tree failed six jobs: features/contracts (scripts/features/check.mjs) reported four errors <file>: <language> test file belongs to no feature for the two Go files, the PHP file and the TypeScript file in feature-stress-mysql-check, feature-stress-pg-sqlite-check, feature-verify-other, feature-helper-check and feature-verify-rust, and the test every tracked file selects a behaviour test or declares its scope of feature-unit-check found the six files selected by no check. | Acceptance: the six files are declared as their siblings of schema_definition are, make feature-unit-check and the features/contracts check exit 0, and no check is weakened. | Evidence: contracts/features.json, feature schema_definition: tests/dbspec/show-create.json joins fixtures beside tests/dbspec/mermaid.json; the five test files join tests; verification inputs: schema-go (make dbspec-go-check) gains the two Go files and the fixture, schema-php (make dbspec-php-check) the PHP test and the fixture, schema-typescript (make dbspec-ts-check) the TypeScript test and the fixture, schema-php-extension (make dbspec-php-extension-check) the extension test and the fixture, and schema-rust (make dbspec-rust-check, whose dbspec-rust-check/show-create reads it through include_str!) the fixture. contracts/check-inputs.json: python-check, whose python-unit-check runs packages/orm-python/tests/test_dbspec_show_create.py, gains the fixture; the Python test itself was already an input through packages/orm-python/**. Red: before the change make feature-unit-check exit 2, with the six files as the actual value of the owners test, and make feature-stress-mysql-check exit 2 at features/contracts with the same four errors as CI. Green: make feature-unit-check exit 0 (17, 19 and 7 tests pass), node scripts/features/check.mjs exit 0 (18 feature contracts passed), and make feature-stress-mysql-check passes features/contracts. Its helper dbspec-apply-stress-mysql then exits 2 on macOS, because its command passes TEST_ENV="$ORM_OWNER_TEST_ENV", which only make feature-owner-check sets, and the macOS sh refuses . without a file name; a passing CI run of an earlier tree ran the helper with the same empty value on Linux, so this is not part of this regression. The declared commands pass: make dbspec-go-check (TestMySQLShownCheckVectors and TestMySQLIntrospectionOfShownChecks pass), make dbspec-php-check/show-create, make dbspec-php-extension-check/show-create, make dbspec-rust-check/show-create, make dbspec-ts-check (438 tests) and make python-unit-check (123 tests) each exit 0. Also exit 0: make repo-check, make documents-check, make docs-rules-check. | [o] |
| T62-5-1 | A column line with a syntax error after a key, index, foreign key or check line is reported as Go reports it, in the other implementations. Cause: Go reports order on a column line only after the line parses (engine/dbspec/parse.go, columnLine after c.done()), so a column line with a syntax error gets syntax alone; after a primary key line, name ending the line is reported at the end of the line. PHP, TypeScript, Python, Rust and the PHP extension report order at the first token (6 3) before they parse the line, so such a line gets order and then syntax. The red run of T62-5 showed it after a primary key line (php 1 differs from go 1). A valid column line after a key line already gets order alone in all six implementations, so the defect is only in the order check of a line with a syntax error. The shared cases use a valid header; a malformed header was not run. The case was removed from T62-5, which is about headers. | Acceptance: a shared case in tests/dbspec/cases.json pins Go's output for it, and all six implementations pass dbspec-compare-check. | Evidence: Red: before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 in invalid/column-after-primary-key-syntax at line 1025; go 1: ! syntax 6 7, php 1: ! order 6 3). Running each runner on the new cases showed that php, typescript, python, rust and php-extension print ! order at the first token before ! syntax in the four syntax-error cases (primary key, index, foreign key and check lines), and agree with Go on the four cases whose column line is valid (! order alone). go test ./engine/dbspec -run TestSharedVectors/invalid/column-after -count=1 passes eight cases with Go's output. Change: each of the five parsers reports order after the column line parses and skips it when the line has a syntax error or the parse stopped at a limit: packages/orm-php/src/Dbspec/Parser.php (tableLine), packages/orm-npm/src/dbspec/parse.ts, packages/orm-python/src/polyspec/orm/dbspec/parse.py, packages/orm-rust/orm-schema/src/dbspec/parser.rs (the new column_line_in_order) and packages/orm-php-extension/src/parse.c. Green: make dbspec-compare-check exits 0 and prints 12 runs agree. make dbspec-go-check, make dbspec-rust-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-ts-check and make python-unit-check exit 0. make interface-check exits 2 after the Rust parser gained the private method column_line_in_order; the Rust symbol snapshot was re-recorded (contracts/symbols/rust.json, one line added) and its hash in contracts/interfaces.json updated, after which make interface-check exits 0. make feature-unit-check exits 0. rustfmt --edition 2021 --check exits 0 on parser.rs. make documents-check and make docs-rules-check exit 0. | [o] |
| T62-5-2 | A foreign key's own index check runs as Go's does when a referenced column is unknown or the primary key line failed. Cause: found by reading the code, not run: Go's leading-index check reads only the child's columns and the failed primary and key lines (validate.go foreignKey), but TypeScript and Python return before that check when a referenced column is unknown, and Python ignores a failed primary key line in it. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output for both, and all six implementations pass dbspec-compare-check. | Evidence: the Cause is correct for TypeScript and Python. Go runs the index check after the referenced column checks and reports in source order, so an index error at the foreign key name comes before an unknown referenced column later on the line, and an index error comes before a set_null error at the same name. Go skips the index check only for a failed child primary or key line and for an unknown child column; an unknown referenced column does not skip it. Shared cases foreign-key-unknown-reference-without-index, foreign-key-unknown-reference-with-index, foreign-key-unknown-reference-set-null, failed-primary-key-under-foreign-key and failed-key-under-foreign-key (five, in tests/dbspec/cases.json) pin Go's output; go test ./engine/dbspec -run TestSharedVectors passes. Red: before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement (typescript 1 differs from go 1 in invalid/foreign-key-unknown-reference-without-index at line 1039; go 1: ! foreign_key 12 15, typescript 1: ! foreign_key 12 57). The check stops at the first difference of each runner, so every runner's output was saved and compared: typescript and python lack the index error in the without-index case and the set_null error in the set-null case, python also prints ! foreign_key 12 15 after ! syntax 11 18 in the failed-primary-key case, and php, rust and php-extension agree with Go on all five. Change: packages/orm-npm/src/dbspec/parse.ts (foreignKey) and packages/orm-python/src/polyspec/orm/dbspec/parse.py (_foreign_key) keep the return for an unknown child column; for an unknown referenced column they run the index and set_null checks (foreignKeyKeyChecks, _foreign_key_keys) and return, skipping only the pairing and type checks. Python's _foreign_key_keys also skips the index check after a failed primary key line, as Go and TypeScript do. Green: make dbspec-compare-check exits 0 and prints 12 runs agree; go test ./engine/dbspec -run TestSharedVectors exits 0; make dbspec-go-check, make dbspec-rust-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-ts-check, make python-unit-check and make feature-unit-check exit 0; make interface-check exits 0 without re-recording a snapshot, because no declaration changed; make repo-check exits 0. Two further divergences that the probes reproduced are registered as T62-5-4 and T62-5-5. | [o] |
| T62-5-3 | make rust-fmt-check/clients passes over the Rust dbspec sources. Cause: T62-5 committed packages/orm-rust/orm-schema/src/dbspec/parser.rs (the Ok(Parsed { … }) expression) and dbspec/mod.rs without running rustfmt, so the format check fails on lines T62-5 wrote. | Acceptance: the sources are formatted by rustfmt with the repository's configuration and make rust-fmt-check/clients exits 0. | Evidence: Red: before the change make rust-fmt-check/clients exits 2 (cargo fmt exits 1), and cargo fmt --all --check -- -l in packages/orm-rust lists exactly orm-schema/src/dbspec/mod.rs (the validate::validate(...) call) and orm-schema/src/dbspec/parser.rs (the Ok(Parsed { … }) expression). Change: rustfmt --edition 2021 (the edition of orm-schema/Cargo.toml; the configuration is packages/orm-rust/rustfmt.toml, which rustfmt finds from each file) in write mode on those two files only; git diff --stat lists only them, 18 insertions and 2 deletions. Green: make rust-fmt-check/clients exits 0 and prints PASS rust-fmt; make dbspec-rust-check exits 0; make dbspec-compare-check exits 0 and prints 12 runs agree; make interface-check exits 0 without re-recording a snapshot, because the change alters no declaration (rust: 2830 native symbols inspected). | [o] |
| T62-5-4 | A foreign key's own index check and set_null check run as Go's do when the referenced table is not defined or the referenced columns differ in number from the foreign key columns, in TypeScript and Python. Cause: found by running probes, not by reading: a child table with no index on team_id and foreign key fk_users_team (team_id) references nowhere (id) gets Go's two errors, table "nowhere" is not a table of this document or a used table and the missing index at the foreign key name, while TypeScript and Python report only the first; foreign key fk_users_team (team_id, other) references teams (id) gets Go's count error and the missing index, and TypeScript and Python report only the count. The early returns for an undefined target (packages/orm-npm/src/dbspec/parse.ts foreignKey, _foreign_key in packages/orm-python/src/polyspec/orm/dbspec/parse.py) and for a count mismatch skip foreignKeyKeyChecks. PHP, Rust and the PHP extension match Go on both. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output for both documents, and all six implementations pass dbspec-compare-check. | Evidence: Red: tests/dbspec/cases.json gained five shared cases, foreign-key-undefined-table-without-index, foreign-key-undefined-table-with-index (a control whose index is present), foreign-key-undefined-table-set-null, foreign-key-column-count-without-index and foreign-key-column-count-set-null. go test ./engine/dbspec -run TestSharedVectors passes with their errors, which were copied from Go's diagnostics: the undefined table reports its name at the target, and the index and set_null errors come at the foreign key name, in source order. Before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement (typescript 1 differs from go 1 in invalid/foreign-key-undefined-table-without-index at line 1052). Each runner was run on the cases and its output compared with Go's: typescript and python omit the index error and the set_null error in the four cases that the early returns skip, while php, rust and php-extension agree with Go on all five. Change: packages/orm-npm/src/dbspec/parse.ts (foreignKey) and packages/orm-python/src/polyspec/orm/dbspec/parse.py (_foreign_key) run foreignKeyKeyChecks and _foreign_key_keys for an undefined target and for a count mismatch, and skip only the pairing and type checks, as Go's target == nil and len(f.columns) != len(f.references) branches do. Reading Go showed that a malformed target name also yields a nil target in v.ref, so the same early return skipped the key checks there; foreign-key-malformed-target-without-index pins it (Go: the index error at the name and name.format at the target), and the TypeScript build made before the change differed on it. The Python parser had the same early return and was not probed before its change. Green: make dbspec-compare-check exits 0 and prints 12 runs agree; go test ./engine/dbspec -run TestSharedVectors exits 0; make dbspec-go-check, make dbspec-ts-check, make python-unit-check, make dbspec-php-check, make dbspec-rust-check, make dbspec-php-extension-check, make feature-unit-check and make repo-check exit 0; make interface-check exits 0 without re-recording a snapshot, because no declaration changed. The runner probe after the change shows no differing case in any runner. make documents-stamp, make documents-check and make docs-rules-check exit 0; docs-rules passes 39 English documents and 39 Korean translations. | [o] |
| T62-5-5 | A foreign key with an unknown child column runs its set_null check and its index check as Go does, in Rust, TypeScript, Python, PHP and the PHP extension. Cause: found by running probes, not by reading: for foreign key fk_users_team (nope, team_id) references teams (id, code) on delete set_null with team_id not null, Go reports the set_null error and the unknown column; TypeScript, Python, PHP and the PHP extension report only the unknown column, since they return when a child column is unknown. Rust (packages/orm-rust/orm-schema/src/dbspec/validate.rs, foreign_key) also reports an index error at the foreign key name, since its index check does not skip an unknown child column, and Go skips that check when a child column is unknown (known in validate.go). For foreign key fk_users_team (nope) references teams (id) on a child with no key, Go reports only the unknown column and Rust also reports the missing index. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output for both documents, and all six implementations pass dbspec-compare-check. | Evidence: the Cause is correct; reading Go's foreignKey (validate.go, known) confirms it. Red: before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement with php 1 differs from go 1 in invalid/foreign-key-unknown-child-set-null at line 1071. The check stops at the first difference of each runner, so every runner's output was saved and compared with Go's: php, typescript, python and php-extension omit ! foreign_key 14 15 (the set_null error) in that case; rust adds ! foreign_key 14 15 in that case and in foreign-key-unknown-child-set-null-nullable (its index error), and ! foreign_key 12 15 in foreign-key-unknown-child-without-index. Shared cases foreign-key-unknown-child-set-null, foreign-key-unknown-child-set-null-nullable and foreign-key-unknown-child-without-index in tests/dbspec/cases.json take Go's output as expectation, and go test ./engine/dbspec -run TestSharedVectors -count=1 exits 0. Change: packages/orm-rust/orm-schema/src/dbspec/validate.rs runs a foreign key's index check only when every child column is known and not repeated, as Go does; packages/orm-npm/src/dbspec/parse.ts and packages/orm-python/src/polyspec/orm/dbspec/parse.py run the key checks for an unknown child column and skip only the index check (the set_null check runs over the known child columns); packages/orm-php/src/Dbspec/Parser.php and packages/orm-php-extension/src/parse.c do the same with a children-known flag. The Rust rule test packages/orm-rust/orm-schema/tests/dbspec_rules.rs (foreign-key-rules) no longer expects the index error at fk_e (x), which pinned the old Rust behavior. The TypeScript private method foreignKeyKeyChecks gained a parameter, so the typescript snapshot is re-recorded: contracts/symbols/typescript.json and its hash in contracts/interfaces.json (no other snapshot changed). Green: make dbspec-compare-check exits 0 and prints 12 runs agree; make dbspec-go-check, make dbspec-rust-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-ts-check, make python-unit-check, make interface-check, make feature-unit-check and make repo-check exit 0. A probe reproduced one further divergence, registered as T62-5-6. | [o] |
| T62-5-6 | A foreign key with an unknown child column checks its referenced columns against the target's keys and types as Go does, in TypeScript, Python, Rust, PHP and the PHP extension. Cause: found by running a probe, not by reading: for foreign key fk_users_team (nope, team_id) references teams (id, code) where teams has primary key (id), Go reports the referenced columns are not the primary key or a unique key of 'teams' at the foreign key name, and the other five implementations skip that check. Their pairing check returns early when a child column is unknown (TypeScript and Python on resolved, Rust on complete, PHP and the PHP extension through $known and known), while Go runs the key check on the referenced columns whatever the child columns are, and the type check on the known child columns. | Acceptance: a shared case in tests/dbspec/cases.json pins Go's output, and all six implementations pass dbspec-compare-check. | Evidence: the Cause is correct; Go's foreignKey (validate.go) runs the count check, the referenced-key check and the type check whenever the referenced columns are known, and it checks the types of the known child columns only, while the index check needs every child column known. Red: tests/dbspec/cases.json gained foreign-key-unknown-child-referenced-key, foreign-key-unknown-child-referenced-type and the control foreign-key-unknown-child-unknown-reference, whose expectations are Go's output; go test ./engine/dbspec -run TestSharedVectors -count=1 passes. Before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 in invalid/foreign-key-unknown-child-referenced-key at line 1078). The full output of every runner was saved and compared with Go's: php, typescript, python, rust and php-extension omit ! foreign_key 14 15 (the referenced-key error) in the first case, and in the referenced-type case they omit that error and the type error at the same position; the control agrees in every runner. Change: packages/orm-npm/src/dbspec/parse.ts (foreignKey and foreignKeyKeyChecks) and packages/orm-python/src/polyspec/orm/dbspec/parse.py (_foreign_key and _foreign_key_keys) keep a placeholder for an unknown child, run the count check whatever the children are, run the pairing and type checks when the referenced columns are known, skip the type check of an unknown child and stop the type check at its first mismatch, as Go does; packages/orm-rust/orm-schema/src/dbspec/validate.rs (foreign_key) does the same with an Option per child and per reference, so the columns stay paired; packages/orm-php/src/Dbspec/Parser.php (checkForeignKeyTargets) and packages/orm-php-extension/src/parse.c (check_foreign_key_targets) make the pairing check depend on the referenced columns only and skip an unknown or invalid-typed child in the type check; the unused known field of the C deferred record is removed. Interface: the private TypeScript method foreignKeyKeyChecks has a new parameter type, so contracts/symbols/typescript.json was re-recorded with node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language typescript, its hash in contracts/interfaces.json was updated, and no other snapshot changed. Green: make dbspec-compare-check prints 12 runs agree; make dbspec-go-check, make dbspec-rust-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-ts-check (pass 405, fail 0), make python-unit-check (Ran 122 tests, OK), make interface-check, make feature-unit-check and make repo-check exit 0. Six further shapes were probed against every runner and Go: PHP, TypeScript, Python and the PHP extension match Go on all of them; Rust differs in two, registered as T62-5-7 and T62-5-8. | [o] |
| T62-5-7 | A foreign key whose target table is not defined is not checked for its column count in Rust, as Go does. Cause: found by a probe after T62-5-6, not by reading: for foreign key fk_users_team (team_id, id) references nowhere (id) on a child that has no index leading with both columns, Go reports the index error and the unknown table, but no count error, because its foreignKey runs the count check only when the target is not nil (target != nil && !target.failed in validate.go); Rust (validate.rs, foreign_key, the count check guarded only by !failed_target) also reports the foreign key lists a different number of columns and referenced columns at the foreign key name. | Acceptance: a shared case in tests/dbspec/cases.json pins Go's output for this document, and all six implementations pass dbspec-compare-check. | Evidence: the Cause is correct. Go's foreignKey (validate.go) runs the count check only when target != nil && !target.failed, while Rust's foreign_key skipped it only for a target whose header failed, so an undefined target also got the count error. Red: tests/dbspec/cases.json gained foreign-key-unknown-table-count, whose expectation is Go's output (the index error foreign_key 12 15 and the unknown table error foreign_key 12 54); go test ./engine/dbspec -run TestSharedVectors -count=1 passes. Before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement with rust 1 differs from go 1 in invalid/foreign-key-unknown-table-count at line 1086 (Go ! foreign_key 12 54, Rust ! foreign_key 12 15). Change: packages/orm-rust/orm-schema/src/dbspec/validate.rs (foreign_key) runs the count check only when the target is defined and not failed. Green: make dbspec-compare-check, make dbspec-rust-check, make rust-fmt-check/clients, make rust-check/clippy, make interface-check, make feature-unit-check and make repo-check exit 0. No declaration changed, so no snapshot was re-recorded. | [o] |
| T62-5-8 | A foreign key whose child column has an invalid type runs its index check in Rust, as Go does. Cause: found by a probe after T62-5-6, not by reading: for team_id nosuch (an unknown type, a syntax error on its column line) with no index leading with team_id and foreign key fk_users_team (team_id) references teams (id), Go reports the index error at the foreign key name, because its known flag depends on the presence of the child column and not on its type; Rust (validate.rs, foreign_key) reports nothing, since a column whose line failed is Lookup::Unresolved there and makes its children-known flag false. PHP, the PHP extension, TypeScript and Python report it as Go does. | Acceptance: a shared case in tests/dbspec/cases.json pins Go's output for this document, and all six implementations pass dbspec-compare-check. | Evidence: the Cause is correct. Go's columnRef finds a column whose line parsed through its type word and whose type is invalid, since Go appends the column after resolveType fails, so its children-known flag stays true. Rust's column() returned None for such a line, which marked the column unresolved. Red: tests/dbspec/cases.json gained foreign-key-invalid-child-type-index, whose expectation is Go's output (the type error at 10:11 and the index error foreign_key 12 15); go test ./engine/dbspec -run TestSharedVectors -count=1 passes. Before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement with rust 1 differs from go 1 in invalid/foreign-key-invalid-child-type-index at line 1089 (Go ! foreign_key 12 15, Rust no error for that case). Change: packages/orm-rust/orm-schema/src/dbspec/parser.rs records the names of column lines that parse with an invalid type (typeless and table_typeless), and packages/orm-rust/orm-schema/src/dbspec/validate.rs (foreign_key) keeps such a child known with no type, so the index check runs and the type check skips it. Green: make dbspec-compare-check, make dbspec-rust-check, make rust-fmt-check/clients, make rust-check/clippy, make interface-check, make feature-unit-check and make repo-check exit 0. Interface: the private Rust structs Parsed, Parser, Scope and TableScope gained fields and TableScope gained a method, so contracts/symbols/rust.json was re-recorded through the make environment (node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language rust), and its hash in contracts/interfaces.json was updated; no public declaration changed. A probe after the change found a further divergence: a set_null foreign key whose child has an invalid type is not checked for null by Rust (Go reports ! foreign_key 13 15, set_null requires every foreign key column to be null), registered as T62-5-9. | [o] |
| T62-5-9 | A foreign key with a set_null action whose child column has an invalid type checks its columns for null in Rust, as Go does. Cause: found by a probe after T62-5-8, not by reading: for team_id nosuch (an invalid type) with index ix_users_team (team_id) and foreign key fk_users_team (team_id) references teams (id) on delete set_null, Go reports set_null requires every foreign key column to be null at the foreign key name, because its set_null check reads every child column that exists (c != nil && c.null == nil); Rust's set_null check (validate.rs, foreign_key) reads only the children that resolved to a typed column, so it reports nothing. | Acceptance: a shared case in tests/dbspec/cases.json pins Go's output for this document, and all six implementations pass dbspec-compare-check. | Evidence: the Cause is correct. Go's foreignKey (validate.go) runs the set_null check over every child column that exists (c != nil && c.null == nil), and columnRef returns a column whose type is invalid, so a typeless child that is not null is an error. Rust's set_null check read only the children that resolved to a typed column. Red: tests/dbspec/cases.json gained foreign-key-invalid-child-set-null, whose expectation is Go's output (the type error at 10:11 and foreign_key 13 15, set_null requires every column to be null); go test ./engine/dbspec -run TestSharedVectors -count=1 passes. Before the change make dbspec-compare-check exits 2 at dbspec-compare/agreement with rust 1 differs from go 1 in invalid/foreign-key-invalid-child-set-null at line 1092 (Go ! foreign_key 13 15, Rust no error for the case). The other four runners were run on the same inputs (PHP, TypeScript, Python and the PHP extension), and each printed the same block for the case as Go, so only Rust differed. Change: packages/orm-rust/orm-schema/src/dbspec/parser.rs records each typeless column's name with its null flag (typeless and table_typeless), and packages/orm-rust/orm-schema/src/dbspec/validate.rs (foreign_key) keeps a null flag for every child column that exists and runs the set_null check over those. Green: make dbspec-compare-check exits 0 and its dbspec-compare/agreement step passes. Interface: the typeless fields of the private Rust structs changed type to pairs of name and null flag, so contracts/symbols/rust.json was re-recorded through the make environment (node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language rust), and its hash in contracts/interfaces.json changed from 9501c34a... to 2523fe80...; no public declaration changed. | [o] |
| T62-6 | A repeated settings block is handled as Go handles it in every implementation. Cause: Go keeps the first settings block of a table and reads a repeated one into a detached block (engine/dbspec/parse.go, tableLine and duplicate): its lines get the syntax checks of the first block's lines (unknown settings, name syntax, lex errors) but are never validated, and it reports order at the keyword of the repeated block only when its header line is exactly settings {; a malformed repeated header reports only its own syntax error. The other implementations appended the repeated block's lines to the first block, so they validated them (a second entity line reported setting, and an unknown column in it was reported), and they reported order for every repeated header. Python also failed with an IndexError on a settings line without {, and Rust read settings without { as a column. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's behaviour and all six implementations pass dbspec-compare-check. | Evidence: the Cause above was corrected before the fix: the first Cause did not state that a malformed repeated header reports no order error, nor that the other implementations kept the repeated lines in the first block. Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 in invalid/repeated-settings-block-invalid-lines); the five other runners differed from Go on the repeated-block cases, and with the cases for a missing { Python exited with an IndexError and Rust read the line as a column. Green: make dbspec-compare-check exits 0 and prints 12 runs agree; go test ./engine/dbspec -run TestSharedVectors passes; make dbspec-go-check, make dbspec-rust-check, make dbspec-php-check, make dbspec-php-extension-check, make dbspec-ts-check, make python-unit-check, make documents-check and make docs-rules-check exit 0. make interface-check first exits 2: the symbol snapshots of PHP, Rust and TypeScript list private fields and methods, so the new private declarations changed them (no public symbol changed). They were re-recorded with node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language <php|rust|typescript> and their hashes were updated in contracts/interfaces.json; then make interface-check exits 0. | [o] |
| T62-6-1 | A settings block that is still open at the end of the document is reported once, at its innermost open brace, in every implementation, as Go reports it. Cause: found by the probe of T62-6 (settings { and one entity line, then the end of the document), not added to tests/dbspec/cases.json because the case would fail T62-6: Go reports only block opened at this { at the settings brace (syntax 6 12), while PHP, the PHP extension, TypeScript, Rust and Python also report the table block is not closed at the table brace (syntax 3 13). | Acceptance: a shared case in tests/dbspec/cases.json pins Go's output for an open settings block at the end, and all six implementations pass dbspec-compare-check. | Evidence: the Cause was confirmed in engine/dbspec/parse.go (parseStructure adds one block opened at this %s is not closed at p.open, the innermost open brace). Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 in invalid/settings-block-open-at-end, with an extra ! syntax 3 13); running the other five runners on the same document shows the same extra line for typescript, python, rust and php-extension. Green: the closing branch of packages/orm-php/src/Dbspec/Parser.php, packages/orm-php-extension/src/parse.c, packages/orm-python/src/polyspec/orm/dbspec/parse.py, packages/orm-npm/src/dbspec/parse.ts and report_open_blocks of packages/orm-rust/orm-schema/src/dbspec/parser.rs reports only the innermost open brace, so an open settings block reports only its own brace; the two unit cases that pinned the double report are updated (unclosed-settings-and-table in packages/orm-rust/orm-schema/tests/dbspec_rules.rs and unclosed settings block in packages/orm-npm/tests/dbspec-rules.mjs). go test ./engine/dbspec -run TestSharedVectors prints ok. Exit codes after the fix: make dbspec-compare-check 0 (12 runs agree), make dbspec-go-check 0, make dbspec-rust-check 0, make dbspec-php-check 0, make dbspec-php-extension-check 0, make dbspec-ts-check 0, make python-unit-check 0, make rust-fmt-check/clients 0, make rust-check/clippy 0, make interface-check 0 (no snapshot changed, so none was re-recorded), make feature-unit-check 0, make repo-check 0. | [o] |
| T62-6-2 | A settings line without { that is the last open block of the document is reported as Go reports it in every implementation. Cause: found by a probe of T62-6-1 (settings without { after the primary key line, then the end of the document). Go reports syntax 6 3 at the keyword (block opened at this settings) and syntax 6 11 at the end of the line; PHP, TypeScript, Python and the PHP extension report only syntax 6 11, because they drop a second syntax error on a line that already has one; Rust already reported both. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output, and all six implementations pass dbspec-compare-check. | Evidence: Go's behaviour was confirmed in engine/dbspec/parse.go (tableLine and settingsLine open the settings block at the keyword when { is missing, cursor.fail adds line ends, expected '{' at the end of the line, and parseStructure adds block opened at this settings is not closed at the keyword). The shared case settings-without-brace-at-end was added with Go's output, and go test ./engine/dbspec -run TestSharedVectors prints ok. The same change covers the table and diagram end-of-document errors, so two more shared cases pin Go's output for a table and a diagram left open whose last line already has a syntax error: table-block-open-with-error-line-at-end (the table brace at line 3 column 13 and unexpected '(' at line 6 column 20) and diagram-block-open-with-error-line-at-end (the diagram brace at line 8 column 14 and unexpected '(' at line 9 column 16). With these cases, make dbspec-compare-check prints 12 runs agree, so every runner already agreed with Go on both, and no runner needed a fix. Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 in invalid/settings-without-brace-at-end, go 1: ! syntax 6 3, php 1: ! syntax 6 11); a direct run of the six runners on the same document showed the same difference for php, typescript, python and php-extension, and rust agreed with Go. Green: the end-of-document block opened error bypasses the one-syntax-error-per-line filter (unclosed in packages/orm-php/src/Dbspec/Parser.php, unclosed in packages/orm-php-extension/src/parse.c, unclosed in packages/orm-npm/src/dbspec/parse.ts, _unclosed in packages/orm-python/src/polyspec/orm/dbspec/parse.py). No unit test pinned the old behaviour. make interface-check first exited 2 because the new private methods entered the PHP and TypeScript symbol snapshots (contracts/symbols/php.json and contracts/symbols/typescript.json, one line each); both were re-recorded with node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language <php|typescript>, their hashes were updated in contracts/interfaces.json, and make interface-check then exits 0. Exit codes after the fix: make dbspec-compare-check 0 (12 runs agree), make dbspec-go-check 0, make dbspec-rust-check 0, make dbspec-php-check 0, make dbspec-php-extension-check 0, make dbspec-ts-check 0, make python-unit-check 0, make rust-fmt-check/clients 0, make rust-check/clippy 0, make interface-check 0, make feature-unit-check 0, make repo-check 0. | [o] |
| T62-6-3 | An error line or a table line inside a settings block that is still open at the end of the document keeps the settings state, as Go does, and the end of the document reports only the settings brace. Cause: found by two probes of T62-6-1 on the fixed tree: a settings block with an unknown setting bogus x on line 8 (all six implementations agree), and one with a table other { line on line 8. Go reports the line's own error and one syntax 6 12 at the settings brace; for the table line Rust reports column 8 7, key 8 7 and syntax 8 13 instead of Go's setting 8 1, and the other five implementations agree with Go. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output, and all six implementations pass dbspec-compare-check. | Evidence: Go's behaviour was confirmed in engine/dbspec/parse.go: settingsLine reads the keyword of a settings line as a setting name and reports unknown setting at it (setting 8 1), and the settings block stays open, so a table line there is not a block opener. Rust's setting_line called unclosed before it read the keyword, which closed the settings block and read table other { as a table line (column 8 7, key 8 7, syntax 8 13). The shared case settings-block-open-with-table-line (the settings brace at line 6 column 12 and setting 8 1) was added with Go's output, and go test ./engine/dbspec -run TestSharedVectors prints ok. Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (rust 1 differs from go 1 in invalid/settings-block-open-with-table-line, go 1: ! setting 8 1, rust 1: ! column 8 7); running the six runners directly on the same document showed the difference for rust only. Green: setting_line in packages/orm-rust/orm-schema/src/dbspec/parser.rs no longer calls unclosed, so a table or diagram line inside an open settings block is an unknown setting, as in Go; the other runners were unchanged. No unit test pinned the old behaviour. Exit codes after the fix: make dbspec-compare-check 0 (12 runs agree), make dbspec-go-check 0, make dbspec-rust-check 0, make dbspec-php-check 0, make dbspec-php-extension-check 0, make dbspec-ts-check 0, make python-unit-check 0, make rust-fmt-check/clients 0, make rust-check/clippy 0, make interface-check 0 (no snapshot changed), make feature-unit-check 0, make repo-check 0. | [o] |
| T62-6-4 | A table line inside an open table block is reported as Go reports it in every implementation. Cause: found by a probe of a table other { line after the column line of table users {, with and without the closing }. Go reports syntax 5 13 at the table line's brace; PHP, TypeScript, Python and the PHP extension also report name.format 5 1 and type 5 7 before it; Rust reports key 5 7 (and column 5 7 in the unclosed document) in its place, and in the closed document reports syntax 3 13, which Go does not. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output, and all six implementations pass dbspec-compare-check. | Evidence: Go's behaviour was confirmed in engine/dbspec/parse.go: tableLine sends a table line that is not a closing } to columnLine, so table other { is a column named table of type other, and the parser reports only the { as syntax 5 13 because the line does not parse; Go reports no name or type error for it. Re-verified on the current binaries before the fix (the compare run that kept its build directory): php, typescript, python and php-extension added name.format 5 1 and type 5 7 before syntax 5 13; rust reported syntax 3 13 in the closed document and key 5 7 in place of syntax 5 13 (column 5 7 as well in the unclosed document). The shared cases table-line-inside-open-table (closed, with name.duplicate 6 3 and column 6 10) and table-line-inside-open-table-at-end were added with Go's output, and go test ./engine/dbspec -run TestSharedVectors prints ok. Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 in invalid/table-line-inside-open-table, go 1: ! syntax 5 13, php 1: ! name.format 5 1). Green: a column line with a syntax error reports only its syntax errors, as Go does: keepSyntaxSince in packages/orm-php/src/Dbspec/Parser.php, keep_syntax_since in packages/orm-php-extension/src/parse.c, keepSyntaxSince in packages/orm-npm/src/dbspec/parse.ts, _column in packages/orm-python/src/polyspec/orm/dbspec/parse.py, and column_line_in_order in packages/orm-rust/orm-schema/src/dbspec/parser.rs; the Rust table_line no longer calls unclosed for a table line, so the line is a column line as in Go. The TypeScript unit case columns count code points pinned the old column error on a line with a syntax error and now expects only syntax 4 32; a shared case column-unknown-type-with-syntax-error pins Go's output for other other { (key 3 7, column 4 10, syntax 5 15), which the other runners reported with an extra type 5 9 before the fix. make interface-check first exited 2 because the new private methods keepSyntaxSince (PHP and TypeScript) changed their symbol snapshots (contracts/symbols/php.json and contracts/symbols/typescript.json, one line each); both were re-recorded with node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language <php|typescript> and their hashes updated in contracts/interfaces.json. Exit codes after the fix: make dbspec-compare-check 0 (12 runs agree), make dbspec-go-check 0, make dbspec-rust-check 0, make dbspec-php-check 0, make dbspec-php-extension-check 0, make dbspec-ts-check 0, make python-unit-check 0, make rust-fmt-check/clients 0, make rust-check/clippy 0, make interface-check 0, make feature-unit-check 0, make repo-check 0. | [o] |
| T62-6-5 | A diagram line inside an open table block is reported as Go reports it in the Rust dbspec parser. Cause: found by a probe of T62-6-4 on the current binaries (diagram main { and users at 0 0 after the column line of table users {, then }, not added to tests/dbspec/cases.json because the case would fail). Go reports key 3 7, column 4 10, syntax 5 14 and syntax 6 12; the PHP, TypeScript, Python and PHP extension runners report the same four errors; the Rust runner reports syntax 3 13 (the table brace) and column 4 10, and omits the two syntax errors of the diagram lines. The Rust table_line still calls unclosed for a diagram line, which closes the table; the table line of T62-6-4 is exempt. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output, and all six implementations pass dbspec-compare-check. | Evidence: Go's behaviour was confirmed in engine/dbspec/parse.go: tableLine sends a diagram line that is not a closing } to columnLine, so diagram main { is a column named diagram of type main, and the parser reports syntax 5 14 at the { and syntax 6 12 at the 0 of the next line (users at 0 0), which is a column line of type at with an unexpected token. The shared case diagram-line-inside-open-table (key 3 7, column 4 10, syntax 5 14, syntax 6 12) was added with Go's output, and go test ./engine/dbspec -run TestSharedVectors prints ok. Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (rust 1 differs from go 1 in invalid/diagram-line-inside-open-table); running the six runners directly on the same document showed the difference for rust only: syntax 3 13 (the table brace) and no syntax 5 14 or syntax 6 12. Green: table_line in packages/orm-rust/orm-schema/src/dbspec/parser.rs no longer calls unclosed for any line, so a table or diagram line inside an open table block is a column line, as in Go; the Korean comment says so. unclosed remains in use for the diagram context. The other runners were unchanged. No unit test pinned the old behaviour. Exit codes after the fix: make dbspec-compare-check 0 (12 runs agree), make dbspec-go-check 0, make dbspec-rust-check 0, make dbspec-php-check 0, make dbspec-php-extension-check 0, make dbspec-ts-check 0, make python-unit-check 0, make rust-fmt-check/clients 0, make rust-check/clippy 0, make interface-check 0 (no snapshot changed), make feature-unit-check 0, make repo-check 0. | [o] |
| T62-6-6 | A malformed type argument list on a column line (varchar(64 x, varchar(64,), decimal(10 2)) is reported as Go reports it in the PHP, Rust and PHP extension dbspec parsers. Cause: found by a probe of the column-line combinations on the current binaries, in a closed table, not added to tests/dbspec/cases.json because the case would fail. Go reports only syntax at the first token the type argument list does not accept (syntax 5 19 in name varchar(64 x, the x). The TypeScript and Python runners report the same syntax 5 19. The PHP, Rust and PHP extension runners report type 5 8 at the type name and no syntax error, so the bad token is never reported. | Acceptance: shared cases in tests/dbspec/cases.json pin Go's output, and all six implementations pass dbspec-compare-check. | Evidence: Go's behaviour was confirmed in engine/dbspec/parse.go: columnLine reads each type parameter with cursor.value, which accepts a word or a number (and - before a number), then requires , or ); any other token is a syntax error at that token (expected a type parameter, or ',' or ')'). A word or a dotted number is accepted here and is rejected later by resolveType as a type error at the type name, and only when the line parses. The shared cases column-type-argument-unclosed (varchar(64 x), column-type-argument-trailing-comma (varchar(64,) null) and column-type-argument-decimal-space (decimal(10 2)) were added with Go's output, each syntax 5 19, and go test ./engine/dbspec -run TestSharedVectors prints ok. Red: before the fix make dbspec-compare-check exits 2 at dbspec-compare/agreement (php 1 differs from go 1 in invalid/column-type-argument-unclosed, go 1: ! syntax 5 19, php 1: ! type 5 8); the per-runner diff showed the same type 5 8 for rust and php-extension, and syntax 5 19 for typescript and python. Green: the type parameter loop of type() in packages/orm-php/src/Dbspec/Parser.php, read_type in packages/orm-php-extension/src/parse.c, and column_type in packages/orm-rust/orm-schema/src/dbspec/parser.rs reads each parameter as Go does: a number, a word or - number is a value (a non-numeric value keeps the type error and the line parses otherwise), any other token where a value belongs is syntax at that token, and any token other than , or ) after a value is syntax at that token. The leftover tokens after the first syntax error are not reported, as in Go. A word is classified by the lexers' own word predicate, not an ASCII-only test: PHP uses Parser::isWord (the generated UnicodeWord table), C uses str_dotted (word_rune_width, the generated unicode_word.h table), and Rust reads the token kind the lexer assigned (Kind::Word, is_word_rune). The shared cases column-type-argument-unicode-word (varchar(é)) and column-type-argument-unicode-decimal (decimal(10,é)) pin Go's output, a type error at the type name (type 5 8), which go test ./engine/dbspec -run TestSharedVectors confirms; all six runners agree on them in make dbspec-compare-check. The first version of the fix used an ASCII-only word test, so these two cases were added after that change, and they were not observed failing against it. No unit test pinned the old behaviour. Exit codes after the fix: make dbspec-compare-check 0 (12 runs agree), make dbspec-go-check 0, make dbspec-rust-check 0, make dbspec-php-check 0, make dbspec-php-extension-check 0, make dbspec-ts-check 0, make python-unit-check 0, make rust-fmt-check/clients 0, make rust-check/clippy 0, make interface-check 0 (no snapshot changed), make feature-unit-check 0, make repo-check 0. | [o] |
| T62-3-1 | engine/dbspec/literal_test.go belongs to a feature of contracts/features.json. Cause: T62-3 added the test of CanonicalLiteral without selecting it in a feature, so the feature-unit check reports that the tracked file belongs to no feature. | Acceptance: the file is an input of the schema feature that owns engine/dbspec/literal.go, and the feature-unit check no longer reports it. | Evidence: Red: before the change make feature-unit-check exits 2 at feature-unit-check/owners; its only failing test, every tracked file selects a behaviour test or declares its scope (scripts/features/owners.test.mjs), reports engine/dbspec/literal_test.go (untestedPaths). The same run prints FAIL sample/owner/... lines (typescript/none, typescript/mysql, typescript/postgres, go/none) of the T62-4-4 fixtures in suites that report fail 0; they are not changed here. The wording belongs to no feature is the message of scripts/features/check.mjs, which this run does not reach. Green: contracts/features.json lists the file in the tests of schema_definition and in the inputs of schema-go (make dbspec-go-check), next to its sibling test files. The tests entry alone left the report, because the check accepts a file only as an input of a verification command. make feature-unit-check exits 0 and no longer reports the file; make interface-check exits 0; make dbspec-go-check exits 0; make version-check/run exits 0; make repo-check/run exits 2 on a toolchain check (Go 1.27.0 is installed, .go-version declares 1.27.2), the same with HEAD's features.json, so this change does not cause it; make documents-check exits 0. | [o] |
| T62-4-1 | The PHP and TypeScript implementations classify name characters from the generated Go table, not from their runtime's Unicode data. Cause: T62-4 found them equal to Go at Unicode 17.0.0 only by an ad-hoc check; PCRE's and the JavaScript engine's Unicode versions change with the runtime, and no committed test pins them. | Acceptance: both read a table produced by the T62-4 generator, and its freshness and exhaustive code-point tests run in their checks. | Evidence: Red: before the tables existed, go test ./tests/dbspec/unicode -count=1 exited 1 at TestCommittedTablesAreGenerated (packages/orm-php/src/Dbspec/UnicodeWord.php: open ... no such file), php packages/orm-php/tests/dbspec_unicode_test.php exited 255 (its uncaught RuntimeException at the table read), and node --test packages/orm-npm/tests/dbspec-unicode.mjs exited 1 (Cannot find module .../dist/dbspec/unicode_word.js). Change: go run ./tests/dbspec/unicode -write wrote packages/orm-php/src/Dbspec/UnicodeWord.php (UnicodeWord::contains, UnicodeWord::RANGES) and packages/orm-npm/src/dbspec/unicode_word.ts (containsWordRune, WORD_RANGES); the generator's freshness test covers both; PHP Parser::wordRune and Parser::isWord (public, read by Expression::operand in place of the \p{L}\p{Nd} regex) and the TypeScript isWordRune of parse.ts classify with them. Green: go test ./tests/dbspec/unicode -count=1 exits 0; php packages/orm-php/tests/dbspec_unicode_test.php exits 0 and prints 1114112 code points agree; make dbspec-php-check exits 0; make dbspec-ts-check exits 0 (ℹ pass 378, ℹ fail 0, with dbspec-unicode.mjs); make dbspec-compare-check exits 0 and prints 12 runs agree; make interface-check exits 0 after contracts/symbols/php.json and contracts/symbols/typescript.json were re-recorded (PHP Parser::isWord changed from private to public, new UnicodeWord class and contains, new TypeScript export containsWordRune) and their hashes in contracts/interfaces.json were updated; make feature-unit-check exits 0; make documents-check and make docs-rules-check exit 0. | [o] |
| T62-4-2 | Every word predicate that reads dbspec or database CHECK text agrees with Go's. Cause: T62-4 left ASCII-only predicates in packages/orm-php/src/Dbspec/CheckDecoder.php (ctype_alpha), packages/orm-php-extension/src/introspect.c, packages/orm-rust/orm-schema/src/dbspec/introspect/check.rs and the CHECK_TOKEN pattern of packages/orm-php/src/Dbspec/PlanObjects.php, without comparing them to the Go code they mirror. | Acceptance: each predicate is compared with its Go counterpart and either reads the generated table or is shown, with a shared case, to read only text whose characters Go also limits to ASCII. | Evidence: Go counterparts: engine/dbspec/introspect_check.go:90-92 (checkTokens, ASCII letters, digits and _ only, any other byte is unexpected character), engine/dbspec/lex.go:104-108 (isWordRune, Unicode, but a non-ASCII word outside a string fails the ASCII name rule validNameFormat at lex.go:252 with name.format), and engine/dbspec/parse.go:316 (isHeaderNameRune, ASCII). Verdicts: CheckDecoder.php ctype_alpha/ctype_alnum read high bytes as word bytes on the PHP 8.5 build here (65 bytes accepted with LC_ALL=C), so the predicate was changed to an explicit ASCII helper isAsciiLetter, which keeps the decision with Go; before it, ê > 0 gave reason name.format and after it unexpected character, as Go gives. introspect.c calpha/word_char, check.rs is_word_start, TypeScript isLetter (introspect_check.ts:53), PlanObjects.php CHECK_TOKEN and isColumn, Renderer.php token pattern and operand regex, and the PHP header and name regexes are ASCII and unchanged. Shared cases: tests/dbspec/cases.json check-non-ascii-literal and non-ascii-digit-after-document-name; tests/dbspec/introspect.json sqlite-check-non-ascii-word and sqlite-check-non-ascii-literal. A plan case with a non-ASCII check literal was removed because its physical MySQL run mojibakes the literal (see T62-4-6). Commands and exit codes: make dbspec-compare-check 0 (12 runs agree); make dbspec-php-check 0; make dbspec-php-extension-check 0 (after the UnicodeWord @internal docblock fix in tests/dbspec/unicode/main.go); make dbspec-rust-check 0; make dbspec-introspect-check, make dbspec-introspect-php-check, make dbspec-introspect-php-extension-check, make dbspec-introspect-ts-check and make dbspec-introspect-rust-check 0, with the local MySQL and PostgreSQL servers of .runtime/servers/env; make dbspec-apply-check, make dbspec-apply-php-check, make dbspec-apply-ts-check, make dbspec-apply-rust-check, make dbspec-apply-php-extension-check and make dbspec-ddl-check 0; make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check and make dbspec-plan-rust-check 0; make interface-check 0 (contracts/symbols/php.json re-recorded: one line, the private CheckDecoder::isAsciiLetter, its hash in contracts/interfaces.json updated); make feature-unit-check 0 (its FAIL lines are the asserted negative samples of T62-4-4); go test ./engine/dbspec and go test ./tests/dbspec/unicode pass; make documents-check and make docs-rules-check 0. | [o] |
| T62-4-3 | cargo clippy -p polyspec-orm-schema --all-targets -- -D warnings passes. Cause: it reports an unused first in packages/orm-rust/orm-schema/src/dbspec/parser.rs and a map_or in is_number of lexer.rs. | Acceptance: make rust-check/clippy exits 0. | Evidence: Red: before the change make rust-check/clippy exits 2 at the library target polyspec-orm-schema (lib), which stops with three lints: unused variable: first (orm-schema/src/dbspec/parser.rs:431), clippy::unnecessary_map_or (orm-schema/src/dbspec/lexer.rs:55, fraction.map_or(true, digits)) and clippy::too_many_arguments (orm-schema/src/dbspec/validate.rs:88, eight arguments against seven; the Cause names two lints, the third is reported by the same run). Causes: the binding first is left over from T62-1 (72e2b5353), which removed the check_line guard of the invalid-character branch, so the branch takes check lines too; no reader remains, so the binding is dead, not a missed check. Go's check branch returns after its lexical error (engine/dbspec/parse.go, the check case), and the Rust branch reports the same first bad character and gives a check line no name effect; no shared case pins a check line with such a character, so this rests on the code. The map_or is clippy's is_none_or, and the eight arguments of validate are the four parse fields of Parsed plus the three outputs. Change: first is removed; fraction.is_none_or(digits); validate(parsed: &Parsed, used, diags, literals) reads document, unresolved, failed_keys, header_failed and defaults from parsed, and dbspec/mod.rs passes parsed. Green: make rust-check/clippy exits 0 and prints PASS rust-check/clippy; make rust-fmt-check/clients exits 0; make dbspec-rust-check exits 0; make dbspec-compare-check exits 0 and prints 12 runs agree. make interface-check exited 2 at interfaces/rust because the signature of validate changed; the Rust symbol snapshot was re-recorded (contracts/symbols/rust.json, only the validate line changed; the Go, PHP, TypeScript, Python and PHP extension snapshots were unchanged), its hash in contracts/interfaces.json was updated, and make interface-check then exits 0 (rust: 2830 native symbols inspected). | [o] |
| T62-4-4 | The feature-unit check passes over its sample/owner fixtures. Cause: the FAIL lines that make feature-unit-check prints for sample/owner/* (typescript/none, typescript/mysql, typescript/postgres, go/none) are not failures. executeCoverage (scripts/features/coverage.mjs) prints each run's FAIL line as the run ends, and the negative cases of scripts/features/coverage.test.mjs run these fixtures on purpose and assert the errors they return: the owner that prints duplicate or unexpected output, the sqlite-only DSN that makes the mysql and postgres state readers fail, and the Go test that skips. A reader of a passing run sees the FAIL lines, but the check reports no failure. | Acceptance: the failures are reproduced at the commit before T62-4, their cause is named and removed, and the feature-unit check exits 0. | Evidence: at 72e2b535, the commit before T62-4 (558fd790), make feature-unit-check prints the same FAIL lines, so they predate T62-4. The asserting tests are native owner and dependent files execute twice from their own parts (scripts/features/coverage.test.mjs lines 166, 168 and 170: unexpected test output, duplicate observed case first and no executable command; the call at line 170 also re-runs the owner and prints its duplicate line), checker reads physical database state around each native test run (lines 312-313 and 317) and Go, PHP, and Rust native cases execute through their owning files (line 381, observed cases .* differ from TestFirst). At HEAD make feature-unit-check exits 0, and node --test scripts/features/coverage.test.mjs prints ℹ pass 17 and ℹ fail 0. Nothing was removed, because no sample fails unexpectedly. The run at 72e2b535 exits 2 at feature-unit-check/owners for the test file of T62-3-1, which is fixed at HEAD. | [o] |
| T63 | Add the markdown IR executor to the Go client. Cause: the flowmark plan (Layering, and step T12.9, the server, which requires the orm protocol item) makes the Go client the first orm client to reach a corpus through markdown://, and every other client follows its executor. | Acceptance: the DSN markdown://127.0.0.1:<port>/<corpus>?token=<token> connects to a flowmark server and runs every IR kind, transactions and savepoints through it; a corpus or token mismatch fails with the CONFIG error of the server; a write that fails with WRITE_CONFLICT is returned to the caller and not retried; the conformance of the Go client runs every IR kind against the flowmark build pinned in tests/markdown/flowmark.version, with exact case IDs, and every case passes; a missing pinned build fails the check and is not skipped. Requires T61, T62 and the flowmark server (flowmark checklist T12.9). State: done on branch feat/markdown-go-T63 (6b6079bb), 33 of 33 cases of tests/markdown green against a local flowmark build; it merges after T114, because its CI step installs a flowmark commit that is not on GitHub until flowmark's T12 items close. The Go Subscribe returning (func(), error) is followed by T64–T67 in their clients, and the exported ir.ParamUses is owed by T89 in the other implementations. | Evidence (local, macOS): make markdown-conformance-check/go against the flowmark build of the pinned revision fd4eee986f7c (go build ./cmd/flowmark in a checkout of it, module version equal to tests/markdown/flowmark.version) exits 0 and every case of tests/markdown/cases.json passes. | [o] |
| T64 | The PHP client's markdown IR executor. Cause: the flowmark plan (Layering, and step T12.9) reaches each orm client through the IR executor that the Go client implements first, so the PHP client needs its own executor for the same protocol. | Acceptance: the PHP client connects with the DSN markdown://127.0.0.1:<port>/<corpus>?token=<token> to a flowmark server and runs every IR kind, transactions and savepoints through it; its conformance runs the same case IDs as the Go client against the flowmark build pinned in tests/markdown/flowmark.version, and every case passes; a missing pinned build fails the check. Requires T63. | Evidence (local, macOS): make markdown-conformance-check/php against the flowmark build of the pinned revision fd4eee986f7c (go build ./cmd/flowmark in a checkout of it, module version equal to tests/markdown/flowmark.version) exits 0 and every case of tests/markdown/cases.json passes. | [o] |
| T65 | The Python client's markdown IR executor. Cause: the flowmark plan (Layering, and step T12.9) reaches each orm client through the IR executor that the Go client implements first, so the Python client needs its own executor for the same protocol. | Acceptance: the Python client connects with the DSN markdown://127.0.0.1:<port>/<corpus>?token=<token> to a flowmark server and runs every IR kind, transactions and savepoints through it; its conformance runs the same case IDs as the Go client against the flowmark build pinned in tests/markdown/flowmark.version, and every case passes; a missing pinned build fails the check. Requires T63. | Evidence: the executor commit had dropped out of main in a rebase, so polyspec/orm/markdown.py and engine/params.py were untracked while tests/test_markdown.py and tests/markdown_conformance.py were on main and the unit discovery failed with ModuleNotFoundError: No module named 'polyspec.orm.markdown'; the executor is restored onto the current client (the Engine of each registered set, the request hook before the markdown dispatch). FLOWMARK=.runtime/bin/flowmark python packages/orm-python/tests/markdown_conformance.py passes 34 cases against the pinned build (tests/markdown/flowmark.version, built from the module cache), and unittest discover -p 'test_*.py' runs 138 tests, test_markdown included. | [o] |
| T66 | The Rust client's markdown IR executor. Cause: the flowmark plan (Layering, and step T12.9) reaches each orm client through the IR executor that the Go client implements first, so the Rust client needs its own executor for the same protocol. | Acceptance: the Rust client connects with the DSN markdown://127.0.0.1:<port>/<corpus>?token=<token> to a flowmark server and runs every IR kind, transactions and savepoints through it; its conformance runs the same case IDs as the Go client against the flowmark build pinned in tests/markdown/flowmark.version, and every case passes; a missing pinned build fails the check. Requires T63. | Evidence (local, macOS): make markdown-conformance-check/rust against the flowmark build of the pinned revision fd4eee986f7c (go build ./cmd/flowmark in a checkout of it, module version equal to tests/markdown/flowmark.version) exits 0 and every case of tests/markdown/cases.json passes. | [o] |
| T67 | The TypeScript client's markdown IR executor. Cause: the flowmark plan (Layering, and step T12.9) reaches each orm client through the IR executor that the Go client implements first, so the TypeScript client needs its own executor for the same protocol. | Acceptance: the TypeScript client connects with the DSN markdown://127.0.0.1:<port>/<corpus>?token=<token> to a flowmark server and runs every IR kind, transactions and savepoints through it; its conformance runs the same case IDs as the Go client against the flowmark build pinned in tests/markdown/flowmark.version, and every case passes; a missing pinned build fails the check. Requires T63. | Evidence (local, macOS): make markdown-conformance-check/typescript against the flowmark build of the pinned revision fd4eee986f7c (go build ./cmd/flowmark in a checkout of it, module version equal to tests/markdown/flowmark.version) exits 0 and every case of tests/markdown/cases.json passes. | [o] |
| T68 | Remove the writing-style forbidden expression from the Korean checklist. Cause: the writing-style rule (docs.writing-style of contracts/rules.json) forbids 밝힌다 in the Korean T58 line, so make docs-rules-check fails on main. | Acceptance: make docs-rules-check exits 0, and the Korean checklist states the same facts as this checklist (T58: docs/spec/dsl.md and docs/dsl.ko.md state the Python client). | Evidence: make docs-rules-check prints PASS docs-rules and make documents-check passes, both exit 0 in this tree. | [o] |
| T70 | The repository check accepts a running Go or Composer whose major and minor version match the declared release, whatever its patch. Cause: scripts/repo/toolchains.mjs requires the exact release that .go-version and .composer-version declare, so a local Go 1.27.0 against a declared 1.27.2 fails make repo-check and blocks every owner check, although a patch release changes neither the language nor the module behaviour; the PHP check already compares major and minor only. | Acceptance: goVersionErrors and composerVersionErrors compare the running major and minor with the declared release, with accepting cases for another patch and rejecting cases for another minor; the declarations and the CI workflows still pin one exact release; make repo-check passes with Go 1.27.0. | Evidence: the item is registered by d5f6f788, and f28b4908 changes goVersionErrors and composerVersionErrors in scripts/repo/toolchains.mjs to compare the running major and minor with the declared release. scripts/repo/check.test.mjs gains the cases "a running Go with the declared major and minor passes whatever its patch" and "a running Composer with the declared major and minor passes whatever its patch", and the rejecting cases for another minor (Go 1.28.0 against the declared 1.27.0 and Composer 2.11.0 against 2.10.3). The Red run before the change is not repeated in this record. Green: make repo-check/unit exits 0 and prints tests 90, pass 90, fail 0; make repo-check exits 0 with go1.27.0 against .go-version 1.27.2 and Composer 2.10.3 against .composer-version 2.10.3, and prints PASS repo; make documents-check and make docs-rules-check exit 0. | [o] |
| T73 | ir.Validate holds every rule of a stored filter. Cause: flowmark's view validation (its T12.3-1) found that ir.Validate does not resolve a column-to-column predicate's ref path and column (only the planner's renderPred does), accepts conn on the top-level group and on a group's first item, and offers no way to learn the type a parameter binds, which the planner's functionType and valueFunctionArgType compute. | Acceptance: ir.Validate resolves Ref.Path (the root, a joined relation, ^ only inside a subquery) and requires the target's column, else COLUMN_UNKNOWN; it refuses conn on the top group and on a group's first item as IR_INVALID; the same rules hold in every implementation's validator, pinned by shared vectors; red-then-green tests. An exported parameter-use list is not part of this item: it enters orm only with a CODEC_ENCODE standard every client keeps, as its own item. | — | [o] |
| T74 | Codec decode is strict and has one public surface in every implementation. Cause: the Go host stages hex and ip live in the unexported hostDecode while the other clients expose their equivalent, and the clients disagree on white space: Go trims around hex and base64 (strings.TrimSpace), and the other implementations each differ. A stored value is the encoder's exact output, so a trim accepts values no encoder wrote. | Acceptance: shared vectors first reproduce the disagreement red in all six implementations; then every implementation refuses white space around and inside hex and base64 text, and Go exports HostDecode as the others' counterpart; docs (EN and ko) state the rule. A keyless decode of every stage for one consumer is not part of orm. | — | [o] |
| T75 | make dbspec-apply-stress-mysql runs on macOS. Cause: its command passes TEST_ENV="$ORM_OWNER_TEST_ENV", which only make feature-owner-check sets, so the macOS sh refuses . with no file name (exit 2), while Linux CI passed it on 7ea3932b (found in T62-4-10). | Acceptance: the helper reads its environment from a declared source on every platform, and runs on macOS and Linux. | — | [o] |
| T76 | AGENTS states the design principles. Cause: the user's rules for every repository — no backward compatibility, the simplest implementation that fully meets the current requirements, modular components with separated concerns, long-term architecture and no stopgap — were in flowmark's AGENTS but not in this repository's. | Acceptance: AGENTS.md and AGENTS.ko.md state them; make documents-check passes. | — | [o] |
| T77-13 | Generated files are never tracked. Cause: the repository had no rule for generated files, and its .gitignore lacked the vendor/, venv/ and generic *.egg-info/ patterns, so a dependency or build output could be committed unnoticed (the history purge of node_modules followed). | Acceptance: AGENTS.md (+ko) states the rule and its named exceptions; .gitignore covers every pattern the rule names; make staged-check refuses a staged covered path that is not a named exception, and .githooks/pre-commit runs it before each commit. | Evidence: node --test scripts/check/staged-generated.test.mjs passes 23 cases (each covered pattern refused, each exception and ordinary path allowed, a mixed list reports only the offender); make staged-check reports no staged generated file; config/checklist.json lists the pre-commit hook. | [o] |
| T77-14 | The Python tests of the interface and conformance checks pass and belong to a feature. Cause: test_python_extractor.py and test_python_natives.py read the symbol snapshot as strings, but since T77-8-1 the snapshot and the extractor carry {exported, signature} per symbol, so 8 tests failed (2 extractor, 6 natives); conformance_result.py ended with unittest.main() instead of testcase.main(), which the harness check rejects, and the three files belonged to no feature. | Acceptance: the tests read the signature of each symbol, the module ends with testcase.main(), the three files are listed by their feature, and make python-unit-check passes. | Evidence: make python-unit-check runs 1352 tests and passes; node scripts/features/check.mts reports no python test file that belongs to no feature. | [o] |
| T77-15 | The performance check passes its static check after the Go check unit was dropped. Cause: dropping the Go unit (T77-8-1-1) left the go-internal parity id in the performance_gate feature and kept the Go owner, so node scripts/features/check.mts failed on main with two findings. | Acceptance: the feature names only the parity units that exist, its owners are the implementations that hold a unit, and the static check passes. | Evidence: node scripts/features/check.mts reports 19 feature contracts passed; make feature-docs leaves the generated docs unchanged. | [o] |
| T77-16 | The generated-file check is kit's pre-commit hook. Cause: the repository kept its own .githooks/pre-commit and staged-generated check beside kit's K210 hook, a second mechanism; the rule now has one owner. | Acceptance: config/generated.json declares the repository's paths and exceptions, the local hook, its check and its test are removed, make hooks installs kit's .githooks/pre-commit, and a staged node_modules path is refused while a fixture path passes. | Evidence: kit's pre-commit hook exits 2 on a staged node_modules/x/index.js and exits 0 on a staged tests/probe/fixtures/a.json, both run directly without a commit; make hooks-check passes. | [o] |
| T77-17 | The conformance result check runs in module mode. Cause: its Makefile target ran the file directly, which leaves packages off sys.path, so the check failed with ModuleNotFoundError on main. | Acceptance: the target runs the module with the venv in module mode, with no PYTHONPATH, and the check exits 0. | Evidence: packages/python/.venv/bin/python -m packages.python.tests.conformance_result exits 0 with OK on main. | [o] |
| T77-18 | The repository convention tests pass on main. Cause: four violations of tests/testcase.mts and scripts/repo/testcases.mts: a Python thin-slice tool outside the four languages (moved into packages/python/examples/thin_slice as the Python client example), a Rust test whose case does not start with the shared testcase package (request.rs, collection_identity.rs), and a JavaScript test that declares its cases with node:test (packages/ts/tests/plan-cache-in.mjs, run by a feature command). | Acceptance: make repo-check/unit exits 0. | Evidence: make repo-check/unit exits 0 in this worktree after the four fixes. | [o] |
| T77-19 | The interface self-test names the missing TypeScript checker dependency before it runs. Cause: the self-test ran the TypeScript declaration extractor without its dependency, so a checkout that had not run install-interfaces-typescript failed in parity-check with ERR_MODULE_NOT_FOUND. | Acceptance: with the dependency absent, interface-check fails at the named prerequisite with the install step; with it present, the self-test passes. | Evidence: interface-typescript-check exits 0 with the dependency present and exits 1 with the dependency moved away, and parity-check then runs the TypeScript interface stage. | [o] |
| T77-20 | A parity-check run without the bench databases names the missing DSN. Cause: parity-check ran with no BENCH_*_DSN, so every conformance and coverage unit failed with the same "BENCH_SQLITE_DSN is required" message (111 units exited non-zero in one run). The bench DSNs come from run-databases, which creates the bench databases. | Acceptance: with a DSN unset, parity-check stops at a guard that names the variable and the command to run; with all three set, the guard passes. Red: an unset BENCH_SQLITE_DSN made the Go conformance unit fail with that message. Green: parity-bench-env-check exits 2 naming the variable when one is unset and exits 0 when all three are set. | — | [o] |
| T77-21 | The TypeScript engine unit expects the bench entity order that bench.dbs declares. Cause: its expected entity list lacked day_cell, which bench.dbs declares between clock_cell and upsert_cell, so the unit reported "entities in document order" FAIL and exited 1 while its 117 other cases passed. | Acceptance: the unit exits 0 with every case passing. Red: that FAIL line and exit 1 from the unit run alone. Green: node --conditions=orm-test --test packages/ts/tests/engine.mjs exits 0 with 117 pass and 0 fail. | — | [o] |
| T77-22 | The parity units receive the bench DSNs of their run. Cause: run-databases writes the bench DSNs to its TEST_ENV file, but make did not export them, so the units kit starts from make ran without BENCH_*_DSN and every bench unit failed with the same message, although the Go SQLite unit passes when the file is sourced. | Acceptance: make reads the bench DSNs of TEST_ENV at parse time and exports them, a child recipe sees BENCH_SQLITE_DSN, and the guard still names a missing DSN. Red: the Go SQLite unit fails with BENCH_SQLITE_DSN is required under the parity run. Green: with the run TEST_ENV, the child sees the DSN and the Go unit passes; with no file, the child sees none and the guard exits 2. | — | [o] |
| T77-23 | The dbspec harness test expects the case names that the harness prints. Cause: T77-9-4-2 flattened the case labels, so the failing-case fixture prints shop, export-shop and the other flat ids, but TestCaseHarnessReportsOnlyFailure still looked for mermaid/export/shop and similar. The Go dbspec package then exited 1 with 1 of 1 ids passed, which the parity reporter did not name. | Acceptance: the expected names are the flat ids, and the dbspec package exits 0. Red: TestCaseHarnessReportsOnlyFailure fails with no FAIL line for mermaid/export/shop and the others. Green: the test passes alone, and go test of packages/go/engine/dbspec exits 0. | — | [o] |
| T77-25 | The markdown conformance units find the flowmark binary they read. Cause: FLOWMARK pointed at .runtime/bin/flowmark, which no parity prerequisite builds, so the Go and other markdown units failed with FLOWMARK is not set or no Go build info (0 of 35 in each language). | Acceptance: parity-check names the missing binary and the install step before it runs units, and with the binary installed the Go markdown conformance unit exits 0. Red: the Go markdown unit without FLOWMARK fails with FLOWMARK is not set. Green: flowmark-check exits 2 naming install-flowmark when the binary is missing, exits 0 after make install-flowmark, and TestMarkdownConformance exits 0 with FLOWMARK set. | — | [o] |
| T77-24 | The physical query-form test reads the DSN names the environment sets. Cause: TestPhysicalQueryForms read ORM_MIGRATION_MYSQL_DSN and ORM_MIGRATION_POSTGRES_DSN, which no make target or script sets, so it failed with ORM_MIGRATION_MYSQL_DSN is required. | Acceptance: the test reads ORM_TEST_MYSQL_DSN and ORM_TEST_POSTGRES_DSN, the names the sibling physical tests read. Red: the test fails with ORM_MIGRATION_MYSQL_DSN is required. Green: the failure moves to the next cause (the MySQL DSN form and the place.location fixture, rows T77-26 and T77-27). | — | [o] |
| T77-28 | The models script scan test covers every directory whose sources call models. Cause: the models script scans packages/ts/testing/conformance/runner_typescript.mts, which calls models, but the test listed only tests/, tests/conformance and examples/thin-slice/ts, so it failed with the runner missing from the calling set (the parity reporter counted that as 2 of 1 cases). | Acceptance: the test lists the client testing directory and passes. Red: the deep-equal failure naming packages/ts/testing/conformance/runner_typescript.mts. Green: packages/ts/tests/models-scan.mjs exits 0 with 3 pass and 0 fail. | — | [o] |
| T77-29 | The examples tests find the Python thin-slice at its moved location. Cause: T77-18 moved examples/thin-slice/python/main.py to packages/python/examples/thin_slice/main.py, but the examples DSN and output tests still ran the old path, so the package failed with can not open file. | Acceptance: both tests use the moved path and the examples package exits 0. Red: the examples package fails with can not open file examples/thin-slice/python/main.py. Green: go test of ./examples exits 0. | — | [o] |
| T77-26 | The physical query-form test opens MySQL with the driver form of its DSN. Cause: TestPhysicalQueryForms passes the mysql:// URL from the environment straight to sql.Open, so the Go driver reports "default addr for network 127.0.0.1:53294 unknown". The conversion exists once in internal/dialects/servers.go (mysqlConfig); acceptance: the test reads the MySQL DSN through that one conversion, not per test. Red: the default addr line. Green: the MySQL subtest opens and runs its cases. | — | Evidence: TestPhysicalQueryForms, run alone with the bench environment, exits 0 on mysql, postgres and sqlite; the mysql subtest now compiles, runs and drops its tables. | [o] |
| T77-27 | The physical query-form functions run on PostgreSQL and SQLite. Cause: TestPhysicalQueryForms fails with COLUMN_UNKNOWN place.location on postgres and sqlite, so the place table the test compiles against has no location column while the query names it. | Acceptance: the fixture the test uses declares place.location, or the query names only declared columns, with the reason stated; the subtests compile and run. Red: COLUMN_UNKNOWN place.location. Green: the postgres and sqlite subtests compile and pass. | Evidence: TestPhysicalQueryForms exits 0 on all three databases. The day_of_week expectation follows the fixture: seq 1 (2020-01-02, Thursday) and seq 2 (48 hours before the run) both have DAYOFWEEK 5 on a Thursday-day run, so the expected count is computed, and the distance case, retired by T148-3-7-4-7, is removed. | [o] |
| T77-30 | The TypeScript generate unit declares setDay with the generated signature. Cause: the unit fails with FAIL: declaration setDay(value: string | Date | null): this, so the generated declaration and the declaration the unit expects disagree. | Acceptance: the generated declaration matches the expected one, the unit exits 0. Red: the FAIL line, exit 1 from the unit alone. | Evidence: generate unit exits 0 on TypeScript; the expected setDay declaration is the text form the spec names (docs/spec/dbspec.md line 347). | [o] |
| T77-31 | The TypeScript SQLite concurrency unit opens its database file. Cause: the unit fails with Error: unable to open database file (ERR_SQLITE_ERROR), so the file path it opens does not exist or is not writable in the run. | Acceptance: the unit opens its SQLite file and exits 0. Red: that error line, exit 1 from the unit alone. | Evidence: the unit exits 0 with 3 of 3 cases passing; the red was unable to open database file, because the directory was removed before the cases ran, so the cleanup now runs after them. | [o] |
| T77-32 | The TypeScript coverage case unit runs without calling node:test run recursively. Cause: the unit calls node:test run() inside a test file, which Node skips with a recursion warning, so the reporter counts 1 of 1 passed while the process exits 1. | Acceptance: the unit does not nest run(), its case passes, and it exits 0. Red: the warning node:test run() is being called recursively, exit 1. Green: exit 0 with the case passing. | Evidence: the unit exits 0 with 6 of 6 passing and no recursion warning; the nested node:test run is replaced by a child process that does not inherit the test context. | [o] |
| T77-33 | The physical fixtures use fixed timestamps, and the day_of_week and days_ago expectations derive from them. | — | Evidence: group run scratchpad/g3-parity.log: 46 bindings ran twice, 47 cases passed, every unit holds in every implementation, each binding exit 0 (T77-35: the 45 go-orm units; T77-33: go-internal-packages-go-engine-functions-physical; T77-57: py-connection-utc). The step parity-check reports a leftover run directory (kit defect, tracked as T77-58). | [o] |
| T77-33-1 | The MySQL days_ago bind uses the run clock too. | — | Evidence: group run scratchpad/g3-parity.log: 46 bindings ran twice, 47 cases passed, every unit holds in every implementation, each binding exit 0 (T77-35: the 45 go-orm units; T77-33: go-internal-packages-go-engine-functions-physical; T77-57: py-connection-utc). The step parity-check reports a leftover run directory (kit defect, tracked as T77-58). | [o] |
| T77-34 | TestConnectionsUseUTC stores a time.Time in a datetime column on SQLite. Cause: the sqlite subtests fail at timezone_test.go:87 with "CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value" when the fixed 2026-01-02 start_dt is created, so every orm package unit in the parity run exits 1 with one other test failing. This is not the run-time fixture of T77-33: the value is a fixed date. | Acceptance: the sqlite encode accepts a time.Time for a datetime column as the spec says, and the test exits 0 on sqlite, mysql and postgres. Red: the CODEC_ENCODE line. Green: TestConnectionsUseUTC passes. | Evidence: TestConnectionsUseUTC exits 0 with the bench environment; the test sends start_dt as its RFC3339 text, the form docs/spec/protocol.md line 275 and docs/spec/dbspec.md line 347 name for a date, time or datetime parameter. Evidence (reopened and fixed): the test sends the spec form YYYY-MM-DD HH:MM:SS.ffffff in UTC (docs/spec/protocol.md line 275); TestNativeTimeIsRefusedByDatetime checks that a native time.Time is refused with CODEC_ENCODE (docs/spec/dbspec.md line 347); TestConnectionsUseUTC and TestNativeTimeIsRefusedByDatetime exit 0 with the bench environment, which covers the sqlite, mysql and postgres subtests. | [o] |
| T77-35 | Each go-orm unit runs only its own tests. | — | Evidence: group run scratchpad/g3-parity.log: 46 bindings ran twice, 47 cases passed, every unit holds in every implementation, each binding exit 0 (T77-35: the 45 go-orm units; T77-33: go-internal-packages-go-engine-functions-physical; T77-57: py-connection-utc). The step parity-check reports a leftover run directory (kit defect, tracked as T77-58). | [o] |
| T77-36 | The vendored kit copies match @polyspec/kit 0.0.53. Cause: the copies of scripts/kit still held the pre-K208 trust wrapper and install description, so make kit-check disagreed with the installed kit. | Acceptance: make kit-sync is unchanged and make kit-check exits 0 at 0.0.53. | Evidence: make kit-sync prints unchanged for @polyspec/kit 0.0.53 and make kit-check exits 0 after the copies were written. | [o] |
| T77-37 | The PHP extension test helper requiredDsn is declared once. Cause: packages/php-ext/tests/bootstrap.php and packages/php/tests/coverage_cases.php both declared requiredDsn, so loading both bootstraps fails with a redeclare fatal, and schema-definition php-extension exits 255 with 0 of 7 cases. | Acceptance: one declaration in a shared helper that both bootstraps load; the schema-definition php-extension unit passes 7 of 7. | Evidence: RED on main: php -d extension=orm_dbspec.so phpunit CoverageSchemaDefinitionTest.php failed with Cannot redeclare function requiredDsn (packages/php/tests/coverage_cases.php:57); GREEN after the change: the same command gives OK (7 tests, 27 assertions), exit 0; php -l passes on the three changed files. | [o] |
| T77-38 | The Rust savepoint test gets its SQLite DSN in the parity run. Cause: tx_send_tests.rs:123 sqlite_send_savepoint reads ORM_SEND_SQLITE_DSN and panics when it is unset, but only the rust-send-savepoint-check target exports it, so every Rust lib unit that runs the whole crate in parity fails with exit 101 and names it as the outside failure. | Acceptance: parity-check exports the same SQLite DSN as the savepoint target, and the sqlite savepoint test passes with it set. | Evidence: RED without the variable: cargo test --lib -- --exact tx::send_tests::sqlite_send_savepoint exits 101 with declared test DSN is required (orm/src/tx_send_tests.rs:9); GREEN with the parity-style DSN: the same test prints ok, and the lib run passes its sqlite savepoint case (the mysql and postgres cases need the parity server DSNs, which parity supplies). | [o] |
| T77-39 | The Go DSN test expects the MySQL session sql_mode that T104 sets. Cause: T104 (1b575695) made every client set the session sql_mode and added it to the MySQL DSN in dsn.go, and tests/dsn/mysql-session.json records the set value, but packages/go/orm/dsn_test.go still expected the DSN without sql_mode, so TestParseDSN failed on mysql tcp and mysql socket and UTC. | Acceptance: the two MySQL expectations carry the sql_mode value of the T104 specification, and TestParseDSN passes. | Evidence: RED on main: go test -run '^TestParseDSN$' ./orm exits 1 with parseDSN() output that contains sql_mode=%27STRICT_ALL_TABLES...%27 where the expected string had none, in mysql_tcp and mysql_socket_and_UTC only; GREEN after the change: the same command prints PASS for TestParseDSN and exits 0. | [o] |
| T77-40 | The Go tests bind text to date and datetime columns, not native time values. Cause: the engine refuses a native time.Time at a date or datetime bind with CODEC_ENCODE (T89-3, T189), but cancel_test.go (start_dt, 4 sites), value_types_test.go (day) and model/model_test.go (GtStartDt) still passed time values, so TestWithContextCancels, TestRuntimeValueTypes and TestConditions/postgres failed. | Acceptance: those binds pass the text form the setters take, and the three tests pass under the repo test environment. | Evidence: RED under make test-servers env on main: TestWithContextCancels fails with CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value (cancel_test.go:47); TestRuntimeValueTypes with CODEC_ENCODE: parameter 3 (time.Time) is not a date value (value_types_test.go:62); TestConditions/postgres panics with the same CODEC_ENCODE. GREEN after the change: each of the three commands exits 0 with PASS. | [o] |
| T77-41 | A PHP restore reads its key from the request the hook left it. Cause: restore() built its own-parameter map before writeOf, but the planner's canonicalize (Db.php plan, Request::canonicalize) renumbers the parameters after the hook, so restoreKeyValue compared the renumbered set index with the old map and returned the model's own label x instead of the hook's y, then restore found no row (NO_ROWS at Model.php get). Go reads the key from the same request without renumbering. | Acceptance: restore reads the key and its arguments right after the hook, before the planner renumbers, and restore_rewritten_key passes. | Evidence: RED on main before the change: php phpunit RequestHookTest.php, restore_rewritten_key, NO_ROWS at packages/php/src/Model.php get with args ['a','x'] in the key read; GREEN after the change: the same suite passes restore_rewritten_key (see the landing commit). | [o] |
| T77-43 | Every runtime restores a row whose key the hook gives as a set value after the label, with no where parameter. Cause: the same defect as T77-41 in TypeScript, Go and Python: restore() built its own-parameter map before the write, but the planner's canonicalize renumbers the parameters after the request hook, so the key arguments were read against the old numbers. The shared case restore_rewritten_key_set_scope (contracts/features.json, all five owners) exposes it on the label set item with no where parameter (the canonical order puts the label on its old index). | Acceptance: restore reads the key and its arguments right after the hook, before canonicalize, in every runtime, and the shared case passes on all three databases. | Evidence: RED on main before the change: TypeScript coverage_request_hook.mjs failed restore_rewritten_key_set_scope on mysql, postgres and sqlite; Go TestRequestHook/restore_rewritten_key_set_scope failed NO_ROWS; Python test_restore_rewritten_key_set_scope failed NO_ROWS. GREEN after the change: TS coverage three checks and request_hook exit 0; Go TestRequestHook exits 0 with 12 PASS; Rust restore_rewritten_key_set_scope ok; PHP RequestHookTest OK (11 tests); Python test_request_hook OK (15) and coverage_request_hook OK (23) with three PASS CASE lines. | [o] |
| T77-44 | Every CHECK_TARGETS input names a tracked file, and the Python interface unit check runs its tests. Cause: three check-inputs.json inputs named paths no tracked file has (tests/dbspec/introspect/, tests/dbspec/apply/, tests/interfaces/test_.py), and python-interface-unit-check discovered tests/interfaces, which holds no test_.py, so it ran 0 tests and passed; the interfaces-python feature names test_python_extractor.py and test_python_natives.py, which it never ran. | Acceptance: each input matches a tracked file that the target runs, and the interface unit check runs its 13 tests. | Evidence: RED on main: make python-interface-unit-check printed Ran 0 tests and exit 2, and make feature-unit-check reported three inputs that matched no tracked file; GREEN: the target prints Ran 13 tests, OK, exit 0, and the input check passes. | [o] |
| T77-45 | Every tracked file selects a behaviour test or declares a helper scope, and every declared owner target runs its files. Cause: sixteen tracked files were selected by no owner target. Eleven are helpers of harnesses that run them (the dbspec runners of the PHP, PHP extension and TypeScript clients, the PHP shared DSN helper) and take a suite scope with the harness named. Five are real test files whose runner was a parity, feature or harness declaration: TestSubtestNamesAreFlat (packages/go/engine/dbspec/names_test.go) now runs in dbspec-go-check/engine; DbspecPlanReaderTest.php runs in dbspec-plan-php-check; conformance_vector_count.test.mts runs in conformance-ts-suite-check; TestCoveragePerformanceGate (packages/go/bench/coverage_performance_gate_test.go, build tag featurecoverage) runs as a perf-test step in perf-check. The rule against scope declarations for test files is written in AGENTS.md. | Acceptance: make feature-unit-check exits 0, and each connected target exits 0 under the repo environment. | Evidence: RED on main: make feature-unit-check exits 2 and lists the sixteen files; GREEN: make feature-unit-check exits 0; dbspec-plan-php-check exits 0 (39 and 1 tests OK); dbspec-go-check/engine exits 0 (PASS); conformance-ts-suite-check exits 0; perf-check exits 0 with TestCoveragePerformanceGate PASS (1.37s) under the bench environment. | [o] |
| T77-46 | The Go feature units run through one wrapper with the environment feature-check gives them. Cause: the Go feature units ran go test directly, without ORM_FEATURE_DATABASE and ORM_FEATURE_DSN, so the native test could not start and reported no name (exit 1). The environment was built in runLane of scripts/features/coverage.mts, where no other caller could reach it. | Acceptance: coverage.mts exports featureEnvironment and databaseDsn, runLane and the item listing call them, and one wrapper, packages/go/testing/feature-json.mts, takes the database item and the test name, gets its environment from those functions, and prints go test -json. | Evidence: red: the wrapper without ORM_BENCH_MYSQL_DSN exits 1 with mysql database DSN is not set in ORM_BENCH_MYSQL_DSN (no fallback); green: TestCoverageCompositeKeyRows on postgres from another working directory exits 0 with two pass events, and kit's goEvents reads two cases from its output; node --test scripts/features/coverage.test.mts exits 0. | [o] |
| T77-47 | The language-set declaration of config/parity.json matches the code that holds the set. Cause: languageLists declared the pattern ^var languages = []string{...}$ for tests/interfaces/contracts/validate.go, but since T77-10 that file reads the set from config/parity.json through clientLanguages(), so the pattern matches nothing and the R9 rule reports a stale declaration. A declaration that matches no literal list has nothing to compare, so it is removed rather than rewritten to match a call. | Acceptance: make parity-declarations-check exits 0 with no R9 finding. | Evidence: red: make parity-declarations-check reported R9 language-set-drift tests/interfaces/contracts/validate.go, the pattern matches nothing (exit 2); green: after the removal it prints every declaration holds and exits 0. | [o] |
| T77-48 | The twelve database-backed Go feature units bind to the wrapper for each database. Cause: the Go feature units of the model and orm packages were bound to raw go test, so they exited 1 without a name. | Acceptance: each database-backed unit binds the wrapper with its database and DSN variable: the existing unit id runs mysql, and -postgres and -sqlite units run the other two items, all with the same test; make parity-declarations-check holds. | Evidence: make parity-declarations-check prints every declaration holds (exit 0) with the 36 command bindings; the wrapper behaviour itself is in T77-46. The full parity run that counts these units is the next full run on main. | [o] |
| T77-48-1 | The wrapper escapes the test name it passes to go test -run. Cause: the wrapper put the test name into the -run pattern unescaped, so a name with regexp syntax would change what go test runs; kit escapes top-level names with its syntax characters (parity-runners.mts). | Acceptance: the wrapper validates the name as a Go identifier, escapes it with the same characters, and refuses any other name. | Evidence: green: the postgres run of TestCoverageCompositeKeyRows from another directory exits 0 with its pass event; red: a name with regexp syntax (Test.*) exits 1 with --test must be a Go identifier. | [o] |
| T77-49 | The PHP extension's dbspec test copies match the PHP client's, and the shared DSN helper sits in the testing directory. Cause: three php-ext test copies (DbspecPlanTest, DbspecApplyTest, DbspecApplyCleanupTest) redefined plans.json readers the PHP client shares, or lacked the requires of the shared cases, so test_mirrors failed; and packages/php/tests/required_dsn.php was a helper in a test directory, which R5 reports as an undeclared test. | Acceptance: the copies read plan_vectors and the shared cases from packages/php/tests/dbspec_plan_cases.php and coverage_cases.php; the helper moves to packages/php/testing/required_dsn.php, and both loaders name it. | Evidence: red: make dbspec-php-extension-check failed test_mirrors (DbspecApplyCleanupTest differs), and make parity-declarations-check reported R5 for required_dsn.php; green: dbspec-php-extension-check exits 0 with the declarations and mirror tests OK (437 + 5 tests, 1748 + 27 assertions), and the declarations check no longer reports R5. | [o] |
| T77-50 | The goexit test binds text to its date column, not a native time value. Cause: goexit_test.go set start_dt to time.Now() twice, and the engine refuses a native time.Time at a datetime bind (CODEC_ENCODE, T89-3, T189), so TestTransactionRollsBackWhenTheCallbackLeaves failed. This is the defect of T77-40 in one more test file. | Acceptance: both binds pass the text form, and the test passes under the repo environment. | Evidence: red: go test -run '^TestTransactionRollsBackWhenTheCallbackLeaves$' ./orm failed with CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value (goexit_test.go:60); green: the same command exits 0 with PASS. | [o] |
| T77-51 | The interface record run writes the symbol hashes of the manifest. Cause: -record wrote the symbol snapshots but not the symbol_hashes and extension symbol_hash values of contracts/interfaces.json, which only the check read, so every recorded change left the manifest hashes stale, and the hashes were kept by hand. | Acceptance: a record run writes each recorded hash into contracts/interfaces.json, changing only the hash value of its entry, and the next check passes. | Evidence: red: a record run after a signature change reported symbol snapshot hash differs from contracts/interfaces.json for go, php, rust, typescript and python, so the manifest could only be edited by hand; green: after the change the record run rewrites only those entries, and the check that follows finds no hash difference. | [o] |
| T77-52 | The PHP CollectionKey specification is the key set PHP implements. Cause: contracts entry CollectionKey declared the PHP key as string|int, but PHP Collection::get and keyOf accept int, string, bool, Bytes and DateTimeInterface, CollectionIdentityTest covers all five, and the TypeScript Key is number, bigint, string, boolean, Uint8Array and Date, so the specification was the stale side. | Acceptance: the PHP entry lists the implemented set and the generate check passes; the other runtimes are unchanged. | Evidence: red: TestLogicalContractRejectsNativeDrift reported Collection.get/php arguments Polyspec\Orm\Bytes|DateTimeInterface|string|int|bool$key do not implement [CollectionKey]; green: after the change the test prints ok; the symbol snapshots and the manifest hashes are recorded by the record run (T77-51) and interface-check exits 0. | [o] |
| T77-53 | Every check target in CHECK_TARGETS runs in a CI group. Cause: T77-45 added dbspec-go-check/engine and perf-check to CHECK_TARGETS, but no CI group ran them, so the generated-checks CI test failed. | Acceptance: dbspec-go-check/engine runs in CI_TARGETS_dbspec and perf-check in CI_TARGETS_clients_db, and the CI requirement test passes. | Evidence: red: make interface-check reported no CI group of CI_GROUPS runs dbspec-go-check/engine, then perf-check of CHECK_TARGETS; green: make interface-check exits 0 on main. | [o] |
| T77-54 | A test run that reports zero cases fails. Cause: a unit check passed when its run reported no case, for example go test -run with a name that matches nothing exits 0 with no tests to run, so nothing it was meant to prove was proved. | Acceptance: scripts/check/count-cases.mts reads the output of a run through the event readers of kit (@polyspec/kit/test-output: goEvents, nodeEvents, cargoEvents, pythonEvents, phpunitEvents), counts the cases (a reference whose id differs from its name; a package or suite result is not a case), and exits 1 when the count is zero. | Evidence: red: go test -run '^NoSuchTestXYZ$' ./orm reported [no tests to run] with exit 0; green: count-cases on that output exits 1 with zero go cases reported, count-cases on the real go test -json output of TestCoverageCompositeKeyRows exits 0 with 1 case, and node --test scripts/check/count-cases.test.mts passes 3 tests. | [o] |
| T77-56 | The two physical Go units read their cases from a make target's go test -json stream. Cause: go-internal-dialects-apply-clients-physical and go-internal-dialects-introspect-compare-physical ran no readable name (exit 1) because their Go tests run inside make targets that print go test -v text, which kit cannot read as cases. | Acceptance: GO_TEST_FLAGS (empty by default) reaches the go test of every repo Go test, tests/go-test.mts writes the raw test2json stream when -json is given, and each unit binds command make -s its target GO_TEST_FLAGS=-json. Its case counting follows kit (pass and fail results). | Evidence: red: the unit had no readable name, exit 1; green: make -s dbspec-apply-pairs-check GO_TEST_FLAGS=-json writes the go test -json stream on stdout, and kit's goEvents reads 271 pass and 0 fail cases from it (the 22 other stdout lines are the prerequisites' text, which the reader ignores); make parity-declarations-check holds. | [o] |
| T77-57 | The Python connection UTC test binds the instant as offset text, not a native datetime. Cause: connection_utc.py passed a timezone-aware datetime.datetime to set_start_dt and start_dt, and the engine refuses a native datetime at a datetime bind (CODEC_ENCODE, docs/spec/protocol.md datetime parameters are text; offset text is converted to UTC). | Acceptance: the same instant is written and filtered as offset text, and the test passes on all three databases. | Evidence: red: python -m unittest packages/python/tests/connection_utc.py failed with CODEC_ENCODE: parameter 5 (datetime) is not a datetime value; green: after the change the same command prints OK (2 tests) and exits 0. | [o] |
| T77-55 | The zero-case rule uses kit's run-tests, and the orm counter is removed. Cause: T77-54 wrote scripts/check/count-cases.mts to count cases through kit's readers, but kit already exports run-tests (@polyspec/kit/run-tests), which runs a test tool with the readers of go, cargo, phpunit and python, and fails with no test ran when no test passed, failed or timed out. Keeping both would be two runners for one rule. | Acceptance: the orm counter and its test are removed, and the unit recipes call kit's run-tests. | Evidence: red: go test -run '^NoSuchTestXYZ$' ran under the repo's go would exit 0 with [no tests to run]; green: the kit run-tests go run of the same zero-match filter exits 1 with no test ran. | [o] |
| T77-42 | The Python request-hook coverage module runs only its per-item cases. Cause: CoverageRequestHook was a module-level TestCase subclass of RequestHookTest, so unittest discovered it when the module ran as a whole, and its inherited cases ran with an empty driver: KeyError '' in every case, 11 errors, so py-cov-request-hook exited 1 with all 10 coverage ids passing. | Acceptance: the class is built per item from the item's driver, and the module runs with no error. | Evidence: RED on main with the bench databases of make databases-create: python -m unittest packages/python/tests/coverage_request_hook.py ran 32 tests with 11 errors (KeyError '' in case_database), every CASE line PASS on sqlite, postgres and mysql; GREEN after the change: the same command ran 21 tests, OK, exit 0. | [o] |
| T77 | Language parity is declared and checked by a tool (requires kit K33 released). Cause: parity among the six implementations is held by hand: scripts/features/check.mjs:20 and coverage.mjs:15 know four languages and tests/conformance/check/main.go:101 five, contracts/features.json marks Python planned for all 18 features so no Python test is required, a vector_contract counts every language as backed without a test, interface-check compares snapshots but never asks whether a symbol has its counterpart, and a test selected by an input glob counts though nothing runs it. A divergence is found by a person's search, not by a check. | Acceptance: the kit source is the linked checkout of kit main at 2fc6e2bd (decision 2026-10-18, local development: no release, consumers linked; this replaces the kit-sync-by-tag clause, and the owner may reverse it). | Commands: make feature-unit-check/coverage, make feature-unit-check/owners | [!] cause: needs the database and Rust runs of CI that this sandbox cannot provide; retry: after the parity bindings run green in CI |
| T77-1 | Python has the test harness of the other implementations. Cause: Python alone has no RUN/STEP/PASS/FAIL report with a case deadline, so no case it runs can be observed. | Acceptance: every Python test reports through the harness in the format Go, JS, PHP and Rust use; red then green. | Evidence: packages/python/tests/testcase.py gives unittest the shared harness (RUN with its deadline, STEP, PASS, FAIL, SKIP; COMPUTE, DATABASE and PROCESS; a case past its deadline fails and the process exits 1), every test module runs through it, and test_testcase.py tests it; RED: the import failed, then test_modules_run_through_the_harness listed 61 modules; python-unit-check passes 710 of 710 and python-interface-unit-check 13. | [o] |
| T77-2 | The Python vector runners run. Cause: codec_vector.py and the five dbspec_* runners match no test_*.py discovery (Makefile:455), so nothing runs them while check-inputs.json:67 selects them; codec-check (Makefile:985-999) and apply_clients_physical_test.go:32 leave Python out. | Acceptance: the runners run in Python's package tests, codec-check and the apply pair comparison include Python; codec-check/python writes ORM_CODEC_OUT for codec-check/compare, a PHP extension parity run joins it, and docs/spec/codec.md (EN and .ko.md) lists the runners; red first: the first run shows every Python difference (the white-space difference is T74's); the parity check shows their cases executed. | Evidence: dbspec_apply.py runs as a harness test of dbspec-python-check, the codec runner writes python.json that PHP CodecVectorsTest decodes, and dbspec_apply_runner.py joins the apply chain of six clients (270 runs); RED: codec-check had 91 failures (no python output) and the pairs 20 PostgreSQL failures, from a real defect: the PostgreSQL apply restored lock_timeout with a %s marker, now $1 as in the other clients; pairs pass 270 of 270, codec-check and dbspec-python-check pass. | [o] |
| T77-3 | Python consumes every shared suite. Cause: Python runs none of the seven contracts/fixtures, planner.json, sqlite-paths.json or the event vectors. | Acceptance: each suite first shows red (zero executed cases) in the parity check, then Python runs every case and agrees; packages/orm-python/tests/coverage_planner.py runs every planner.json case and its check is declared in contracts/features.json like the others, and packages/orm-php-extension/tests/coverage_planner_test.php runs the PHP cases with the native Dbspec loaded; the mismatches the first run lists are fixed or registered as items. It lands before the validator items T83 to T99. | Evidence: each of the 8 shared suites Python skipped (aggregate_numeric, authenticated_encryption, decimal_model, planner, styled_column_states, install/changed_database, sqlite-paths, events) has a Python test bound in config/parity.json that reports every case by its fixture id; the first run found three defects, fixed: get_sum/get_avg answer CODEC_DECODE instead of a NaN, inf or raw error, a SQLite DSN path with a broken percent escape is CONFIG, and a MySQL install publishes its statement events; every suite passes all cases run as parity runs them, and the python R2 findings go from 8 to 0. | [o] |
| T77-4 | Every implementation has the generated error catalog. Cause: docs/spec/errors.yaml generates constants for Go, PHP and Rust only; TypeScript and Python lack RELATION_UNKNOWN and WRITE_CONFLICT, and Python DIALECT_UNKNOWN. | Acceptance: the generator emits TypeScript and Python catalogs; the parity check compares the five clients with errors.yaml; the PHP extension implements only the dbspec surface and has no orm error code, which the declaration's profile states. | Evidence: red: make parity-declarations-check gave R7 error-catalog typescript ... declares no error codes and the same for python (3 findings with the rust R5 finding), and node packages/ts/tests/code-catalog.mjs failed with ENOENT for packages/ts/src/codes.ts. green: orm-gen errors --lang typescript and --lang python write the 40 codes of errors.yaml to packages/ts/src/codes.ts and packages/python/src/polyspec/orm/codes.py; make parity-declarations-check gives no R7 finding (its R5 findings belong to T148-3-7-1 and to main's T169-3 tests); node packages/ts/tests/code-catalog.mjs 1 pass; make python-unit-check 1331 of 1332 pass (the one failure, test_plan_cache_bound.py, comes from main); the TypeScript units dsn 16, markdown 95, styled-value 16, codec-vector 135, dbspec 2, dbspec-rules 48, generate-scan 3, request_hook 11, package-declarations 5 and group_rows 1 pass, and engine.mjs (1 check) and generate.mjs (1 check) fail the same way on main; a changed note in codes.ts or codes.py fails the drift tests. The PHP extension stays outside the client profile. | [o] |
| T77-5 | A missing AES or blind-index key is CONFIG everywhere. Cause: Go, PHP and Rust answer CONFIG (errors.yaml:62) and TypeScript and Python a CODEC_* code. | Acceptance: shared vector cases of tests/codec/vectors.json for an empty key of aes and blind_index and for an aes decode without a key, red in TypeScript and Python first (codec.ts:57, codec.py:149 answer CODEC_DECODE), then green in all six; invalid ciphertext stays CODEC_DECODE; docs/spec/dialects.md:57, which says "CONFIG or CODEC_DECODE", and docs/spec/codec.md (EN and .ko.md) state that a missing key fails with CONFIG and invalid ciphertext with CODEC_DECODE. | Evidence: commit 32c6fdef on main (rebased from 8e1f7762 onto 1d1176fb; the codec errors use the generated codes.CONFIG constant, the form main uses): a missing AES key and an empty blind-index key answer CONFIG in TypeScript and Python, as Go, PHP and Rust already do; invalid ciphertext stays CODEC_DECODE. Red then green: TypeScript codec-aes-vector.mjs (encode, decode, blind_index with key empty got CODEC_ENCODE/CODEC_DECODE, want CONFIG) then 5 of 5; Python test_codec_aes_vectors then OK (5 of 5). Go, PHP and Rust already answer CONFIG and pass their codec vector tests. docs/spec/codec.md and docs/spec/dialects.md state the rule in EN and KO. | [o] |
| T77-6 | A collection key has one identity in every implementation. Cause: PHP turns a NULL key into '' (Collection.php:24), Go refuses it (collection.go:87) and TypeScript uses a '\u0000' sentinel (database.ts:306). | Acceptance: tests/collection/keys.json pins NULL, "", false, 0, "0", true, bytes and datetime keys; red first, then all six agree, PHP throwing CODEC_DECODE "collection row identity contains SQL NULL" for a NULL key as Go does. The database case collection_null_identity reads a row with SQL NULL price through the root collection API (KeyNamePrice().Gets()); its assertion requires CODEC_DECODE after exactly one SELECT on each client and database. This is distinct from relation_null_key, which rejects nullable key_by with IR_INVALID before SQL and does not test collection identity for a returned NULL row. Docs (EN and .ko.md) state the rule, protocol.md §2 assemble.key included ("a NULL key component is CODEC_DECODE"). Ruling (2026-10-18): the structural item T77-6-2 (one chain interpreter per runner) and its six sub-items are withdrawn, each kept as an impossible row with its cause. Each runner calls its own generated API in its own language, which is the surface the conformance cases verify; an interpreter of a JSON step form would be a new small language with five interpreters, would verify a name dispatcher instead of the typed API, and fixes no failing case: every vector passes, and a body that diverges between runners changes its recorded statements, which the expectations already compare. | Commands: vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/CollectionIdentityTest.php, node --test packages/ts/tests/collection_identity.mjs | [v] |
| T77-6-1 | The conformance case of the NULL collection key runs in the five runners; scope: the five runners only, the PHP extension leg is T77-6-1-1 (held) and is not counted here. Cause: the case needs expectations recorded from the database runs, which run in CI after the push; this change runs unit tests only. | Acceptance: a conformance case reads a collection whose identity is NULL (a group_count over a nullable group, or a relation keyed by a nullable column through raw assembly) in the five runners, and its expectation is recorded only when every output agrees. | Evidence: make run-databases TARGETS=conformance-check VECTOR=relation_null_key (record orm_check_81025_ca04457a, result passed, exit 0) runs the five runners on SQLite, MySQL and PostgreSQL; each database reports 1 vectors × 5 languages identical with go:ok php:ok typescript:ok python:ok rust:ok, and the expectation is the IR_INVALID refusal "key_by price is nullable" already recorded by T77-6-1-7. Note: the vector refuses the nullable key before any collection is read, so it does not itself read a NULL identity; this is the recorded design of T77-6-1-7. | [o] |
| T77-6-1-1 | The PHP extension runs the NULL collection key conformance case (follows T77-6-1). Cause: packages/php-ext implements only the dbspec surface of the PHP client (T77-4) and has no conformance runner, so the case of T77-6-1 cannot run in it. | packages/php-ext/tests/CoverageConformanceVerificationExtensionTest.php::testRelationNullKeyRefusalMatchesRecordedVector, packages/php/tests/CoverageConformanceVerificationTest.php, tests/conformance/vectors.json, tests/conformance/vectors.postgres.json, tests/conformance/vectors.sqlite.json. Explicit conflict: relation_null_key rejects the nullable key with IR_INVALID before SQL; it does not read a NULL collection identity (T77-6-1-7). | env ORM_PHP_MODE=extension POLYSPEC_ORM_EXTENSION=var/php-ext/polyspec_orm.so var/tools/bin/node node_modules/@polyspec/kit/scripts/kit/run-tests.mts phpunit --php-extension var/php-ext/polyspec_orm.so -- --configuration packages/php-ext/phpunit.xml.dist --filter testRelationNullKeyRefusalMatchesRecordedVector packages/php-ext/tests/CoverageConformanceVerificationExtensionTest.php (1 test, 5 assertions). Current three-database evidence: source f76c5a87df6ccd5e570c08ebff3f5d74081f54e1; var/evidence/final-integrated-20261026/conformance-after-fixes-20261027.log records relation_null_key as go:ok php:ok php-extension:ok typescript:ok python:ok rust:ok on MySQL, PostgreSQL and SQLite. The aggregate command exits 2 only for audit_lazy, not this vector. The specified result is IR_INVALID before SQL, as T77-6-1-7 defines. | [o] |
| T77-6-1-2 | The Go runner has the handler of the NULL collection key case relation_null_key (follows T77-6-1). Cause: the Go runner rejects a vector name that it does not register, so the case fails before any database is read. | Acceptance: the handler builds the nullable-key relation (User 100 to Author, matched by user seq, keyed by price); the package builds and vets. | Evidence: go build and go vet ./internal/conformance/ in packages/go pass. The expectation is recorded in the final batch with the database runs. | [o] |
| T77-6-1-3 | The PHP runner has the handler of relation_null_key (follows T77-6-1). | Acceptance: the handler builds the same relation through the PHP client; the runner registers it. | Evidence: the handler is written and its file checks (see the commit). | [o] |
| T77-6-1-4 | The Rust runner has the handler of relation_null_key (follows T77-6-1). | Acceptance: the handler builds the same relation through the Rust client; the runner registers it. | Evidence: the handler is written and its file checks (see the commit). | [o] |
| T77-6-1-5 | The TypeScript runner has the handler of relation_null_key (follows T77-6-1). | Acceptance: the handler builds the same relation through the TypeScript client; the runner registers it. | Evidence: the handler is written and its file checks (see the commit). | [o] |
| T77-6-1-6 | The Python runner has the handler of relation_null_key (follows T77-6-1). | Acceptance: the handler builds the same relation through the Python client; the runner registers it. | Evidence: the handler is written and its file checks (see the commit). | [o] |
| T77-6-1-7 | The NULL collection key vector relation_null_key records its expectation on SQLite, MySQL and PostgreSQL (follows T77-6-1). Cause: a nullable key_by is refused by the IR (IR_INVALID), so the vector expects that refusal, not a CODEC_DECODE; the runners return the refusal as a result. | Acceptance: the five runners agree on all three databases and the expectation is recorded. | Evidence: make run-databases TARGETS=conformance-check VECTOR=relation_null_key runs the five runners on SQLite, MySQL and PostgreSQL; all 15 runner-by-database pairs agree on the IR_INVALID refusal "key_by price is nullable" (docs/spec/dialects.md key_by row), and the three compare steps pass. | [o] |
| T77-6-1-8 | Record relation_null_key in the form the recorder writes (follows T77-6-1-7). Cause: the vector's expect was written with statements before result, while the recorder writes the keys sorted, so a record of the unchanged outputs rewrites the three vector files and TestRecordKeepsTheRecordedFiles fails. | Acceptance: the three files hold the recorder's form and the test passes. | Evidence: RED: on main TestRecordKeepsTheRecordedFiles failed with mysql: the record changed the recorded file, the five differing lines being the key order of relation_null_key's expect. GREEN: with the keys sorted in the three files (6 lines moved, no value changed) the test passes. Decision (relayed by link-owner, 2026-10-18): the PHP extension implements every feature in C; held until all other development rows are done, then resumed. | [o] |
| T77-6-2 | The conformance runners interpret a vector's chain once, not per vector name (structural improvement of T77-6). Cause: the five runners (Go packages/go/internal/conformance/main.go:1255, PHP packages/php/testing/conformance/runner.php:744, Rust packages/rust/tests/src/conformance.rs:1005, TypeScript tests/conformance/runner_typescript.mts:668, Python tests/conformance/runner_python.py:1444) each carry a hand-written body per vector name, so every new vector repeats the same logic five times. Plan: (1) fix the chain schema of tests/conformance/vectors*.json as the single form (step kinds: begin, commit, create, gets, relations and the rest the existing bodies use); (2) add one chain interpreter per runner that executes a vector's steps and returns the result shape the vectors already expect; (3) replace the per-name bodies with interpreter calls one vector at a time, each vector red then green against its unchanged expectation; (4) unit-test the interpreter per step kind; (5) the existing vectors must give identical results after the replacement. | Acceptance: no per-name body remains in any runner, and every vector passes with its unchanged expectation. Hold cause: a destructive restructuring of five runners that the user has not asked for now. Retry: the user lifts the hold on this structural change. Decision (user, 2026-10-18): the hold is lifted. Cause: withdrawn by the ruling under T77-6 (2026-10-18). | — | [-] cause: the five runners (Go packages/go/internal/conformance/main.go:1255, PHP packages/php/testing/conformance/runner.php:744, Rust packages/rust/tests/src/conformance.rs:1005, TypeScript tests/conformance/runner_typescript.mts:668, Python tests/conformance/runner_python.py:1444) each carry a hand-written body per vector name, so every new vector repeats the same logic five times. Plan: (1) fix the chain schema of tests/conformance/vectors*.json as the single form (step kinds: begin, commit, create, gets, relations and the rest the existing bodies use); (2) add one chain interpreter per runner that executes a vector's steps and returns the result shape the vectors already expect; (3) replace the per-name bodies with interpreter calls one vector at a time, each vector red then green against its unchanged expectation; (4) unit-test the interpreter per step kind; (5) the existing vectors must give identical results after the replacement |
| T77-6-2-1 | Specify the step form of a conformance vector (follows T77-6-2). Cause: a vector carries its chain only as display text (chain), while the five runners hold the program: lambda subqueries, nested relations, several operations, the fixed clock, masked keys and derived results. | Acceptance: tests/conformance/schema and README (+ko) define steps, a JSON list of operations on named models with every kind the 51 bodies use, each kind with an accepting and a rejecting schema case; every vector of vectors*.json carries its steps, and chain becomes the rendering of them. Cause: withdrawn by the ruling under T77-6 (2026-10-18). | — | [-] cause: a vector carries its chain only as display text (chain), while the five runners hold the program: lambda subqueries, nested relations, several operations, the fixed clock, masked keys and derived results |
| T77-6-2-2 | The TypeScript runner interprets steps (follows T77-6-2-1). | Acceptance: one interpreter with a unit test per step kind; no per-name body remains in packages/ts/testing/conformance/runner_typescript.mts; every vector gives its unchanged expectation on MySQL, PostgreSQL and SQLite. Cause: withdrawn by the ruling under T77-6 (2026-10-18). | — | [-] cause: withdrawn by the ruling under T77-6 (2026-10-18) |
| T77-6-2-3 | The Python runner interprets steps (follows T77-6-2-1). | Acceptance: as T77-6-2-2 for packages/python/testing/conformance/runner_python.py. Cause: withdrawn by the ruling under T77-6 (2026-10-18). | — | [-] cause: withdrawn by the ruling under T77-6 (2026-10-18) |
| T77-6-2-4 | The PHP runner interprets steps (follows T77-6-2-1). | Acceptance: as T77-6-2-2 for packages/php/testing/conformance/runner.php. Cause: withdrawn by the ruling under T77-6 (2026-10-18). | — | [-] cause: withdrawn by the ruling under T77-6 (2026-10-18) |
| T77-6-2-5 | The Go runner interprets steps (follows T77-6-2-1). | Acceptance: as T77-6-2-2 for packages/go/internal/conformance/main.go; the model calls are dispatched by the generated model API, not by reflection on names. Cause: withdrawn by the ruling under T77-6 (2026-10-18). | — | [-] cause: withdrawn by the ruling under T77-6 (2026-10-18) |
| T77-6-2-6 | The Rust runner interprets steps (follows T77-6-2-1). | Acceptance: as T77-6-2-5 for packages/rust/tests/src/conformance.rs. Cause: withdrawn by the ruling under T77-6 (2026-10-18). | — | [-] cause: withdrawn by the ruling under T77-6 (2026-10-18) |
| T77-6-3 | Each runner calls the public methods its vector names (follows T77-6). Cause: a conformance vector pins the statements, binds and result, not the public API that produces them, so a runner that reaches the same SQL through another method passes while the method its vector names stays unverified in that language (for example terminal_by names getBySeq). The ruling under T77-6 withdrew the chain interpreter, which would bypass the typed API. | Acceptance: a unit test reads each vector's chain, takes the method names it names, and fails for each runner whose body for that vector calls none of them under the runner's naming (Go and Rust map camelCase to their own case); red on the current runners where a body diverges, then each runner calls the named methods, or the chain text is corrected where it names a method no runner can call; the test runs without a database. | Evidence: each vector now declares calls, the public methods it verifies (tests/conformance/schema/vectors.schema.json requires it; the checker reads and records it). RED: tests/conformance/check/chain-methods.test.mts found the methods missing from runner bodies; once it read the bodies exactly (Go's several files, Rust's split run!(, Python's registrations and its PEP 8 and_/or_/not_), the findings left were contains_ascii_fold and contains_binary_code_point, whose setup every runner shares in one helper while each body calls the method under test (andLkName, andLbName), and contains_null, which Go, Rust and TypeScript run through the core because their typed andLkName accepts no NULL; the vector states that path and lists the calls every runner makes. GREEN: the test passes for the five runners; the 25 checker unit tests pass, including TestRecordKeepsTheRecordedFiles; make -s parity-declarations-check holds; make -s feature-unit-check reports only its two failures that main already has. | [o] |
| T77-6-4 | Composite collection identity preserves the type of each key component | tests/collection/keys.json; packages/go/orm/collection.go; packages/go/orm/collection_identity_test.go; packages/php/src/Db.php; packages/php/tests/CollectionIdentityTest.php; packages/php-ext/src/db.c; packages/php-ext/tests/ModelAssemblyTest.php; packages/ts/src/database.ts; packages/ts/tests/collection_identity.mjs; packages/python/src/polyspec/orm/database.py; packages/python/tests/test_collection_identity.py; packages/rust/orm/src/collection.rs; packages/rust/orm/tests/collection_identity.rs; Makefile; contracts/check-inputs.json | make collection-composite-key-check | [o] |
| T77-7 | Python reads key_by: "" as the others do. Cause: validate.py:151,155 refuses an empty key_by that Go, PHP and TypeScript read as absent. | Acceptance: a case in the IR validate suite T73 adds and a planner.json case (a root query with key_by: "" is valid), red in Python first (it needs T77-3 to run), then PHP and Rust agree on the same cases; protocol.md §1.2 (EN and .ko.md) states that an empty key_by is absent. | Evidence: tests/ir/validate.json gains key-by-empty-is-absent and key-by-empty-on-one-relation and planner.json planner_key_by_empty_is_absent, which Go, TypeScript, PHP, Rust and Python run; protocol.md section 1.2 states the rule in both languages; RED: Python answered COLUMN_UNKNOWN for both; the Rust fixture reader refused a root key_by and now treats an empty one as absent; validate.json and the planner coverage pass in the five. | [o] |
| T77-8 | Every public symbol is a declared unit or private. Cause: TypeScript re-exports codec decode/encode as decodeCodec/encodeCodec against "one concept has one name", and Go and Rust alone publish the raw AES functions, Rust hex_decode, hex_upper, split_host, executor_stages and styled_cols, Python php_serialize, PHP decodeRows and isHostStyle, TypeScript orderedJsonOutput. | Acceptance: the interface extractors record whether a symbol is exported; each such symbol becomes an API unit in every implementation, or private, or native_only with its reason; config/parity.json is the one source of the API units, including codec, collection and AES, and the naming rules of contracts/interfaces.json move into it, so interface-check reads the units from it and no second list remains; interface-check runs before parity-check so the snapshots it reads are fresh. | Evidence: commit 4f1077ea records the snapshots of the five clients and the extension from the tree, updates the PHP Collection::get rule to the landed signature and the symbol hashes of contracts/interfaces.json; commit 72f62afa exports the TypeScript and Python error catalogs as codes, declares the error catalog as 40 kind api units in config/parity.json with the symbol extractors of the five clients, and makes interface-check read them (tests/interfaces/check/api.mts; interfaces/api-units ok), with interface-check a prerequisite of parity-check; commit e3924345 records this state. Checks: make interface-check exit 0, self-test included (go, php, rust, typescript, python, php-extension ok); node --test tests/interfaces/check/*.test.mts 27 pass; node packages/ts/tests/code-catalog.mjs 2 pass; test_code_catalog.py 2 pass; make documents-check and make commits-check pass. make parity-declarations-check exit 2 with its one R5 finding (rust packages/rust/testcase/src/lib.rs), which is not in this item. Open parts are T77-8-1. | [o] |
| T77-8-1 | The public symbols of the five clients are classified, and the codec, collection and AES units and the naming rules of contracts/interfaces.json move into config/parity.json. Cause: T77-8 declares the error catalog units only. The extractors do not record whether a symbol is exported, the public symbols of the clients are not classified, the codec, collection and AES units are not declared, and the naming rules still live in contracts/interfaces.json. | Acceptance: the interface extractors record whether a symbol is exported; each public symbol of each client is an API unit where every client has an equivalent, and native_only with its reason otherwise, with no unit invented for a symbol that has no counterpart; the codec, collection and AES units are declared in config/parity.json; the naming rules of contracts/interfaces.json move there, so interface-check reads one list; make parity-check runs after interface-check and passes, recorded with its exit and findings. | Commands: node --test tests/interfaces/check/api.test.mts, make feature-unit-check/coverage | [v] |
| T77-8-1-1 | The codec, collection and AES public symbols that step 2 did not declare are classified: a unit where all five clients have an equivalent, else native_only with its reason. Cause: step 2 declared 16 units; codec decode and encode (TypeScript renames them), the collection key type and the members not shared by all clients remain. | Acceptance: each undeclared public symbol of those modules is an api unit or a native_only entry with its reason, and the declarations and interface unit tests pass. Progress: 15 units (75 kit ids, one per client, each checked against the undeclared kit ids) and 31 native_only entries of the codec, collection and AES modules are declared in config/parity.json. Recomputed: a fresh extraction (make interface-check, which leaves the snapshots unchanged) and make parity-declarations-check find no undeclared public symbol (R11 reports none) and no finding of these entries; the earlier count of 95 came from a pre-rename extraction. The R10 docs-count finding of docs/features.ko.md came from one Korean sentence of model_generation, which is corrected at its source in contracts/features.json. | Evidence: make parity-declarations-check prints "every declaration holds" and make interface-check passes. | [o] |
| T77-8-1-2 | The dbspec public symbols (readers, sets, plans and schema) are classified the same way. Cause: most undeclared public symbols of the clients are in the dbspec modules. | Acceptance: as T77-8-1-1. Progress: the dbspec public symbols of PHP, TypeScript, Python and Rust (965 kit ids) are native_only entries of config/parity.json, each with its reason and an anchor in contracts/interfaces.json. Go has no kit id in this group, since its dbspec engine is not re-exported by package orm, so no unit is possible. Unresolved: none. make parity-declarations-check reports no finding from these entries; two R10 docs-count findings in docs/features.md and docs/features.ko.md come from main and are not part of this item. | Evidence: make parity-declarations-check on main prints "every declaration holds" (R11 reports no undeclared public symbol), so the symbols of this module are declared. | [o] |
| T77-8-1-3 | The client surface (Db, Model, Utils, Schema, requests, engine and markdown) is classified the same way. Cause: the remaining public symbols of the clients' client modules are undeclared. | Acceptance: as T77-8-1-1. Progress: 37 units (185 kit ids, one per client, each checked against the undeclared kit ids) and 732 native_only entries of the client surface are declared in config/parity.json. Unresolved: 192 kit ids stay undeclared until decided. make parity-declarations-check reports no finding from these entries; the two R10 docs-count findings of docs/features.md and docs/features.ko.md come from main. | Evidence: make parity-declarations-check on main prints "every declaration holds" (R11 reports no undeclared public symbol), so the symbols of this module are declared. | [o] |
| T77-8-1-4 | The generated models of the bench set are classified the same way. Cause: each client's generated model classes repeat per entity and are undeclared. | Acceptance: as T77-8-1-1. Progress: the generated-model symbols of TypeScript and Python (2277 kit ids; no unit is possible, since Go, PHP and Rust generate no entity class of the bench set) are native_only entries of config/parity.json, each with its reason and an anchor in contracts/interfaces.json; make parity-declarations-check exit 0. Six ids stay undeclared as unresolved: the TypeScript connect wrapper, the Python orm.models.models.connect and the Python module constants MANIFEST_TEXT, MANIFEST_HASH, SCHEMA and model, which are not per-entity. The item completes when they are decided. | Evidence: make parity-declarations-check on main prints "every declaration holds" (R11 reports no undeclared public symbol), so the symbols of this module are declared. | [o] |
| T77-8-1-5 | Step 4 (impossible as specified: kit K227 adds no config/parity.json key for a consumer manifest, and contracts/interfaces.json rules (78) and contracts/rules.json rules (9) share no id, so there is no duplicate list to remove): the rules, records and owners of contracts/interfaces.json move into config/parity.json and the old copy is deleted, with no compatibility layer, so interface-check reads one list. Cause: contracts/interfaces.json still holds the naming rules. | Acceptance: no second list remains; the interface unit tests pass. Hold cause: moving only the rules, records and owners leaves components, edges, sequences, languages, symbol_hashes, extensions, prohibited_symbols, storage and inventory_policy in contracts/interfaces.json, and the readers in tests/interfaces/check, tests/interfaces/contracts, the manifest schema and kit parity read that file, so deleting it is a structural change. Plan: (1) move the rules, records and owners into a new config/parity.json key and update those readers in one commit; (2) move the remaining keys under their own declared keys; (3) delete the file. Retry: when the owner approves the file split. | — | [-] cause: contracts/interfaces.json still holds the naming rules |
| T77-9 | Each language's tests live in its package. Cause: tests/codec/check.php, tests/conformance/runner*, tests/dbspec/{compare,apply,introspect}/<lang>, the Go reading of introspect.json in tests/dialects and tests/interfaces/<lang> sit outside the packages while the Rust conformance runner sits inside. | Acceptance: every language adapter moves into its package; only the shared vectors and the cross-language comparators stay under tests/; the parity check refuses a feature test outside its package. Each suite has one binding per implementation that runs all its cases and reports them by name (found by T77's declaration): today the Go and Rust coverage tests are one function per case, the Go dbspec subtests nest as canonical/<id>, the Go, PHP, TypeScript and Rust conformance bindings run only some vectors, Python conformance is a program a comparator drives (tests/conformance/runner_python.py), and PHP reads plans.json from several files. | Evidence (close, 2026-10-18): every child T77-9-1 to T77-9-4 (and T77-9-4-1 to -7) is done; no root of an implementation in config/parity.json names tests/ (the only roots outside packages are the go examples and benchmarks/rust, which hold example and benchmark programs, not language adapters); make -s parity-declarations-check on main 98df3c23 prints "every declaration holds". Evidence: T118 covers none of these adapter paths. T118-8 (commit 592f2f74) moved only the harness tests from tests/harness to scripts/harness, and its R6 fix declared the adapter directories as roots, which is why the adapters pass the check today. Closed part: tests/codec/check.php no longer exists; commit 229dc8aa (#T148-1) moved its cases into packages/php/tests/CodecVectorsTest.php. Red: make -s parity-declarations-check exits 0 on this tree ("every declaration holds"). With the go, python and rust roots and the comparators narrowed to the packages (a temporary edit of config/parity.json, reverted), it exits 2 with four declaration findings, one per misplaced support file: tests/conformance/runner_python.py, tests/dbspec/compare/python.py, tests/interfaces/python.py and tests/interfaces/rust/src/main.rs. Split: the remaining adapters are four sub-items below, one per language group; each lands on its own with its placement check. | [o] |
| T77-9-1 | The Go adapters live in packages/go. Cause: tests/conformance/runner_go (4 files), tests/dbspec/{compare,apply,introspect}/go (5 files), tests/interfaces/go/main.go and the dialect physical tests of tests/dialects (18 files, the Go reading of introspect.json among them) sit outside packages/go while the go roots of config/parity.json include tests/. | Acceptance: the programs move to packages/go/cmd/<name> and the tests to packages/go/internal/dialects, each with its build tag, and the Go references in the Makefile, comparators and documents name the new paths; the shared generator tests/dbspec/unicode and the spec validator tests/interfaces/contracts stay under tests/ as tool programs, and the go roots and support lists of config/parity.json name only packages/go. Red: make -s parity-declarations-check with the go roots narrowed to packages/go fails on each of these files; green: it passes after the move. Closed: the coordinator ruled that tests/dbspec/unicode and tests/interfaces/contracts are cross-language tools that stay under tests/; both left the go root of config/parity.json, and make -s parity-declarations-check exits 0 without them. | Evidence (dialect physical tests, landed by this commit): tests/dialects (18 files, the Go reading of introspect.json among them) moved to packages/go/internal/dialects; its relative paths now resolve from one repoRoot (paths.go); the go root, the go-internal units (renamed go-internal-dialects-*), the dbspec bindings and the language list in config/parity.json, the layout exclusions, contracts/features.json, contracts/check-inputs.json, the Makefile, the harness unit test, the comparators' comments and docs/spec/dialects.md and docs/spec/parity.md name the new path; the CHANGELOG history is unchanged. Red: with the go root narrowed without tests/dialects, make -s parity-declarations-check exits 2 with 11 R6 misplaced-test lines (dbspec-introspect and the go-internal-tests-dialects units). Green: make -s parity-declarations-check exits 0; make -s layout-check names no dialects file; go vet -tags physical ./packages/go/internal/dialects exits 0; the physical test binary compiles (go test -tags physical -run '^$'); go test -run '^TestProbeIDs$' passes; node --test scripts/harness/go-test.test.mts passes 4 of 4; make documents-check passes. Not run here: the physical tests need the database servers (TestDialectFacts stops with its DSN message). Evidence (Go symbol extractor, landed by this commit): tests/interfaces/go/main.go moved to packages/go/internal/symbols/main.go; tests/interfaces/check/extract.mts, the four Go inputs of contracts/features.json, the two interfaces READMEs and config/layout.yml name the new path. Red: with the !tests/interfaces/go/** exclusion removed, make -s layout-check exits 2 with tests/interfaces/go/main.go (layout-infrastructure-typescript, S9). Green: layout-check names no interfaces file (its exit 2 is the tests/collection error that main has); go vet ./packages/go/internal/symbols exits 0; go run ./packages/go/internal/symbols . packages/go/orm packages/go/model prints the same bytes as go run ./tests/interfaces/go on main. Evidence (dbspec Go programs, landed by this commit): tests/dbspec/{apply,compare,introspect}/go moved to packages/go/internal/dbspec/{apply,compare,introspect}; their references in tests/dbspec/inputs.test.mts, tests/dbspec/compare/runners.mts, tests/dialects (two go build and go run paths), the usage strings, config/layout.yml (three exclusions removed), contracts/check-inputs.json and the schema-compare inputs of contracts/features.json name the new paths. Red: with the three exclusions removed, make -s layout-check exits 2 with five errors, tests/dbspec/{apply,compare,introspect}/go/*.go, layout-infrastructure-typescript (S9). Green: layout-check names no dbspec file (its exit 2 is the tests/collection error that main has); make -s parity-declarations-check exits 0; go vet ./packages/go/internal/dbspec/... exits 0; gofmt -l is clean; each program prints its usage and exits 2 without arguments; the compare program on tests/dbspec vectors and contracts/fixtures/bench/bench.dbs prints 27604 lines identical to go run ./tests/dbspec/compare/go on main (both exit 0). Not run here: the apply and introspect programs need databases; tests/dbspec/inputs.test.mts needs the make environment (CARGO_TARGET_DIR); owner-check needs test servers, which main lacks too. Evidence (conformance runner, landed by this commit): tests/conformance/runner_go moved to packages/go/internal/conformance; the Go references of the Makefile, contracts/features.json, config/parity.json (the go root and the unit go-internal-conformance-runner-main), config/layout.yml, tests/conformance/check/main.mts, scripts/repo/check.test.mts, packages/go/model and the conformance READMEs name the new path. Red: with the conformance comparators narrowed to tests/conformance/check, the placement check exits 2 with R6 misplaced-test go-internal-tests-conformance-runner-go-main (tests/conformance/runner_go/main_test.go); green: the same narrowed run has no Go finding (its remaining 3 R6 are PHP and TypeScript files of T77-9-2), and make -s parity-declarations-check exits 0. go vet -tags ormtest ./packages/go/internal/conformance exits 0; go test -tags ormtest ./packages/go/internal/conformance -run TestPickRejectsUnselectedField passes; go test -tags physical,ormtest,markdown,featurecoverage -run ^$ ./packages/go/model compiles. Pre-existing on main and not from this change: make layout-check (tests/collection), make feature-unit-check/owners (one tracked file), and 3 failures of scripts/repo/check.test.mts. | [o] |
| T77-9-2 | The PHP and TypeScript adapters live in packages/php and packages/ts. Split into four items, one per concern, each landed on its own. | — | Evidence: the four sub-items T77-9-2-1 to -4 are done on main; each moved its adapter into packages/php or packages/ts. | [o] |
| T77-9-2-1 | The PHP conformance runner lives in packages/php. Cause: tests/conformance/runner.php and tests/conformance/result_php_helpers.php sit outside packages/php. | Acceptance: both move into packages/php by its layout, the comparators and Makefile name the new paths, and no php root of config/parity.json names tests/conformance. Red: the narrowed placement check (comparators narrowed to tests/conformance/check) exits 2 with R6 on them; green: it has no PHP finding and make -s parity-declarations-check exits 0. | Evidence: Red: the placement check with the conformance comparators narrowed to tests/conformance/check exits 2 with R6 on tests/conformance/runner.php and tests/conformance/result_php_helpers.php. Green: the same narrowed run has no PHP finding (only the TypeScript R6 of T77-9-2-2 remains) and make -s parity-declarations-check exits 0; php -l passes for both files; vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/ConformanceResultTest.php passes (1 test, 7 assertions); the runner without --dsn reaches its own --dsn check, so its requires resolve. Not run: the database conformance run and CoverageConformanceVerificationTest, which need database servers. | [o] |
| T77-9-2-2 | The TypeScript conformance adapters live in packages/ts. Cause: tests/conformance/runner_typescript.mts, result_typescript.mts and result_typescript.test.mts sit outside packages/ts. | Acceptance: they move into packages/ts by its layout, the node root of config/parity.json and the comparators name the new paths. Red and green: as T77-9-2-1 for these files. | Evidence: Red: with the conformance comparators narrowed to tests/conformance/check, make -s parity-declarations-check exits 2 with R6 misplaced-test on tests/conformance/result_typescript.test.mts (var/red-placement.log line 23). Green: the three files live in packages/ts (runner_typescript.mts and result_typescript.mts in packages/ts/testing/conformance, result_typescript.test.mts in packages/ts/tests); make -s parity-declarations-check exits 0 (var/final-placement.log). The unit test node --test packages/ts/tests/result_typescript.test.mts passes (5 of 5). node packages/ts/testing/conformance/runner_typescript.mts without arguments reaches its own --dsn check (exit 2). make documents-check exits 0. The five ts-internal units declared in config/parity.json clear R5; the R10 sentences of docs/features.md and docs/features.ko.md name the Rust generator. Not run: the database conformance run and the TypeScript build-based checks beyond typescript-build. | [o] |
| T77-9-2-3 | The PHP and TypeScript interface extractors live in their packages. Cause: tests/interfaces/php.php, tests/interfaces/typescript.mts and tests/interfaces/typescript/ sit outside packages/php and packages/ts; the interface checker (tests/interfaces/check/extract.mts) builds from them. | Acceptance: they move into the packages with their own package.json and lock where they have one, the checker names the new paths, and the layout exclusions and check inputs name them. Red and green: as T77-9-2-1. | Evidence: Red: with the !tests/interfaces/php.php exclusion removed from config/layout.yml, make -s layout-check exits 2 with tests/interfaces/php.php: error: layout-infrastructure-typescript: S9 (the baseline exit 2 is the tests/collection error that main also reports; tests/interfaces/typescript.mts is a TypeScript file and gets no finding). Green: the PHP extractor is packages/php/testing/interfaces/php.php, the TypeScript extractor is packages/ts/testing/interfaces/typescript.mts with its project packages/ts/testing/interfaces/typescript (package.json and lock); make -s layout-check names no interfaces file (exit 2 is the tests/collection error only); php -l passes; node --test tests/interfaces/check/contract.test.mts passes 7 of 7 after make install-php; both extractors run on packages/ts/src and packages/php/src with exit 0; make -s parity-declarations-check exits 0; make documents-check exits 0. The checker, the layout and check inputs, the dependency records, toolchain and release records name the new paths. Not run: the database-backed interface checks, which need the database servers. | [o] |
| T77-9-2-4 | The PHP and TypeScript dbspec adapters live in their packages. Cause: the php, php-extension and typescript adapters of tests/dbspec/{compare,apply,introspect}, which the comparators run. | Acceptance: each moves into packages/php, packages/php-ext or packages/ts by its layout, the comparators and their readers name the new paths, and the php, php-extension and typescript roots of config/parity.json name only packages. Red and green: as T77-9-2-1. | Evidence: Red: with the php adapter exclusions of tests/dbspec/{apply,compare,introspect} removed from config/layout.yml, make -s layout-check exits 2 with tests/dbspec/apply/php.php, tests/dbspec/compare/php.php and tests/dbspec/introspect/php.php, each layout-infrastructure-typescript: S9 (the baseline exit 2 is the tests/collection error that main also reports; the php-extension and TypeScript adapters are not restored in the red run). Green: the PHP adapters live in packages/php/testing/dbspec, the php-extension adapters in packages/php-ext/testing/dbspec, the TypeScript adapters in packages/ts/testing/dbspec; make -s layout-check names no dbspec adapter (exit 2 is the tests/collection error only); php -l passes for all seven PHP files; node --check passes for the three TypeScript files; each adapter run without arguments reaches its usage message (PHP and TypeScript, exit 2); node --test tests/dbspec/compare/check.test.mts passes 6 of 6; make -s parity-declarations-check exits 0. The shared inputs and vectors (tests/dbspec/input., cases., vectors.*) and the php-interface helper stay where the comparators read them, except php-interface.php, which moves with the PHP compare adapter. Not run: the database apply, introspection and comparison runs, and the Go physical tests, which need database servers. | [o] |
| T77-9-3 | The Python and Rust adapters live in packages/python and packages/rust. Cause: tests/conformance/runner_python.py, tests/conformance/result_python.py, tests/dbspec/compare/python.py, tests/interfaces/python.py, tests/interfaces/test_python_*.py and tests/interfaces/rust/. | Acceptance: each moves into its package (the Rust conformance runner's layout is the precedent, packages/rust/tests/src/conformance.rs), the python and rust support lists of config/parity.json name only packages, and the comparators name the new paths. Red and green: the placement check as in T77-9-1. | Evidence: Red: with the six Python adapter exclusions of tests/conformance, tests/dbspec/compare and tests/interfaces removed from config/layout.yml, make -s layout-check exits 2 with layout-infrastructure-typescript: S9 on tests/conformance/result_python.py, tests/conformance/runner_python.py, tests/dbspec/compare/python.py, tests/interfaces/python.py, tests/interfaces/test_python_extractor.py and tests/interfaces/test_python_natives.py, and layout-infrastructure-shebang on tests/interfaces/python.py:1:1 (the baseline exit 2 is the tests/collection error that main also reports). Green: the runner and the comparator adapter live in packages/python/testing (conformance and dbspec/compare), the extractor in packages/python/testing/interfaces, the Python tests in packages/python/tests (conformance_result.py, test_python_extractor.py, test_python_natives.py), and the Rust interface crate in packages/rust/testing/interfaces (its own Cargo.lock; cargo check --locked passes). make -s layout-check names no Python or Rust interface file (exit 2 is the tests/collection error only). make -s parity-declarations-check exits 0; the python roots name only packages/python and the python and rust support lists name only packages. python3 -m unittest packages.python.tests.conformance_result passes. Not passing, and not caused by this move: packages.python.tests.test_python_extractor (2 failures) and packages.python.tests.test_python_natives (6 errors) fail identically on the HEAD tree (checked from an archive of HEAD); the extractor code is unchanged except its usage line. Not run: the make-environment Python suite, which needs the venv, and the database runs. | [o] |
| T77-9-4 | Each suite has one binding per implementation that runs all its cases and reports them by name. Cause: today the Go and Rust coverage tests are one function per case, the Go dbspec subtests nest as canonical/<id>, the Go, PHP, TypeScript and Rust conformance bindings run only some vectors, Python conformance is a program a comparator drives (tests/conformance/runner_python.py, moved by T77-9-3), and PHP reads plans.json from several files. | Acceptance: each suite has exactly one binding per implementation of its profile, which runs every case of the suite and reports each by name, checked by the parity check. Red: the parity check on the current bindings; green: after the bindings are rewritten. Verified (2026-10-18): every child is done (T77-9-4-1 in f7453dda, the others earlier); the acceptance held on cf01f139, so the parent closes with the evidence of T77-9-4-1 and T77-9-4-6. | — | [o] |
| T77-9-4-1 | The Go coverage tests run one binding per case-set (follows T77-9-4; depends on the kit rule "one binding per implementation", link-owner's kit item). Cause: the Go coverage tests (packages/go/**/coverage_*_test.go) declare one test function per case, so the parity binding of a suite lists many functions instead of one. | Acceptance: each Go suite has one binding that runs every case of the suite and reports each case by name. Red: the Go coverage files of config/parity.json that declare more than one binding for the same suite, counted by the binding check once the kit rule exists; until then, a node unit test that counts the Go bindings per suite fails on the current file. Green: one binding per suite and the unit test passes. Hold cause: the Go file packages/go/model/coverage_conformance_verification_test.go is bound by three units, conformance-mysql, conformance-postgres and conformance-sqlite, and the same is true of the PHP and Rust files of those units, so the red check fails. One binding per suite needs one unit across the five clients, which changes the parity units of every client; that is a structural change, recorded as a hold with this plan: (1) declare one suite unit conformance-verification over the three databases in config/parity.json, with each client binding its existing per-database function as a subcase; (2) remove the three per-database units and their declarations in the same commit; (3) update docs/features.md and its ko twin with make feature-docs. Retry: when the owner approves this unit merge. Verified (2026-10-18): the acceptance already held on cf01f139, so no code changed and no red existed. The Go suites run each case as a t.Run subtest of one function (TestCoverageAuthenticatedEncryption, TestCoveragePlanner and TestCoverageSchemaDefinition through runFixtureCases; TestCoverageConformanceMySQL, Postgres and SQLite through runConformanceVectors); the Rust suites are one module per suite with one #[test] per case. The hold cause (one Go file bound by three units) is not a kit violation: R12 counts bindings per unit, and each conformance unit has its own vector file, so the "kit rule" dependency is stale. | Evidence: make run-databases TARGETS=parity-check UNITS="authenticated-encryption planner schema-definition conformance-mysql conformance-postgres conformance-sqlite" exit 0 (every unit holds in every implementation; 31 bindings ran twice; 1207 cases passed); make parity-declarations-check exit 0. | [o] |
| T77-9-4-2 | The Go dbspec subtests report flat case names (follows T77-9-4; depends on the kit rule). Cause: the Go dbspec subtests nest as canonical/<id>, so a case is reported under its group instead of by its own name. | Acceptance: each case is reported by its id as one name, and the binding runs every case of its suite. Red: a unit test on the reported names of go test -run for the dbspec suite fails while a name contains /. Green: names are flat and the test passes. | Evidence: TestSubtestNamesAreFlat fails with 24 names containing "/" before the renames and passes after; TestVectorLoadersRejectMalformedVectors passes. | [o] |
| T77-9-4-3 | The PHP conformance bindings run every vector (follows T77-9-4; depends on the kit rule). Cause: the PHP conformance binding (packages/php/tests/ConformanceResultTest.php) and the runner run only some vectors. | Acceptance: the binding runs every vector of tests/conformance/vectors*.json and reports each by name. Red: a unit test that counts the vectors the PHP binding reports fails while it is below the vector count. Green: the counts are equal. | Evidence: ConformanceResultTest passes 54 tests (vendor-php PHPUnit) once relation_null_key is declared by the vector files, so the count test is green. | [o] |
| T77-9-4-4 | The TypeScript conformance bindings run every vector (follows T77-9-4; depends on the kit rule). Cause: the TypeScript conformance binding (packages/ts/tests/result_typescript.test.mts and conformance_case.mjs) runs only some vectors. | Acceptance: the binding runs every vector and reports each by name. Red: a unit test that counts the reported vectors fails while it is below the vector count. Green: the counts are equal. | Evidence: conformance_vector_count.test.mts passes 3 of 3 (node --test): the declared vectors of each dialect equal the vectors the runner runs, and each dialect file names the same set. The suites already run every declared vector through conformanceSuite(). | [o] |
| T77-9-4-6 | The Python conformance program becomes a test binding (follows T77-9-4; depends on the kit rule). Cause: packages/python/testing/conformance/runner_python.py is a program that the comparator drives, not a test binding. | Acceptance: a Python test binding in packages/python/tests runs every vector and reports each by name, and the comparator keeps driving the same program. Red: the binding check reports no Python conformance binding for the suite. Green: the binding exists and the check reports it. Progress: the bindings read the runner at its moved path (packages/python/testing/conformance/runner_python.py); the parity run is in the final batch. Verified (2026-10-18): the acceptance already held on cf01f139. The Python bindings conformance_mysql.py, conformance_postgres.py and conformance_sqlite.py each run every vector by name through the unchanged runner runner_python.py, so no code changed and no red existed. | Evidence: make run-databases TARGETS=parity-check UNITS="authenticated-encryption planner schema-definition conformance-mysql conformance-postgres conformance-sqlite" exit 0 (every unit holds in every implementation; 31 bindings ran twice; 1207 cases passed; Python 51 of 51 per dialect); make parity-declarations-check exit 0. | [o] |
| T77-9-4-5 | The Rust conformance bindings run every vector (follows T77-9-4; depends on the kit rule). Cause: the Rust conformance binding (packages/rust/tests/src/conformance.rs) runs only some vectors. | Acceptance: the binding runs every vector and reports each by name. Red: a unit test that counts the reported vectors fails while it is below the vector count. Green: the counts are equal. | Evidence: every_vector_of_the_vector_files_is_declared passes (cargo test, 1 passed); cargo check of the conformance bin passes. The run of the 50 declared vectors needs no change. | [o] |
| T77-9-4-7 | The PHP plans reader reads plans.json from one file (follows T77-9-4). Cause: the PHP binding reads tests/dbspec/plans.json and its parts from several files. | Acceptance: one reader loads the file and every plan case of it is one binding. Red: a unit test that counts the files the PHP plan reader opens fails while it is above one. Green: one file. | Evidence: DbspecPlanReaderTest::test_one_file_opens_plans_json fails with four openers on the HEAD test tree and passes with one (dbspec_plan_cases.php); php -l passes on the five changed files. | [o] |
| T77-10 | One language set. Cause: the feature checks, conformance, apply comparison and runners each name their own set. | Acceptance: all read config/parity.json; the hand-written clients flags and check.mjs:170-180 are removed, features reference parity units, and the tests.language-parity rule of contracts/rules.json uses the parity check. | Evidence: scripts/client-db-test.mts and tests/interfaces/contracts/validate.go read clientLanguages() from config/parity.json; go vet of tests/interfaces/contracts exits 0, gofmt reports no file, and node --experimental-strip-types --check of the script exits 0. | [o] |
| T77-10-1 | The feature checks, coverage, build, decimal runner, conformance and apply readers name the language set from config/parity.json (follows T77-10). Cause: scripts/features/build.mts:14 and check.mts:24 (clients), scripts/features/coverage.mts:53 (languages), scripts/decimal-physical-check.mts:7, tests/conformance/check/main.mts:54 and result.physical.test.mts:13 (requiredLanguages), tests/interfaces/contracts/validate.go, tests/dialects/apply_clients_physical_test.go:32 each hand-write a language list that config/parity.json already holds as the client and dbspec profiles. | Acceptance: each reader takes its list from those profiles, the matching languageLists entries in config/parity.json are removed in the same commit, and a node unit test in the feature-unit-check target proves that a hand-written list disagrees with the profile (red first, then green). Red: that test fails on the current hand-written lists. Green: it passes after the readers take the profile. | Evidence: commits 468e34b5 and d770650c on main (the readers take the language set from the client profile of config/parity.json; scripts/docs/rules.mts accepts the profile-derived requiredLanguages): red then green in scripts/features/languages.test.mts (fails 6 of 7 on main's readers, passes 7 of 7 after). documents-check and docs:rules-check exit 0. The three owning feature checks exit 2, 2 and 1 on main with the same failures (registered as T198), so they are not this row's regression. | [o] |
| T77-10-2 | The features' clients flags and check.mts:170-180 are removed, and tests.language-parity uses the parity check (follows T77-10). Cause: contracts/features.json has 19 clients objects, read by scripts/features/coverage.mts (lines 75, 79, 105) and by check.mts:170-180; removing them needs a schema change in scripts/features/schema/features.schema.json, a mapping of each feature to its parity units, and tests.language-parity in contracts/rules.json switched from feature-check to the parity check. | Acceptance: no clients flag remains, the rule uses the parity check, and the feature checks pass. Waits for T77-10-1. | Evidence: node --test on scripts/features/coverage.test.mts 22/22, languages.test.mts 8/8 and manifest.test.mts 3/3 pass; make documents-check passes (44 document pairs, 1 checklist, 1 changelog). | [o] |
| T77-11 | The documents name every implementation. Cause: 29 places and their twins still say four implementations, including the rules at AGENTS.md:202-203,212, and docs/spec/codec.md:39 omits one. | Acceptance: they name the implementations that config/parity.json declares, among them protocol.md:174-185 (the §4 table gains the Python row packages/orm-python/src/polyspec/orm/engine/ and the text says five planners and clients) and tests/conformance/README.md (five runners), each with its .ko.md twin; the parity check refuses a stale count and asserts that the client list equals the runner list of tests/conformance/check/main.go:101, red on today's text first. | Evidence: make docs-rules-check (docs/rules.mts, the documents check, reads config/parity.json) refuses a count word that differs from the client profile in English and Korean documents, and a section 4 table of docs/spec/protocol.md without a row for each client; it checks requiredLanguages of tests/conformance/check/main.mts (main.mts:54, not main.go:101) against the client profile, which is equal today. RED on the text before the fix: 8 findings, docs/spec/codec.md and docs/spec/interface-implementation.md "four outputs", CONTRIBUTING.md "four clients", tests/codec/README.md "four outputs", CONTRIBUTING.ko.md "네 클라이언트", docs/spec/protocol.md and docs/protocol.ko.md section 4 with no Python row. GREEN: 6 stamped by make documents-stamp, make docs-rules-check passes (42 English documents, 42 Korean translations), make documents-check passes 43 pairs, make docs-check passes (docs-build, docs-site/pages, docs-site/links). | [o] |
| T77-11-1 | The documents name every declared client in their tables and set enumerations. Cause: whole-set lists and implementation tables still drop Python, among them the logical-type and operation tables of interfaces.md, the operation table of schema.md, the table of perf.md and enumerations such as docs/index.md:17. | Acceptance: make docs-rules-check refuses a table whose header names two or more client languages without a column for each declared client, and a sentence that uses two or more client names as the set ("the clients", "implementations", "languages", "runners", "planners") without naming all of them; a client without data says so ("not measured", "none"); every place it names is fixed in English and in its .ko.md twin. | Evidence: RED on the text before the fix, 30 findings (11 table headers, 19 enumerations), in the commit message. GREEN: make docs-rules-check passes (42 English documents, 42 Korean translations), make documents-check passes 43 pairs, make docs-check passes (docs-build, docs-site/pages, docs-site/links). | [o] |
| T77-11-2 | The rule code of T77-11 passes make lint-ts. Cause: the rules of T77-11 in scripts/docs/rules.mts (commits 5e6520a5 and 0a1c643c) were checked by the docs checks only, and make lint-ts fails on them: line 137 reads config/parity.json with JSON.parse(...) as Parity, an unsafe type assertion from any (typescript/no-unsafe-type-assertion), and line 164 tests a line with the caret regex /^|/, which unicorn/prefer-string-starts-ends-with asks to write as startsWith. | Acceptance: the file is read through a shape guard (isParity, isProfile, isStringArray) that records a finding for a file of another shape, the table rows are filtered with startsWith('|'), and make lint-ts passes with no rule disabled, no inline suppression and no change to the lint configuration. | Evidence: RED, make lint-ts after make build of @polyspec/orm, @polyspec/dbspec-language and @polyspec/dbspec-lsp (which lint-ts reads through their dist) reports 2 problems, scripts/docs/rules.mts:164:51 (prefer-string-starts-ends-with) and scripts/docs/rules.mts:137:16 (no-unsafe-type-assertion), and oxlint exits 1. GREEN, make lint-ts exits 0 (oxlint passed, knip passed); make docs-rules-check exits 0 (42 English documents, 42 Korean translations passed); make docs-check exits 0 (docs-build, docs-site/pages and docs-site/links passed). | [o] |
| T77-12 | Every push runs the parity checks. | Acceptance: parity-check and parity-declarations-check are in CHECK_TARGETS, a CI group and the check inputs, exit 0 with no divergence left, in the same push as T77 to T77-11, T73 and T74. parity-check runs with the test servers, locally as make run-databases TARGETS=parity-check and in CI in a group that sets up the databases; a database unit runs all its cases on every database it names, never its SQLite cases alone; each binding receives the database DSNs through ${NAME} references in config/parity.json (kit K39), so a run without the servers is a declaration finding naming the missing variable; the Go build tags of the database tests are declared once in the Go implementation's args. Progress: parity-check and parity-declarations-check are in CHECK_TARGETS, the static and conformance CI groups, and contracts/check-inputs.json. make repo-check/unit fails four tests on main too (examples/thin-slice/python/main.py language, packages/ts/tests/plan-cache-in.mjs node:test, packages/rust/orm/src/request.rs and collection_identity.rs case start, and the PHP/TS case-report test); the parity-check run is in the final batch. | Evidence: CHECK_TARGETS (Makefile:164) lists parity-check and parity-declarations-check; the static CI group (line 182) lists parity-declarations-check and the conformance CI group (line 185) lists parity-check; contracts/check-inputs.json lists both; make parity-declarations-check: every declaration holds (scratchpad/t148-decl4.log). | [o] |
| T199 | Implement the complete ORM surface in native C, including queries, collections and the remaining runtime classes. | Acceptance: the extension implements the query, collection and remaining ORM surface in C, and the client-profile conformance outputs agree with the other five clients. Native classes retain the PHP client class names and do not load PHP product implementations. | make parity-declarations-check. Native surface evidence: T182-6-12 through T182-6-20, including C-only execution and fallback exclusion; 110 native model cases and 39 required fixture cases; native declaration comparison has zero findings. The 59 conformance vectors agree across six clients on MySQL, PostgreSQL and SQLite, combining the retained passing vectors with the focused audit_lazy correction; interfaces/results reads all 18 outputs successfully. Evidence: var/evidence/final-integrated-20261026/t1826-acceptance-evidence-index.json and var/evidence/KSTD-5/current-php-ext-targets/declarations-root-after-port.log. Implementation: 8a91e2697 and 379a177a4; native acceptance completed in dd22a6c2e. | [o] |
| T78 | Every client's test entry point makes the next statement fail with a given ORM error. Cause: a consumer test must make an insert of the PHP client fail with DRIVER, the request-log case of consumer needs it, but Polyspec\Orm\Db is final and takes \PDO, so a test can inject the failure only by subclassing \PDO, which a consumer that bans driver names in source (consumer sqlban) cannot do. The class of the item is a testing fault seam that every client keeps, a cross-backend standard and not a feature of one consumer, so it follows the rollback fault of protocol §3.1 in the five clients that hold a connection. | Acceptance: protocol §3.2 and usage define FailNextStatement (Go, build tag ormtest), fail_next_statement (Rust, feature test-faults), failNextStatement (TypeScript testing subpath), Faults::failNextStatement (PHP) and fail_next_statement (Python testing package), each taking the code and the message; an empty code or message is refused with the usage error of the client and arms nothing; the next statement of the connection is not sent and is reported as OrmException with that code and message at the statement end that a driver error takes; the fault is consumed by that statement, and the database and the next statement are unaffected. Each client runs every case of contracts/fixtures/statement_fault.json on SQLite, MySQL and PostgreSQL and fails when a named case does not run. The dbspec apply and introspection of every implementation, the PHP extension included, run on the driver connection outside Db and do not consume the fault (protocol 3.2). | Evidence: RED commit 77bfe13f runs the four cases of contracts/fixtures/statement_fault.json on SQLite, MySQL and PostgreSQL in each client and fails on the missing function in every client (Go undefined: orm.FailNextStatement, Rust E0425, TypeScript export missing, PHP undefined method, Python AttributeError). GREEN on the tree of this commit: Go go test -tags ormtest -run '^Test(RollbackFault|StatementFault)' passes every StatementFault subtest on the three databases, and the untagged package run passes; Rust --test statement_fault 13 of 13 and --test rollback 10 of 10; TypeScript statement_fault.mjs 13 of 13 and rollback.mjs 9 of 9; Python test_statement_fault.py 7 of 7 and test_rollback_fault.py 6 of 6; PHP statement_fault_test.php 12 PASS and rollback_test.php 11 PASS. The dbspec apply and introspection of every implementation, the PHP extension included, run on the driver connection outside Db and do not consume the fault (protocol 3.2). The dynamic parity-check run under the test servers shows 154 R3 not-executed findings in other units, which T77 owns. | [o] |
| T78-1 | The checker case of the client test lanes and the Python driver error test pass with the statement fault tests of T78. Cause: T78 added one statement fault test to the PHP lane of scripts/client-db-test.sh and to scripts/typescript/sqlite-test.sh, and two cases of scripts/check/continue.test.mjs compared the commands that ran with counts written by hand (11 and 10), so the test failed on a correct change of the lanes; T78 also added the keyword faultable to TxFrame.run of the Python client, which the SQLite row lock passes, and the frame double of test_a_busy_nowait_row_lock_is_lock_not_available in packages/orm-python/tests/test_driver_errors.py did not accept it. | Acceptance: the two cases read the commands they expect from the lane function php_lane and from scripts/typescript/sqlite-test.sh and compare the whole sequence that ran, so adding or removing a test file of a lane needs no change of the case; the frame double takes the arguments of TxFrame.run and fails when a row lock statement would consume the statement fault (protocol 3.2). | Evidence: Red: node --test scripts/check/continue.test.mjs exits 1, 2 of 9 cases fail with 12 !== 11 (the PHP lane) and 11 !== 10 (the TypeScript SQLite tests); the Python unit test run of test_driver_errors.py (python -m unittest discover -s packages/orm-python/tests -p test_driver_errors.py) runs 12 tests with 1 error, TypeError: ... Frame.run() got an unexpected keyword argument 'faultable'. Green: the same commands pass 9 of 9 and 12 of 12. | [o] |
| T78-2 | make python-unit-check does not run the statement fault test, which needs the databases. Cause: python-unit-check runs unittest discover -p 'test_*.py', which also picks up packages/orm-python/tests/test_statement_fault.py; its four cases need ORM_TEST_MYSQL_DSN and the other database variables, but the needs of python-check hold no database, so python-check fails under the check runner with "ORM_TEST_MYSQL_DSN is required". | Acceptance: the file is named statement_fault.py, without the test_ prefix, as dbspec_introspect.py is, so the discover pattern does not match it; python-statement-fault-check, config/parity.json and contracts/features.json name the new path; the needs of python-check gain no database. | Evidence: Red: ls packages/orm-python/tests/test_*.py (the discover pattern) lists test_statement_fault.py besides test_rollback_fault.py. Green: the same listing no longer holds it, and python-statement-fault-check runs packages/orm-python/tests/statement_fault.py. | [o] |
| T79 | contains folds only ASCII case on MySQL, as on PostgreSQL and SQLite. Cause: protocol-defect fix. contains is case-insensitive by the protocol (engine/dialect/dialect.go:13, docs/spec/protocol.md:97), but MySQL LIKE under the column collation utf8mb4_0900_bin compares code points and is case-sensitive, while PostgreSQL ILIKE under C and SQLite LIKE fold ASCII letters only, so docs/spec/dialects.md:12 ("collation-driven case-insensitivity") is false. LOWER and every _ci collation fold beyond ASCII, and REGEXP_LIKE reads a digit escape as a back-reference, refuses an empty pattern and stops past regexp_time_limit, so neither is a fix. | Acceptance: Red first: the probes mysql.like.binary_collation_case_sensitive, mysql.like.ascii_fold_replace, postgres.ilike.c_collation_ascii_only and sqlite.like.ascii_only in tests/dialects record the facts; planner.json cases give the MySQL SQL; the conformance vector contains_ascii_fold (rows Apple, apple, Évian, évian, x_y; patterns PPL, ÉV, _, "") differs today between tests/conformance/vectors.json and the PostgreSQL and SQLite files (PPL misses Apple). Then the MySQL dialect of Go, PHP, TypeScript, Python and Rust renders F(col) LIKE F(?), F being 26 nested REPLACE of each of A–Z with its lower case, with the like_contains transform unchanged; the conformance results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the planner.json and conformance vectors, each from a test in its own package. Docs (EN and .ko.md): the dialects.md:12 MySQL cell with its reason, the rule "contains matches a substring with the ASCII letters compared without regard to case and every other character by code point, on every database" in "Rules that keep the three databases identical", and protocol.md:97 (contains compares ASCII letters without regard to case and every other character by code point; contains_binary compares by code point). | Evidence: RED: the conformance vector contains_ascii_fold gave [[],[],[5],[1..5]] on MySQL against [[1,2],[3],[5],[1..5]] on PostgreSQL and SQLite, and planner_contains_ascii_fold failed in Go, PHP, TypeScript, Python and Rust; the MySQL dialect of the five clients now renders F(col) LIKE F(?), F folding A-Z only, the probes mysql.like.binary_collation_case_sensitive, mysql.like.ascii_fold_replace, postgres.ilike.c_collation_ascii_only and sqlite.like.ascii_only pass, and the vector is identical in the three vector files. The PHP extension holds no query layer, so it runs no planner or conformance case. | [o] |
| T79-1 | Let the scan see the contains calls of the TypeScript runner (follows T79). Cause: the T79 runner called andLkName and andLbName on a callback parameter, which orm-gen's scan does not tie to CompositeAccount, so the generated type lacked them and make lint-ts failed on main (no-unsafe-call, no-unsafe-return at runner_typescript.mts:388-389). | Acceptance: each call is a chain from new CompositeAccount(), and lint-ts passes. | Evidence: lint-ts reported the four findings; with the chains written out the generated model declares both methods and lint-ts passes. | [o] |
| T79-1-1 | Record the TypeScript symbols that T79-1 added (follows T79-1). Cause: with the contains calls written as chains, orm-gen declares CompositeAccount andLkName and andLbName, but T79-1 recorded neither the symbols nor the TypeScript hash, so make interface-check failed on main ("unexpected … CompositeAccount#field.andLbName"). | Acceptance: both symbols and the hash the checker computes are recorded, and interface-check passes. | Evidence: interface-check reported the two unexpected symbols; contracts/symbols/typescript.json gains them, contracts/interfaces.json takes the computed hash sha256:9cbbf7a0…, and interface-check passes. | [o] |
| T80 | A non-string contains value is CODEC_ENCODE, and NULL binds NULL. Cause: protocol-defect fix. errors.yaml defines CONFIG as a missing secret, a bad DSN or misuse, and CODEC_ENCODE as a value that cannot be encoded for its slot; a non-string value of a like_contains or contains_binary slot is the second, yet the clients answer CONFIG (packages/orm-go/orm/exec.go:257, packages/orm-npm/src/database.ts:834, packages/orm-python/src/polyspec/orm/database.py:770, and the same transform in Db.php:1409 and value.rs:364), and they refuse NULL although col LIKE NULL is unknown on all three databases. | Acceptance: Red first: cases of the shared bind vectors tests/bind/vectors.json of T89 (5 → CODEC_ENCODE, null → NULL, "a%" → %a\%%) answer CONFIG today, and a conformance vector in which contains null matches no row. Then the five clients bind a NULL value as NULL and answer CODEC_ENCODE "%s requires a string value" for another non-string; the conformance results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package. Docs (EN and .ko.md): protocol.md §2 bind_slots (transform). Requires T89. | Evidence: a non-string contains value fails with CODEC_ENCODE ("%s requires a string value") in the five clients, where it was CONFIG and PHP cast it to text, and NULL binds NULL, which the condition builders of the five clients had refused with CONFIG; the contains bind cases and the conformance vector contains_null (count 0) are identical in the three vector files. RED: CONFIG in the five clients on the three databases. The PHP extension query vectors are T182 work. | [o] |
| T81 | MySQL contains_binary renders LIKE under the binary collation, not LIKE BINARY. Cause: protocol-defect fix. dialects.md:13 renders Lb as LIKE BINARY, an operator MySQL deprecates since 8.0.27 (warning 1287), so the dialect relies on syntax the database will remove; every orm character column is already utf8mb4_0900_bin (dbspec.md:93), under which plain LIKE compares code points. | Acceptance: Red first: the planner.json case that expects col LIKE ? on MySQL fails on LIKE BINARY ?, and the probe mysql.like.binary_collation_code_point records the same rows as LIKE BINARY (%pp% → 1, 2; %É% → 3). Then the MySQL dialect of Go, PHP (Dialect.php:110), TypeScript (dialect.ts:128), Python (dialect.py:169) and Rust (dialect.rs:155) renders col LIKE ?; the conformance vector contains_binary_code_point is identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the planner.json and conformance vectors, each from a test in its own package. Docs (EN and .ko.md): the dialects.md:13 MySQL cell "LIKE (binary collation utf8mb4_0900_bin)". | Evidence: RED: planner_contains_binary_collation failed in the five clients with LIKE BINARY ?; the probe mysql.like.binary_collation_code_point shows LIKE and LIKE BINARY give the same rows; MySQL contains_binary now renders col LIKE ? in the five clients, and contains_binary_code_point is [[1,2],[3],[],[5]] in the three vector files. The two contains vectors now delete in key order, since an unordered read changed the PostgreSQL bind order between runs. | [o] |
| T82 | NULL sorts first ascending and last descending on every database. Cause: protocol-defect fix. One order request gives different rows: MySQL and SQLite put NULL first ascending and PostgreSQL last; the planner renders no NULLS clause (engine/planner/planner.go:443-475), and dialects.md:141 covers index key order only. | Acceptance: Red first: the probes mysql.order.null_first_asc, postgres.order.null_last_asc and sqlite.order.null_first_asc; planner.json PostgreSQL cases with and without the suffix; the conformance vector order_nulls (ascending, descending, limit_per_parent), whose PostgreSQL file differs today. Then NULL is less than every value: the PostgreSQL dialect of the five planners appends NULLS FIRST to ASC and NULLS LAST to DESC on an order expression that can be NULL (a nullable column or a column function of one; a NOT NULL column keeps its btree order), in the root order, the ROW_NUMBER() OVER (… ORDER BY …) of limit_per_parent and the group_count order, and MySQL and SQLite render nothing; the conformance results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package. Docs (EN and .ko.md): the dialects.md rule "An order item sorts NULL before every value ascending and after every value descending: MySQL and SQLite do so natively, and PostgreSQL renders ASC NULLS FIRST and DESC NULLS LAST on an expression that can be NULL", and the same rule in protocol.md §1.4 Order. State: the PostgreSQL dialect of the five planners appends NULLS FIRST to ASC and NULLS LAST to DESC on an order expression that can be NULL (a nullable column or a column function of one), in the root order, the ROW_NUMBER() OVER (… ORDER BY …) of limit_per_parent and the group_count order, and MySQL and SQLite render nothing (a new dialect method NullsOrder/nullsOrder/nulls_order); five planner.json cases (a nullable column, a NOT NULL column, a column function, group_count, limit_per_parent) run in Go, PHP, TypeScript and Rust, and Python runs every case. Not done: the probes mysql.order.null_first_asc, postgres.order.null_last_asc, sqlite.order.null_first_asc and the conformance vector order_nulls over the three databases, split into T82-1. Docs: protocol.md §1.4, dialects.md. | — | [o] |
| T82-1 | The NULL-order probes and the conformance vector order_nulls (split from T82). Cause: T82 made PostgreSQL render the NULLS clause, and no probe or vector shows the three databases now agree. | Acceptance: the probes mysql.order.null_first_asc, postgres.order.null_last_asc and sqlite.order.null_first_asc are recorded, and the conformance vector order_nulls (ascending, descending, limit_per_parent) gives identical results in the three vector files and runs in the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust), each from a test in its own package. | Evidence: probes mysql.order.null_first_asc, postgres.order.null_last_asc and sqlite.order.null_first_asc pass and dialects.md cites them; the bench seeds give prices to the authors of service 100 except every third, and the conformance vector order_nulls (ascending, descending and groupLimit) is identical in the three vector files, NULL first ascending and last descending, with NULLS FIRST and NULLS LAST on PostgreSQL. RED: the PHP, Rust, TypeScript and Python runners reported unknown vector order_nulls on the three databases. The PHP extension query vectors are T182 work. | [o] |
| T83 | group_by is valid only on the root of count and group_count, and a group order names group columns. Cause: protocol-defect fix. The planner emits SQL that two databases refuse and drops an input: engine/engine_test.go:239 expects an all with GROUP BY that selects the primary key (MySQL 1055, PostgreSQL 42803, an arbitrary row on SQLite), planner.go:294-296 ignores group_by on sum and avg, and ordering groups by a non-group column is 1055, 42803 or arbitrary. | Acceptance: Red first: planner.json reject cases (all, sum, a join query, a subquery, an order by a non-group column) pass validation today; accept cases (count, and group_count ordered by a group column, a column function of one, and random). Then the five validators (ir.go, Validator.php, validate.ts, validate.py, validate.rs) answer IR_INVALID for group_by on a one, all, paginate, sum or avg root and in a join, relation or subquery query, and IR_INVALID "order by %s, which is not a group column" for an order item under group_by that is not a group column, a column function of one or random; the planner paths for sum/avg and subquery GROUP BY that become unreachable are deleted; engine_test.go:239 expects IR_INVALID; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the planner.json cases, each from a test in its own package. Docs (EN and .ko.md): protocol.md §1 group_by (valid on the root of count and group_count; elsewhere IR_INVALID) and §1.4 (with group_by, an order item names a group column, a function of one, or random), and dsl.md:255 (groupBy<Col>() sets grouping on getsCount() and group counts; on another read it is IR_INVALID). State: the five validators answer IR_INVALID for group_by outside the root of count and group_count and for an order item that is not a group column, a function of one or random; the unreachable planner paths (GROUP BY on rows, sum/avg and subqueries) are deleted; engine_test.go no longer expects an all with GROUP BY; nine planner.json cases (four accepted, five rejected) run in Go, PHP, TypeScript and Rust, Python runs every case of the file, and tests/ir/validate.json holds the validator cases; the PHP extension parses dbspec only and has neither a validator nor a planner, so it carries no rule. Docs: protocol.md §1 and §1.4, dsl.md. | — | [o] |
| T84 | A scalar column subquery yields at most one row by its shape. Cause: protocol-defect fix. A scalar Sub without agg (engine/ir/ir.go:826-830) that matches several rows has no single result: MySQL answers 1242, PostgreSQL 21000, and SQLite returns the first row; recording that difference is no rule, so the request shape must make one row certain. | Acceptance: Red first: planner.json cases (a scalar column subquery without limit passes validation today, and one with limit is refused) and the conformance vector scalar_column_subquery over two matching rows. Then the five validators require of a scalar column subquery (columns.sub with agg "") limit with count 1 (any offset) and an order that includes every primary key column of its entity and no random item, else IR_INVALID; the refusal at ir.go:852-853 opens for this form only, limit stays IR_INVALID in an in subquery (MySQL 1235) and on an aggregate subquery, and a subquery with group_by is IR_INVALID (T83); the five planners' subSelect (planner.go:1533) render ORDER BY … LIMIT … for it with the NULL order of T82; the conformance results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package. Docs (EN and .ko.md): protocol.md §1.3 Sub ("a scalar column subquery has limit count 1 and an order that includes every primary key column, so it yields at most one row on every database; it is NULL when none matches"). Requires T82 and T83. State: the five validators require of a scalar column subquery (columns.sub with no agg) limit with count 1 (any offset) and an order that includes every primary key column and no random item, else IR_INVALID; limit stays IR_INVALID in an in subquery and in an aggregate subquery, and a subquery with group_by is IR_INVALID (T83); the five planners' subSelect render ORDER BY … LIMIT … for it with the NULL order of T82; seven validator vectors and four planner.json cases (one accepted on three dialects, three rejected) run in Go, PHP, TypeScript and Rust, and Python runs every case of both files. Not done: the conformance vector scalar_column_subquery over two matching rows on the three databases, split into T84-1. Docs: protocol.md §1.3, dsl.md. | — | [o] |
| T84-1 | The conformance vector scalar_column_subquery (split from T84). Cause: T84 made the scalar column subquery yield at most one row by its shape, and no vector shows the three databases return the same row. | Acceptance: the conformance vector scalar_column_subquery over two matching rows gives identical results in the three vector files and runs in the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust), each from a test in its own package. | Evidence: the conformance vector scalar_column_subquery reads the name of one of two matching authors ordered by price then seq with limit(0, 1): author-99 ascending and author-5099 descending, identical in the three vector files. RED: unknown vector scalar_column_subquery from the five runners on the three databases. The PHP extension query vectors are T182 work. | [o] |
| T85 | flatten is the client model's merge, and the parent's value wins. Cause: protocol-defect fix. protocol.md:73 says flatten merges the child columns into the parent row, usage.md:247 adds that parent values win, and plan.go:104 places the merge in the array and JSON forms; every model layer merges the child's whole array form (model.go:1329-1370, model.ts:782, Model.php:2020, model.py:187, model.rs:455), and no vector pins a name collision, an absent child or a flattened grandchild. | Acceptance: Red first: the conformance vector relations_flatten (parent and child sharing id and name, an absent child, a flattened grandchild) is written before any change, and a model layer that differs fails it. Then the array form is identical in the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust), each running the vector from a test in its own package. Docs (EN and .ko.md): protocol.md §1.2 flatten: "flatten merges a one child into the parent's array and JSON forms: after the parent's own members, the members of the child's array form — its outputs, its relations, and a flattened grandchild merged likewise; a name the parent holds keeps the parent's value, and an absent child adds nothing. The rows a plan assembles and an IR executor answers hold the child under rel; the merge is the client model's." usage.md:247 links to it. | Evidence: the conformance vector relations_flatten flattens a service region whose columns clash with the parent and holds a flattened service with its members, and only closed authors get it; RED: Go alone gave service_region_model null for the closed author 406 on MySQL and SQLite, because it compared the bool if_parent with the raw 0/1 cell; ifParentMatch now reads the cell as a bool for a bool parameter, the five clients agree on the three databases, and protocol.md 1.2 states the flatten rule and that if_parent compares by the column logical type. The PHP extension query vectors are T182 work. | [o] |
| T85-1 | Regenerate the Go models after T85 (follows T85). Cause: the relations_flatten vector of the Go runner reads the service relation of a service region, which makes orm-gen add GetServiceModel to packages/go/model/service_region.go, but T85 did not commit the regenerated model, so go-model-check and the model_generation coverage failed on main (run 38698025914). | Acceptance: the committed models equal go generate, and go-model-check passes. | Evidence: go generate adds GetServiceModel to service_region.go, the only difference CI reported; with it committed, and its symbol and the Go hash recorded, go-model-check, interface-check and go-vet-check pass. | [o] |
| T86 | key_by names a non-null column. Cause: protocol-defect fix. A NULL collection key gives a different answer by language (Go CODEC_DECODE, PHP '' colliding with an empty key; T77-6), and a nullable key_by column makes the answer depend on the data. | Acceptance: Red first: a planner.json reject case with a nullable key_by column passes validation today. Then the five validators (ir.go:547-552 and its counterparts) answer IR_INVALID "key_by %s is nullable"; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the case, each from a test in its own package, the PHP extension run proving the nullability fact. Docs (EN and .ko.md): protocol.md §1.2 ("key_by names a non-null column of the child; a nullable column is IR_INVALID"). State: the five validators answer IR_INVALID "key_by %s is nullable" for a nullable key_by column; the validator vectors (key-by-non-null-column, key-by-nullable-column) and the planner.json case planner_rejects_nullable_key_by run in Go, PHP, TypeScript and Rust, and Python runs every case of both files; the PHP extension parses dbspec only and carries no validator. Docs: protocol.md §1.2. | — | [o] |
| T87 | on_duplicate has exactly one conflict key. Cause: protocol-defect fix. An upsert gives different results across the databases, and dialects.md:17 and :398 record the divergence instead of a rule: MySQL updates the row met on any unique key, in the order primary key, NOT NULL unique keys, nullable unique keys, while PostgreSQL and SQLite name one target (conflictTarget, planner.go:1226-1245: the first unique key fully covered, else the primary key) and a meeting on another key is a unique violation, so no key order reproduces both. | Acceptance: Red first: the probes mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts and sqlite.upsert.other_key_conflicts; planner.json reject cases (a primary key and a unique key both assigned; an identity table whose unique key is partly assigned; a table with two unique keys), which plan today, and accept cases; the conformance vector upsert_one_key. Then the five validators accept an insert with on_duplicate only when exactly one key can meet the inserted row and the insert assigns every column of it, else IR_INVALID "on_duplicate needs exactly one conflict key; %s can meet the row"; the candidates are the primary key, unless the insert leaves it out (an identity, or a key an IR executor assigns), and each unique key; conflictTarget of the five planners returns that key, and MySQL keeps ON DUPLICATE KEY UPDATE, which then meets only it; the conformance results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package, the PHP extension run proving the key, identity and unique facts. Docs (EN and .ko.md): protocol.md §1 on_duplicate (applied when the inserted row meets an existing row on its one conflict key; otherwise IR_INVALID), dialects.md:17 (the three cells name the one conflict key, with the reason that MySQL's key order and the single PostgreSQL and SQLite target agree only for one key), and dsl.md:266 (duplication(model) requires a table and insert with one conflict key). State: the five validators accept an insert with on_duplicate only when exactly one key can meet the inserted row (the primary key unless the insert leaves it out, and each unique key; a blind index column counts with its AES source) and the insert assigns every column of it, else IR_INVALID "on_duplicate needs exactly one conflict key; %s can meet the row"; conflictTarget of the five planners returns that key and the first-covered-key rule is deleted; the validator vectors (upsert-one-unique-key, upsert-primary-key-and-unique-key, upsert-unique-key-not-assigned, upsert-no-key, upsert-composite-primary-key) and four planner.json cases run in Go, PHP, TypeScript and Rust, and Python runs every case of both files. The benchmark schema has no table with two unique keys or a partly assigned composite unique key, so those two reject cases are not run; the rule reads the same candidate list for them. Not done: the probes mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts, sqlite.upsert.other_key_conflicts and the conformance vector upsert_one_key over the three databases, split into T87-1. Docs: protocol.md §1, dialects.md rows duplication and on_duplicate. | — | [o] |
| T87-1 | The upsert probes and the conformance vector upsert_one_key (split from T87). Cause: T87 made the validators and planners allow one conflict key, and no probe or vector shows the three databases agree on it. | Acceptance: the probes mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts and sqlite.upsert.other_key_conflicts are recorded, and the conformance vector upsert_one_key gives identical results in the three vector files and runs in the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust), each from a test in its own package, the PHP extension run proving the key facts. | Evidence: probes mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts (23505) and sqlite.upsert.other_key_conflicts pass; the bench table upsert_cell (identity key and unique code) and the conformance vector upsert_one_key in the five runners: an upsert that inserts, one that updates on code, and one without code, which fails with IR_INVALID because the insert does not assign the one conflict key; recorded identical in the three vector files with a clean re-run. The vector was red with no recorded expectation; T87 had already made the clients agree. tests/dbspec/mermaid.json gains the unique key of upsert_cell, which the new table needed for TestMermaidVectors. The PHP extension query vectors are T182 work. | [o] |
| T88 | Write affected counts an upsert as 1 and an update as its matched rows. Cause: protocol-defect fix. The same upsert that updates answers 2 on MySQL and 1 on PostgreSQL and SQLite, because every client returns the driver's number (exec.go:966, driver.ts:195, Db.php:839, driver.rs:414, database.py:495); an update counts matched rows only because Go (dsn.go:58) and PHP (Orm.php:56) set found rows, which no rule states and no case checks for the other clients. | Acceptance: Red first: the conformance vector affected_counts (an upsert that inserts, an upsert that updates, and an update matching two rows of which one already holds the value) answers 2 for the updating upsert in tests/conformance/vectors.json. Then the five clients report 1 for an insert with on_duplicate, inserted or updated, and the matched rows for an update (MySQL CLIENT_FOUND_ROWS in every MySQL driver configuration); the results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vector, each from a test in its own package. Docs (EN and .ko.md): protocol.md §5 Write, and a "Write result" sentence in §2 ("affected counts each written row once; an insert with on_duplicate is 1 on every database, and an update counts the rows it matched"). State: the five clients report 1 for an insert with on_duplicate, inserted or updated (Go exec.go, TypeScript database.ts, PHP Db.php, Python database.py, Rust model.rs), and an update counts the rows it matched in every MySQL driver configuration: Go (clientFoundRows), PHP (ATTR_FOUND_ROWS), TypeScript (mysql2's default flags, measured 2 on a live MySQL for an update matching two rows of which one already holds the value) and Rust (sqlx's FOUND_ROWS capability) already did, and Python's PyMySQL did not (measured 1) and now passes CLIENT.FOUND_ROWS (measured 2, with packages/orm-python/tests/test_mysql_found_rows.py red on the earlier code). Not done: the conformance vector affected_counts over the three databases and a test of the upsert count in each client, split into T88-1. Docs: protocol.md §5 Write. | — | [o] |
| T88-1 | The conformance vector affected_counts and a test of the upsert count in each client (split from T88). Cause: T88 made the five clients report 1 for an upsert and the matched rows for an update, and the only recorded evidence is a live measurement per MySQL driver and one Python test. | Acceptance: the conformance vector affected_counts (an upsert that inserts, an upsert that updates, and an update matching two rows of which one already holds the value) gives identical results in the three vector files and runs in the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust), each from a test in its own package; packages/orm-python/tests/test_mysql_found_rows.py is declared in contracts/features.json. | Evidence: the test entry point of protocol.md 3.4 reads the affected count of the last write in the five clients (Go orm.LastAffected under ormtest, Rust testing::last_affected under test-faults, TypeScript @polyspec/orm/testing lastAffected, PHP Faults::lastAffected, Python polyspec.orm.testing.last_affected), and the conformance vector affected_counts gives 1 for an upsert that inserts, an upsert that updates, and an update of a row that already holds its value, identical in the three vector files. The acceptance named an update matching two rows, but the model DSL updates one loaded row (usage.md section 7), so the vector proves the rule (matched rows, not changed rows) on one unchanged row. RED: without the Go upsert override and clientFoundRows, Go gave upsert_updated 2 and update_unchanged 0 on MySQL. test_mysql_found_rows.py is declared in contracts/features.json. The PHP extension query vectors are T182 work. | [o] |
| T89 | Every client checks a parameter against its slot's logical type, and ir.ParamUses lists the slots. Cause: protocol-defect fix; this is the CODEC_ENCODE standard that T73 leaves to its own item. dialects.md says clients reject excess scale (:89), length (:91), U+0000 (:94) and fraction digits past p (:110), but the bind paths check only decimals and SQLite times (exec.go:160-197), so on the same request MySQL coerces or answers 1406 or 1264, PostgreSQL answers DRIVER (22001, 22021, 42883, 22003) and SQLite answers CONSTRAINT or stores the value; an IR executor receives no plan, so it cannot learn a slot's type. | Acceptance: Red first: the new shared tests/bind/vectors.json (a slot's logical type, precision, scale, length, nullability and transform; a value; the bound value or the code) with an accept and a reject case per rule, and its bind-check target in the shape of codec-check; planner.json cases that pin Precision and Length on slots, and ParamUses cases (a column function, an interval, contains, if_parent, optimistic); the conformance vectors datetime_precision_rejected, varchar_length_rejected, text_nul_rejected and uuid_form_rejected, whose three files differ today. Then ir.ParamUses(model, request) in Go and its counterparts in the four other validators list each parameter's uses with its logical type, the type before any SQL-side style and distinct from bind_slots[].col_type: the compared or assigned column with precision, scale, length and nullability; a column function's result (functionType and valueFunctionArgType move from the planner into engine/ir); i32 or f64 for a relative interval; text for contains and contains_binary; the parent column for if_parent; the column for optimistic. plan.BindSlot carries Precision for time and datetime and a new Length for varchar, set by colParam in the five planners. Before any statement, the five bind paths answer CODEC_ENCODE naming the parameter and the type for a value outside its slot's logical type: the host value kind, the i16/i32/i64 range, a non-finite f64, a decimal's precision and scale, a date outside 0001-01-01..9999-12-31, a time or datetime with a non-zero digit past p (a zero is accepted), a varchar longer than n characters with trailing spaces counted, U+0000 in text or varchar, and a uuid not in lower-case canonical form; NULL is accepted except as T90 states; a codec column's slot checks the value against its first stage's input. The conformance results are CODEC_ENCODE before any statement, identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the bind, planner.json and conformance vectors, each from a test in its own package. Docs (EN and .ko.md): protocol.md §2 bind_slots and §5 Parameters ("every client checks each parameter against its slot's logical type before any statement, and a value outside it is CODEC_ENCODE; an IR executor answers the same"), dialects.md:89, 91, 94 and 110 (clients reject with CODEC_ENCODE; a zero past p is accepted), dialects.md:55 (the logical slot type against col_type), and the errors.yaml CODEC_ENCODE comment (kind, range, scale and precision, time precision, length, U+0000, uuid form, a styled input that cannot be encoded, SQL NULL for a non-null column). Requires T91 and T92. | Evidence: done through T89-1 (slot lengths and parameter uses pinned in the five planners) and T89-2 (every parameter checked against its slot type before a statement, with make bind-check and four conformance vectors); T89-3 holds the native time values the item found. | [o] |
| T89-1 | Pin the slot lengths and the parameter uses in the five planners (part of T89). | — | Evidence: every varchar slot carries its length in Go, PHP, TypeScript, Python and Rust (the PHP runtime model gained the varchar length), the TypeScript and Python planners name an unknown function result text as Go does, and planner.json pins length on every varchar slot with five param_uses cases (column function, interval, contains, if_parent, optimistic); RED: the TypeScript, Python, PHP and Go runners failed with length 0 against 191; all five pass. | [o] |
| T89-2 | Check every parameter against its slot type before a statement (part of T89). | — | Evidence: the five clients check each parameter on SQL connections and in the IR executor and answer CODEC_ENCODE before any statement; tests/bind/vectors.json gains a bind section with an accepting and a rejecting case per rule, run by the new make bind-check in the five clients, and the conformance vectors datetime_precision_rejected, varchar_length_rejected, text_nul_rejected and uuid_form_rejected on the bench table bind_cell are identical in the three vector files; RED: MySQL answered DRIVER for varchar, stored NUL and gave CONSTRAINT for uuid, PostgreSQL answered DRIVER and stored an uppercase uuid, PHP stored a datetime past p and Python failed with a JSON TypeError. Defects fixed on the way: PHP dropped a non-zero digit past p, Python failed on datetime JSON and near year 1, and TypeScript read years below 100 as 1900 and more. | [o] |
| T89-3 | Accept the same native time values in every client (follows T89). Cause: the shared bind vectors use text values only, and the clients accept different native values for date, time and datetime slots: a TypeScript Date cannot fill a time slot, Rust has no native time value, and PHP, Python and Go accept their own (found in T89). | Acceptance: the protocol states which native value of each language fills each time slot, each client accepts exactly those, and a shared vector proves it in the five clients. | Evidence: commit fbca32dc (main): RED before the fix: Go accepted 5 of 5 native cases, TypeScript and Rust 3 failed (native date and datetime accepted), PHP and Python failed; GREEN: bind-check passes in all five, and go test of the engine passes. Scope: date, time and datetime slots, text only; the now slot is the client clock and is not bound from a caller value. interface-check: the TypeScript snapshot hash is the checker's value sha256:cdc43cdb…. The generated setters still type native values; that is T189. | [o] |
| T89-3-1 | Make the conformance physical stage pass with text inputs for date and time (follows T89-3). Cause: T89-3 refuses native time values at the bind layer, but tests/conformance/vectors.json's write_cycle still feeds native datetime and time values (Python datetime, Go time.Time, PHP DateTimeImmutable, TS Date, Rust chrono) into datetime(6) and time slots, so the conformance physical stage aborts in every runner (Go panic at runner_go/main.go:241; Python CODEC_ENCODE at runner_python.py:766). Red: make run-databases TARGETS=conformance-check aborts in the physical stage. | Acceptance: write_cycle's datetime(6) and time inputs become the canonical text in the vector and in every runner, with no other change; the physical stage completes in all five runners and the three vector files are identical. | Evidence: commit 9eb65ebb (fix/row-T89-3-2, on main after the ff-merge; from 5e61acd5 before the rebase): the runners pass text for the date, time and datetime inputs of write_cycle, now_defaults, restore and datetime_precision_rejected in Go, Rust, PHP, TypeScript and Python; expectations unchanged. RED: the physical stage aborts in every runner (CODEC_ENCODE: parameter 3 (time.Time) is not a datetime value, and the PHP and TS setStartDt inputs). GREEN (shared with T89-3-2): make run-databases TARGETS=conformance-check exit 0, PASS check/conformance-check, zero FAIL lines (log t891-physical3.log). | [o] |
| T89-3-2 | The optimistic update binds the loaded row's version as canonical text, in Go and Rust (follows T89-3). Cause: Core.Update(optimistic=true) binds the loaded row's update-time version as r.param(version), where version is the native time.Time of the original row (packages/go/orm/model.go), and Rust's update(true) on the loaded row does the same; T89-3's bind check refuses native time values for datetime and time slots, so conformance write_cycle's loaded.SetName(...).Update(true) fails in Go and Rust. Red: an update(true) on a datetime(6) and a time column fails with CODEC_ENCODE in Go and Rust. | Acceptance: the version is bound in its canonical text form in every client that binds it (Go, Rust, and any other that does), the red case passes, and the physical stage gets past write_cycle's optimistic update. | Evidence: commit 28b9871b (fix/row-T89-3-2, on main after the ff-merge; from 9b1c879d before the rebase): Go and Rust bind the loaded version as canonical text in update(true). RED: TestWrites on mysql panics with CODEC_ENCODE: parameter 3 (time.Time) is not a datetime value; Rust write_cycle gives CODEC_ENCODE with a DateTime parameter. GREEN: TestWrites ok; shared physical stage exit 0 (see T89-3-1). TypeScript and Python: already satisfied, no fix needed: the optimisticText case and test_optimistic_text bind the loaded version as text with string seeds, passing on sqlite, mysql and postgres. | [o] |
| T89-4 | Run the Python bind vectors through the case harness (follows T89-2). Cause: the bind-check line of the Makefile ran python -m unittest packages.python.tests.test_markdown.BindMessageVectors …, which test_testcase.py refuses (every Python test runs through packages.python.tests.testcase), so python-check failed on main (ci run 38723975175). | Acceptance: the line runs the two classes through the harness and python-unit-check passes. | Evidence: python-unit-check failed with the bare line listed; the line now runs python -m packages.python.tests.testcase with the same two classes, python-unit-check and bind-check pass. | [o] |
| T90 | NULL bound to a non-null column is CODEC_ENCODE. Cause: protocol-defect fix. A NULL parameter assigned to a NOT NULL column reaches the database and maps to DRIVER (MySQL 1048, PostgreSQL 23502, SQLite 1299), while the errors.yaml CODEC_ENCODE comment already lists SQL NULL for a non-null column for styled input, so one input has two codes by path and an IR executor has no rule. | Acceptance: Red first: a tests/bind/vectors.json case and the conformance vector null_into_not_null, whose three files answer DRIVER with three driver codes today. Then the assignment slot (set, rows, on_duplicate, the restore set) carries the column's nullability from the slot of T89, the five bind paths answer CODEC_ENCODE "parameter %d assigns NULL to the non-null %s" before any statement, and the driver mapping of 1048, 23502 and 1299 stays DRIVER for raw SQL; the conformance results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package. Docs (EN and .ko.md): protocol.md §5 Parameters, and the errors.yaml comment (raw SQL keeps DRIVER). Requires T89. | Evidence: assignment slots of a non-null column carry not_null and column in the five planners, and the five clients fail with CODEC_ENCODE ("parameter %d assigns NULL to the non-null %s") before any statement on both paths; the null-into-not-null bind cases, the planner.json pins and the conformance vector null_into_not_null are identical in the three vector files. RED: DRIVER in four clients on the three databases, and a TypeError of the PHP typed setter. The PHP extension query vectors are T182 work. | [o] |
| T91 | A time value has one form in an IR executor message. Cause: protocol-defect fix. protocol.md:193 defines the runtime value model JSON of an IR executor message for decimal, bytes and datetime only, so a time parameter or answer value has no form an executor can decode consistently (flowmark refuses 10:00:00.000000 for time(0)). | Acceptance: Red first: IR message cases of tests/bind/vectors.json for time(0) and time(3) (the encoded JSON); no client writes six digits for time(0) today. Then the five clients' IR executor message encoders write a time as HH:MM:SS.ffffff with zeros past p; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the cases, each from a test in its own package, and a client that has no IR message encoder yet takes the case into its executor item (T63 to T67) as acceptance. Docs (EN and .ko.md): protocol.md §5 Parameters ("a datetime is YYYY-MM-DD HH:MM:SS.ffffff in UTC and a time is HH:MM:SS.ffffff; a value of datetime(p) or time(p) has zeros past p"). | Evidence: tests/bind/vectors.json section message (10 cases) runs in the five clients; RED: Go, TypeScript and Rust failed 8 of 10 ("10:00:00" stayed unchanged, "10:00" was accepted), PHP failed at time-p0, Python passed; the four encoders now write a time, date or datetime string with six fraction digits in the connection zone and answer CODEC_ENCODE for other text, and all five pass. The PHP extension query vectors are T182 work. | [o] |
| T91-1 | Start the Go bind vector test as a case (follows T91). Cause: TestBindMessageVectors of packages/go/orm/markdown_param_test.go did not start its case with packages/go/testcase, so go-vet-check failed on main ("does not start its case with packages/go/testcase"). | Acceptance: the test starts a Compute case and go-vet-check passes. | Evidence: the main ci run of 8b230229 failed go-vet-check with that line; the test now calls testcase.Start(t, testcase.Compute), passes, and go-vet-check passes. | [o] |
| T92 | SQLite binds datetime(p) and time(p) with exactly p fraction digits. Cause: protocol-defect fix. dialects.md:48 says SQLite datetimes are text with six fraction digits and the Go bind path writes six (sqlite_time.go:40), but the renderer CHECK requires exactly p digits (render.go:445-457, length = 20+p and 9+p), so a bound datetime(p<6) value violates its own column's CHECK. | Acceptance: Red first: the conformance vectors datetime_p0_roundtrip and time_p3_roundtrip (insert, read back, compare with a parameter); the SQLite insert of a datetime(0) parameter fails its CHECK (CONSTRAINT) today. Then the five clients on SQLite bind a datetime or time value with exactly p fraction digits (T89 refuses a non-zero digit past p first, so only zeros are dropped); the results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package. Docs (EN and .ko.md): dialects.md:48 ("SQLite datetimes are UTC text YYYY-MM-DD HH:MM:SS with exactly p fraction digits, and times HH:MM:SS with p digits, as the renderer CHECK requires; the client binds that form"). State: the planner puts the column's precision on every datetime and time parameter slot (six for a function's argument or result), the shared planner fixture pins it in Go, PHP, Python, Rust and TypeScript, and the Go client binds p digits, drops zero digits past p and refuses a non-zero one as CODEC_ENCODE (f0eb77d8, TestSQLiteTimePrecision); the conformance vectors datetime_p0_roundtrip and time_p3_roundtrip in the six implementations remain. | Evidence: the bench table time_cell, the conformance vectors datetime_p0_roundtrip and time_p3_roundtrip in the five runners and the planner cases planner_time_select and planner_time_insert; RED: Rust failed on every database (time value is not HH:MM:SS with 3 fraction digits), PHP on SQLite hit the time_cell$clock CHECK, Python failed CODEC_DECODE on MySQL and PostgreSQL, and Go and Rust bound six digits where the others bound p; every client now binds datetime and time text with exactly p digits, the four other planners render the time(p) SQL the Rust planner rendered, and the vectors are identical in the three vector files with a clean re-run. The PHP extension query vectors are T182 work. | [o] |
| T92-1 | Render a PostgreSQL uuid column in one form in every planner (follows T92). Cause: the Rust planner wraps a PostgreSQL uuid column in CAST(... AS text) and the Go, PHP, TypeScript and Python planners do not, so the clients send different SQL for the same read; no planner case covers it (found in T92). | Acceptance: a planner case of a uuid read on PostgreSQL fails in the planners that differ, then all five render one SQL and a conformance vector reads the same value in every client. | Evidence: the planner cases planner_uuid_select and planner_uuid_insert failed on PostgreSQL in Go, TypeScript, PHP and Python (a bare column and $1) and passed in Rust; the four planners now read CAST(col AS text) and bind CAST(CAST(? AS text) AS uuid) as Rust does, all five pass, and the conformance vector uuid_roundtrip on the bench table uuid_cell reads the same uuid on the three databases. The PHP extension query vectors are T182 work. | [o] |
| T92-2 | Assert the T92 time rule in the Rust value test (follows T92). Cause: packages/rust/orm/tests/value_types.rs still expects CODEC_ENCODE for 08:30:00.25 in a time(3) column, which T92 made valid (fewer digits read as zeros), so runtime_value_types failed on main in clients_db, clients_pooler and feature_verify_rust ("sqlite: time text without p fraction digits"). | Acceptance: the test writes 08:30:00.2501, a non-zero digit past p, expects CODEC_ENCODE, and passes on MySQL, PostgreSQL and SQLite. | Evidence: the main ci run 38695977729 of a807cd4b passed every job, among them clients_db, clients_pooler and feature_verify_rust, which run runtime_value_types on MySQL, PostgreSQL and SQLite with the new assertion; the run of 8b230229 had failed them with the old one. | [o] |
| T93 | A statement has one clock value, truncated when written and kept at six digits when compared. Cause: protocol-defect fix. protocol.md:139 says a clock that the ORM writes or compares keeps microseconds and a statement reads the clock once, but PostgreSQL CURRENT_TIMESTAMP and the statement_timestamp() default round into timestamp(p<6) (32.784706 → 33) while MySQL CURRENT_TIMESTAMP(p) truncates and SQLite binds the truncated slot; PostgreSQL can mix the transaction-start now() and statement_timestamp() in one statement; and the SQLite planner compares at p digits (planner.go:806-810) while MySQL and PostgreSQL compare a datetime(p) column with a six-digit NOW(6). | Acceptance: Red first: the probes mysql.now.precision_truncates, postgres.now.precision_rounds, mysql.now.compare_six_digits and postgres.now.compare_six_digits; planner.json PostgreSQL and SQLite cases (the slots, the SQLite extension); the conformance vectors clock_truncate_write (datetime(0) and datetime(3) written under a client clock fixed at .999999 through the clock test entry point of tests/conformance) and clock_compare_six_digits, where today the PostgreSQL file shows the rounded second and the SQLite comparison differs. Then MySQL keeps CURRENT_TIMESTAMP(p) and NOW(6); PostgreSQL binds the now slot truncated to p for the update time, soft deletion and every default now column an insert omits (the DDL default stays statement_timestamp() for raw SQL), and at six digits for the now value function and the relative forms; SQLite binds the slot as today with the value-function slot always at precision 6, and compares a datetime(p<6) column with a clock by extending the column to six digits (col || '.000000' for p = 0, col || '<6−p zeros>' for 0 < p < 6); the five planners and dialects do so; the results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package. Docs (EN and .ko.md): protocol.md:139 rewritten to this rule (MySQL inserts leave such a column to its database default; PostgreSQL and SQLite inserts bind the now slot, truncated to p, for every omitted default now column), dialects.md:25 (the PostgreSQL clock cell: now slot truncated to p), :46 (the UPDATE rule says CURRENT_TIMESTAMP(p) for MySQL, as mysql.go:18 renders), :112 (Current time) and :288 (defaults: raw SQL only). | Evidence: probes mysql.now.precision_truncates, mysql.now.compare_six_digits, postgres.now.precision_rounds and postgres.now.compare_six_digits pass; the planner cases planner_clock_compare and planner_clock_insert and the conformance vectors clock_truncate_write and clock_compare_six_digits on the bench table clock_cell; RED: MySQL gave {at0_ge_now:0, at3_lt_now:1}, SQLite {at0_ge_now:1, at3_lt_now:0}, and PostgreSQL changed between runs by rounding; PostgreSQL now binds the now slot truncated to p and SQLite extends a datetime(p<6) column when comparing, the five clients gain a test clock entry point (protocol.md 3.3), and the vectors are identical in the three files with a clean re-run. The PHP extension query vectors are T182 work. | [o] |
| T94 | ir.Validate refuses a primary key or identity assignment in update and restore. Cause: protocol-defect fix. One rule lives in two layers: on_duplicate is refused by ir.go:372-374, but update and restore only by the five planners (planner.go:1289, planner.ts:789, Planner.php:864, planner.py:910, planner.rs:1206), so an IR executor, whose client validates with ir.Validate and does not plan (protocol.md §5), does not see it. | Acceptance: Red first: a validator-only case in which ir.Validate accepts the request today. Then the five validators answer IR_INVALID "cannot update %s" for an update or restore set that assigns a primary key or identity column (restore: also the soft delete column, protocol.md:112), and the planner checks, now unreachable, are deleted; the existing planner.json "cannot update" cases move to validation with the same code, plus a restore case; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run them, each from a test in its own package, the PHP extension run proving the key and identity facts. Docs (EN and .ko.md): protocol.md §1.4 ("An update that assigns a primary key or identity column fails with IR_INVALID"), consistent with dsl.md:275. State: the five validators answer IR_INVALID "cannot update %s" for an update or restore set that assigns a primary key or identity column (restore: also the soft delete column), and the five planners' unreachable checks are deleted; the validator vectors (update-plain-column, update-primary-key, update-identity-key, restore-soft-delete-column, restore-primary-key, restore-plain) run in Go, PHP, TypeScript and Rust, Python runs them too, and each implementation's own cannot update seq compile case answers the same code through the validator; the PHP extension parses dbspec only and carries no validator. Docs: protocol.md §1.4. | — | [o] |
| T95 | A join child excludes soft-deleted rows. Cause: protocol-defect fix. protocol.md:118 and dbspec.md:158 and 262 say reads always exclude soft-deleted rows; the root (planner.go:280) and subquery (:1555) reads do, but the five planners omit it in join rendering (planner.go:690-708, planner.ts:494, Planner.php:398, planner.py:587, planner.rs:769). | Acceptance: Red first: planner.json join cases whose SQL lacks the condition, and the conformance vector joins_soft_deleted (an inner join drops the parent, a left join gives null). Then the five planners append AND <alias>.<soft_delete> IS NULL to a joined entity's ON clause; the results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package. Docs (EN and .ko.md): protocol.md:118 (the root, each join child in its ON condition, each relation and each subquery) and dbspec.md:158 and 262 likewise. | Evidence: commit 49546d69 (on main; the join commit is e8425dff): the five planners add AND <alias>.<soft_delete> IS NULL to a joined entity's ON. RED: the three planner.json join cases fail in Go, Rust, Python and PHP without the fix, and TypeScript throws; the vector joins_soft_deleted is DIFF on the old planners. GREEN: all five pass; the vector gives the same rows on MySQL, PostgreSQL and SQLite. Unlinked: env -u POLYSPEC_CHECKOUTS go test ./packages/go/engine/... and make feature-unit-check/owners both exit 0 on main. | [o] |
| T96 | Two compared columns meet as dbspec check operands do. Cause: protocol-defect fix. ir.Validate checks no types for *_col (ir.go:789-793), the in subquery (:705-716), join keys (:459-470) or relation keys (:516-523), so the databases disagree: PostgreSQL answers 42883 on integer = varchar where MySQL coerces and SQLite compares by affinity; date = datetime gives 1, t and 0; SQLite's numeric affinity makes decimal(4,2) = i64, decimals of different scales and i64 = f64 near 2^53 disagree; and SQLite stores time and datetime with p digits. dbspec.md:128-140 already states which columns meet. | Acceptance: Red first: planner.json reject and accept cases for each site (*_col, an in subquery, a join, relation keys), whose reject cases pass validation today, and the conformance vector column_pairs. Then the five validators require the two columns of a *_col predicate, of a column in/not_in a subquery column, of a join's left/right and of each relation key pair to meet by dbspec's table plus bytes with bytes (integers with integers, decimal of one scale, f64, bool, varchar/text, uuid, date, time or datetime of one precision, bytes), else OPERATOR_NOT_ALLOWED; one table in each implementation serves dbspec check validation and the IR validator, so the two cannot drift; the results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vectors, each from a test in its own package, the PHP extension run proving the type, precision and scale facts. Docs (EN and .ko.md): protocol.md §1.3 (a column comparison, an in subquery, a join's left/right and a relation key pair compare two columns that meet as check operands do, and two bytes columns; another pair is OPERATOR_NOT_ALLOWED), and dbspec.md (the IR uses the same table). State: the five validators require the two columns of a *_col predicate, of a column in/not_in a subquery column, of a join's left/right and of each relation key pair to meet by dbspec's table, else OPERATOR_NOT_ALLOWED; the validator vectors (eight, with an accepting and a rejecting case per site) and five planner.json cases run in Go, PHP, TypeScript and Rust, and Python runs every case of both files. The conformance vector over the three databases and the one shared table are split into T96-1. Docs: protocol.md §1.3. | — | [o] |
| T96-1 | The conformance vector column_pairs and one table for dbspec check validation and the IR validator (split from T96). Cause: T96 made the validators refuse two columns that do not meet, and each implementation still holds meets twice, in dbspec's check code and in the validator, and no vector shows the three databases agree on the pairs that meet. | Acceptance: the conformance vector column_pairs (pairs that meet and pairs that do not, for *_col, an in subquery, a join and a relation) gives identical results in the three vector files and runs in the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust), each from a test in its own package; one table per implementation serves dbspec check validation and the IR validator, so the two cannot drift. | Evidence: commits 81122bb6, e1953d29 and 49546d69 (main): one meet table per client (Go internal/meet, TypeScript meet.ts, Python meet.py, PHP Dbspec/Meet.php, Rust Type::meet_kind as #[doc(hidden)] pub, internal to polyspec-orm per link-owner's ruling). RED: the vector column_pairs has no expectation before recording. GREEN: identical on the three databases; Rust's drift (bytes = bytes) is fixed and dbspec.md has the bytes row. Unlinked: go-model-check, interface-check, go-vet-check and go-fmt-check exit 0 on main. | [o] |
| T97 | A column-comparison operator is allowed by the column's type and codec. Cause: protocol-defect fix. ir.OpAllowed accepts every *_col op on every type and codec (ir.go:268-271), such as bool gt_col or uuid lt_col, while opsByType (ir.go:212-226) refuses gt on bool, uuid and bytes, and an aes column's eq_col compares randomized ciphertexts. | Acceptance: Red first: planner.json reject cases (bool gt_col, uuid lt_col, bytes gt_col, aes eq_col), which pass validation today, and the accept case ip eq_col ip. Then the five validators allow a *_col op when its base op (eq for eq_col, and so on) is in opsByType of the left column's type and neither column has a codec, two columns whose only codec is ip take eq_col and not_eq_col because both sides are the dialect's same stored form, and anything else is OPERATOR_NOT_ALLOWED; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the cases, each from a test in its own package. Docs (EN and .ko.md): protocol.md §1.3, with T96. State: the five validators allow a *_col op when its base operator is in the left column's type and neither column has a codec (two ip-only columns take eq_col and not_eq_col), else OPERATOR_NOT_ALLOWED; the validator vectors (col-op-integers-order, col-op-ip-with-ip, col-op-ip-with-ip-not-eq, col-op-bool-order, col-op-ip-order, col-op-aes-equality) and four planner.json cases (ip eq_col ip accepted; bool gt_col, ip gt_col, aes eq_col rejected) run in Go, PHP, TypeScript and Rust, and Python runs every case of both files. The benchmark schema holds no uuid column, so the uuid lt_col case is not run; the rule reads the same table for it as for bool. The PHP extension parses dbspec only and carries no validator. Docs: protocol.md §1.3. | — | [o] |
| T98 | A relative time function outside 0001 to 9999 is NULL. Cause: protocol-defect fix. Past the range MySQL and SQLite give NULL, MySQL gives 0000-00-00 below 0001-01-01, a value outside orm's range (dialects.md:111), and PostgreSQL errors on a huge interval ("interval out of range") and returns year 10000, so one request answers a row, nothing or an error. | Acceptance: Red first: the probes mysql.relative.out_of_range_null, postgres.relative.interval_out_of_range and sqlite.relative.out_of_range_null, and the conformance vector relative_time_out_of_range (seconds_ago(1e300), days_later(4000000), months_ago(200000), each and its negation matching no row), whose PostgreSQL file errors today. Then a relative time function whose result lies outside 0001-01-01 00:00:00 to 9999-12-31 23:59:59.999999, or whose interval cannot be represented, is NULL: the five dialect function renderers guard the MySQL (functions.go:52-60) and PostgreSQL (functions.go:94-110) renderings with CASE WHEN <arg> BETWEEN <min> AND <max> THEN … END on the interval and CASE WHEN (expr) BETWEEN '0001-01-01' AND '9999-12-31 23:59:59.999999' THEN … END on the result, and SQLite takes the lower result guard only if its probe shows a non-NULL; the results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vector, each from a test in its own package. Docs (EN and .ko.md): a dialects.md rule and protocol.md §1.3 value. | Evidence: commit 596fcc9e (main): probes mysql.relative.out_of_range_null, postgres.relative.interval_out_of_range and sqlite.relative.out_of_range_null pass; the five renderers return NULL outside 0001 to 9999 with a CASE around the interval. RED: the vector relative_time_out_of_range fails on PostgreSQL with interval out of range (22008) in all five runners. GREEN: records [0,0] for days_later, months_ago and seconds_ago on all three databases. Unlinked: rust-fmt-check and the Go engine tests exit 0 on main. | [o] |
| T99 | An insert assigns by value or NULL. Cause: protocol-defect fix. The planner renders a table-qualified plus_p or minus_p reference inside VALUES (planner.go:1259-1266): MySQL computes it from the default (3 + 5 = 8), while PostgreSQL and SQLite refuse it. | Acceptance: Red first: a planner.json reject case, which plans today, and accept cases. Then the insert branch of the five validators answers IR_INVALID "insert cannot assign %s by plus or minus" for plus_p or minus_p in set, which stay valid in on_duplicate; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the cases, each from a test in its own package. Docs (EN and .ko.md): protocol.md §1.4 ("An insert assigns by p or null; plus_p and minus_p are IR_INVALID there and valid in on_duplicate"). State: the insert branch of the five validators answers IR_INVALID "insert cannot assign %s by plus or minus" for plus_p or minus_p in set, which stay valid in on_duplicate and update; the validator vectors (insert-by-value, insert-on-duplicate-plus, insert-set-plus, insert-set-minus, update-set-plus) and four planner.json cases (two accepted, two rejected) run in Go, PHP, TypeScript and Rust, and Python runs every case of both files; the planner's renderAssign is still reached by update and on_duplicate, so no planner path is deleted; the PHP extension parses dbspec only and carries no validator. Docs: protocol.md §1.4. | — | [o] |
| T100 | The dialects record the RESTRICT timing of one multi-row delete. Cause: cross-backend standard, database facts only. One DELETE that removes a referenced row and a row referencing it is refused by MySQL and SQLite when the referenced row comes first in their row order and accepted otherwise, and PostgreSQL accepts it; orm states no fact about it, and a portable program needs one. Refusing such a delete is flowmark's rule, not orm's, because orm's SQL backends do not keep it. | Acceptance: Red first: a docs-check case that requires the row fails on today's dialects.md, and the probes mysql.fk.restrict_row_order, sqlite.fk.restrict_row_order and postgres.fk.restrict_statement_end in tests/dialects are written against the facts. Then the dialects.md "Foreign keys" Actions row (EN and .ko.md) states: "One DELETE that removes a referenced row and a row referencing it is refused by MySQL and SQLite when the referenced row comes first in their row order and accepted by PostgreSQL; portable code deletes the referencing rows first." No client behavior changes, so no implementation vector is added and the suites of the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) are unchanged; the probes are the shared evidence. The markdown executor's refusal appears only in the mirror table of T102. | Evidence: commit cd105aee (main): TestRestrictRowOrder (tests/dialects) requires dialects.md and its twin to state the RESTRICT row order; RED: missing rule on both pages; TestProbeCitations RED: 3 new probes uncited on both pages; GREEN: both pass. Physical TestDialectFacts: 276 probes, 0 failed (mysql 116, postgres 86, sqlite 73). Found and not changed: the PostgreSQL column of the Actions row says RESTRICT is checked at once, while postgres.fk.restrict_statement_end passes; a follow-up row for that correction is needed. | [o] |
| T101 | The dialects record the fault order of a refused statement. Cause: cross-backend standard, database facts only. One row breaking a CHECK and a key reports the CHECK on all three databases, and PostgreSQL and SQLite check foreign keys at the end of the statement while MySQL checks them per row; ordering and listing several faults is flowmark's rule, since a SQL statement reports one error. | Acceptance: Red first: a docs-check case that requires the rule fails on today's dialects.md, and the probes mysql.fault.check_before_unique, postgres.fault.check_before_unique, sqlite.fault.check_before_unique and fault.fk_statement_end of each database are written against the facts. Then a dialects.md rule (EN and .ko.md): "A statement with several faults reports a row's CHECK fault before its key fault on every database; PostgreSQL and SQLite report foreign keys at the end of the statement and MySQL per row." No implementation vector, as in T100. | Evidence: probes mysql, postgres and sqlite .fault.check_before_unique (3819, 23514, CHECK constraint failed) and .fault.fk_statement_end (MySQL 1452; PostgreSQL and SQLite accept a reference the same statement satisfies) pass on the servers; the new compute case TestProbeCitations requires dialects.md and its twin to cite every probe and only probes or ddl.json vectors (RED: 12 does-not-cite failures), and the CHECK table gains a Fault order row. Checks pass unlinked, on installs restored from the locks (dependency-policy-check included). | [o] |
| T102 | The Markdown IR executor table equals flowmark spec 60. Cause: protocol-defect fix. dialects.md:387 says the table's rulings are those of the flowmark capability table, but the two drifted: the table has no row for the unavailable table (spec 60:60); flowmark T12.4 changes or adds the rows for ordering, group_by, insert, update, delete/restore, on_duplicate, primary key comparison, relations, scalar column subquery, column comparisons, contains, contains_binary, now/soft_delete, soft_delete and fulltext; and the on_duplicate row records a divergence that T87 removes. | Acceptance: Red first: a docs check that compares the table's surfaces and texts with flowmark docs/spec/60-dialect.md at the version pinned in a new tests/markdown/flowmark.version fails on today's table (the missing unavailable-table row, the Lb row). Then dialects.md "Markdown IR executor" (EN and .ko.md) holds one row per spec 60 row with the same surface and rule text, split into Result and Rule, and the check passes. It lands after T79 to T101, whose rules it mirrors; no client behavior changes, so the suites of the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) are unchanged. State: tests/markdown/flowmark.version pins flowmark 444f27f (spec 60 with the rulings of flowmark T12.1-38 to T12.4-9); the Go, PHP, TypeScript and Rust clients pass tests/markdown/cases.json on a flowmark built at the revision before it, which differs from it in documents only (the Python runner could not run here: this environment lacks polyspec.ordered_json); scripts/docs/markdown-table.mjs (make markdown-table-check, declared in contracts/check-inputs.json) downloads the pinned module and compares the table "Markdown IR executor" of docs/spec/dialects.md and docs/dialects.ko.md row by row with its spec 60 and Korean twin, the ruling split at the first top-level ;, :, — or . into Result and Rule and relative links made absolute; it failed on the earlier table and passes on the rewritten one. The table mirrors flowmark's spec 60, which already carries the rulings of orm T79 to T101; the clients' own work for those items (bind paths, vectors, decoders) stays in their items, and no client behavior changes. | — | [o] |
| T103 | MySQL decodes hex on the client, as the dialects table says. Cause: protocol-defect fix. dialects.md:22 says the aes and hex styles are client-side on all three databases, but the MySQL dialect decodes hex in SQL with UNHEX (engine/dialect/mysql.go:63, 76; the conformance SQL UNHEX(a.aes_hex_email)), so an invalid stored hex is NULL on MySQL and CODEC_DECODE on PostgreSQL and SQLite. | Acceptance: Red first: the conformance vector hex_invalid_stored (an invalid hex inserted by raw SQL and read back → CODEC_DECODE) answers NULL in the MySQL file today. Then the MySQL dialect of the five planners stops rendering HEX/UNHEX for a host hex stage; the results are identical in the three vector files; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the vector, each from a test in its own package. Docs (EN and .ko.md): dialects.md:22 adds "an invalid hex text is CODEC_DECODE on every database". T74 fixes which text the decode accepts. | Evidence: the read side was already client-decoded (T74, vector hex_malformed gives CODEC_DECODE); RED: with write_cycle expecting ?, the five runners still sent HEX(?) on MySQL; the MySQL planner of the five clients now writes the client-encoded hex as ?, write_cycle and hex_malformed pass on the three databases, and the planner.json cases planner_bind_types_update and _insert expect ? with varchar(255) slots. Checks pass unlinked, on installs restored from the locks (dependency-policy-check included). | [o] |
| T104 | The client sets the MySQL session sql_mode. Cause: protocol-defect fix. No client sets sql_mode (dsn.go:57-58 sets time_zone and clientFoundRows, and the others match), dialects.md:73 records the probe server's STRICT_TRANS_TABLES and the default ONLY_FULL_GROUP_BY as server facts, and MySQL's coercion, truncation and grouping answers depend on that mode, so a result depends on a server setting that the protocol does not fix. | Acceptance: Red first: the probe mysql.session.sql_mode_set and a conformance case reading @@SESSION.sql_mode on MySQL, which holds the server default today. Then the five clients set the session sql_mode on connect to one declared value, STRICT_ALL_TABLES,ONLY_FULL_GROUP_BY,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION or the set the probes justify; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the case, each from a test in its own package. Docs (EN and .ko.md): config.md and dialects.md state the session settings. | Evidence: tests/dsn/mysql-session.json declares the session sql_mode, the probe mysql.session.sql_mode_set confirms how the server reads it back, and each client reads @@SESSION.sql_mode over its own connection (Go TestMySQLSessionSQLMode, PHP test_session_sql_mode, TypeScript session_sql_mode, Rust session_sql_mode_is_the_declared_one, Python mysql_session.py through python-mysql-session-check); RED: all five had STRICT_TRANS_TABLES, Rust also PIPES_AS_CONCAT and IGNORE_SPACE from sqlx and TypeScript IGNORE_SPACE from mysql2; all five pass, through ProxySQL too. Checks pass unlinked, on installs restored from the locks (dependency-policy-check included). | [o] |
| T105 | An IR executor's read answer has one JSON shape. Cause: protocol-defect fix. protocol.md §5 Read says the rows are assembled to the request's shape but gives no JSON form: not how each kind's answer is wrapped, nor the order of members, nor the value forms (protocol.md:193 covers parameters only), so a client cannot decode an answer and a second executor could answer differently. | Acceptance: Red first: the shared answer fixture contracts/fixtures/ir_answers.json (answer JSON → assembled model array form) fails in every client without a decoder. Then protocol.md §5 Read (EN and .ko.md) states: "A read answers {"rows": [...]} for one, all and group_count, {"count": n} for count, {"value": v} for sum and avg, and {"rows": [...], "count": n} for paginate, n counting every matching row. A row is a JSON object of its outputs in projection order, then each join (an object, or null for a left join without a match) and each relation (a one relation an object or null, a many relation an array, or an object keyed by key_by); a flatten relation is answered as one (section 1.2). Values use the parameter value model: a datetime YYYY-MM-DD HH:MM:SS.ffffff, a time HH:MM:SS.ffffff, a decimal and bytes as strings, an integer or f64 a JSON number, a bool a JSON bool, a date and a uuid their text."; §5 Write states {affected, keys} with affected as T88 states. Each client's IR executor answer decoder reads this form, Go first and the others with their executor items (T63 to T67), which list the fixture as acceptance; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the fixture, each from a test in its own package. Requires T85 and T91. | Evidence: contracts/fixtures/ir_answers.json holds 13 IR read answers taken from flowmark, and make ir-answers-check runs each through a markdown conformance chain against a listener in the five clients, which all decode them alike; no RED, since every client already decoded the shape, so the fixture is the missing proof. The fixture covers integer, f64, bool, decimal, date, null, joins and one, many and null relations; the corpus has no time, bytes, uuid, datetime, key_by or flatten answer. Checks pass unlinked, on installs restored from the locks (dependency-policy-check included). | [o] |
| T106 | WRITE_CONFLICT also covers a change since the read that makes a transaction's writes others' writes, and it is never retried. Cause: protocol-defect fix. errors.yaml:54 and protocol.md:197 define WRITE_CONFLICT only as "a target changed since it was read", but an IR executor that re-runs a transaction's reads at commit (flowmark's commit re-run) finds a change since the read that turns the transaction's writes into other writes without its own target changing, and has no code for it: DEADLOCK is not one, because the clients map SQLSTATE 40001 to DEADLOCK (packages/orm-go/orm/db.go:558, errors.yaml:79), which TransactionOptions may retry, while re-running a callback over a changed read is never safe. | Acceptance: Red first: the shared answer fixture contracts/fixtures/ir_answers.json (T105) gains a commit answered WRITE_CONFLICT for a read that changed, which every client must return as WRITE_CONFLICT without running the callback again even with retry options, and which fails today in every client; the generated catalog comment (packages/orm-go/orm/codes.go, generated by cmd/orm-gen from errors.yaml) differs from the new errors.yaml text until it is regenerated. Then errors.yaml:54 reads "a target changed since it was read, or a change since the read makes the transaction's writes other writes; names the file and line; the client never retries it", every generated error catalog (Go, PHP, Rust, and TypeScript and Python through T77-4) is regenerated from it, and the five clients return the code unretried; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the fixture case and the catalog comparison, each from a test in its own package. Docs (EN and .ko.md): protocol.md §5 Errors with the same meaning, dialects.md:395 (the update row) and the client error table of usage.md, which state it and that it is not retried, unlike DEADLOCK. Requires T105 for the fixture. | Evidence: commits 1daef989 and 5ff5b555 (main): a changed read is never retried at commit. RED: ir_answers.json gained commit_write_conflict and commit_deadlock, and Rust retried a deadlocked commit; GREEN: all six implementations pass ir_answers; make ir-answers-check and go test pass on main. | [o] |
| T107 | The begin message carries the read-only option that the dialects table says the executor honours. Cause: protocol-defect fix. protocol.md:198 defines begin as {op: begin, name?} with no read-only field, while dialects.md:409 says "readOnly is honoured" for the markdown IR executor, so a client has no way to send the option and either the table is false or the client drops it. The protocol gains the field rather than the table dropping the claim, because readOnly is a transaction option of every client (usage.md:278), a write in a read-only transaction is READ_ONLY on every SQL database (config.md:66, usage.md:458), and a SQLite read-only transaction takes no write lock (config.md:25); an IR executor that ignored the option would accept writes the same program is refused on a SQL backend, a silent fallback that AGENTS forbids. | Acceptance: Red first: shared IR message cases (the IR message section of tests/bind/vectors.json, T91) give the encoded begin of a transaction with readOnly as {"op": "begin", "read_only": true} and of one without it as {"op": "begin"}, and an answer fixture case of contracts/fixtures/ir_answers.json (T105) answers a write inside it with READ_ONLY; no client encodes the field today. Then protocol.md §5 Session states {op: begin, read_only?}, where read_only true makes the executor take no write lock for the transaction and answer every write in it with READ_ONLY; each client's IR executor message encoder sends it from the readOnly option (Go first, the others with their executor items T63 to T67, which list these cases as acceptance), and decodes READ_ONLY; the six implementations (Go, PHP, PHP extension, TypeScript, Python, Rust) run the cases, each from a test in its own package. Docs (EN and .ko.md): protocol.md §5 Session, and dialects.md:409, which names the field and the READ_ONLY answer. Requires T91 and T105. | Evidence: commit 2abd1fbb (main): the begin message carries the read-only option; RED: no shared case for {op:begin, read_only:true} and no READ_ONLY answer case; GREEN: control cases in tests/bind/vectors.json pass in all five and read_only_write passes in all six; make bind-check passes on main. | [o] |
| T108 | A stage list of only base64 or only gz stores the same bytes in every client. Cause: protocol-defect fix. Rust PHP-serializes the value before such a stage while Go and TypeScript store it as plain text, so a cell one client writes decodes to another value in the others (found in T74). | Acceptance: shared cases in tests/codec/vectors.json for each single-stage list, red first in the diverging clients, then the six implementations agree, each from its own package; docs/spec/codec.md (EN and ko) states which stage lists serialize. The same suite pins two more divergences of single stages (found in the review of T74): PHP skips the UTF-8 check after base64, so +/+/ and /w== decode in PHP while Go, TypeScript and Python refuse them, and "" encoded with ["base64"] writes an empty cell that Go, TypeScript, Python and PHP refuse to decode while Rust round-trips it. | Evidence: commit 3c81acf3 (main): a stage list of only base64 or only gz stores the UTF-8 text; RED: Rust serialized values, PHP decoded +/+/ and /w==, four clients refused an empty base64 cell; GREEN: all five agree; codec-check passes (630 tests). | [o] |
| T110 | TypeScript and Python decode a request's shape as strictly as Go and PHP. Cause: protocol-defect fix. An unknown member inside ref or at the top level (such as column on a sum request) is IR_INVALID in Go and PHP and accepted in TypeScript and Python, and a non-string column is IR_INVALID in Go and PHP and COLUMN_UNKNOWN in TypeScript and Python, so one request passes in two clients and fails in two (found in the review of T73). | Acceptance: shape cases in tests/ir/validate.json (unknown members at each level, wrong member types), red first in TypeScript and Python, then all five request validators agree, each from its own package; protocol.md (EN and ko) states that unknown members and wrong types are IR_INVALID. | Evidence: commits 03df6529, 98a4d667, fb941879 (main): a request's shape is decoded strictly. RED: TypeScript and Python accepted unknown members, and a float literal in an integer slot; GREEN: 26 shape cases in tests/ir/validate.json and the float-literal case pass in all five, through ordered-json in TypeScript; go test ./packages/go/engine/... passes. | [o] |
| T109 | The ip encoder refuses an input with white space instead of trimming it. Cause: protocol-defect fix. The encoders trim white space around an ip input before packing it, so two different inputs store the same bytes and the stored value does not round-trip to the input, which T74 refuses on the decode side (found in T74). | Acceptance: shared cases with leading, trailing and inner white space give CODEC_ENCODE in all six implementations, red first; docs/spec/codec.md (EN and ko) states it. | Evidence: commit c5e1f085 (main): the ip encoder refuses white space; RED: every client trimmed padded input, PHP threw an uncoded ValueError on NUL; GREEN: all five refuse with CODEC_ENCODE; codec-check passes (690 tests). | [o] |
| T111 | The PHP extension runs the dbspec shared suites it implements. Cause: protocol-defect fix. The PHP extension implements the dbspec surface, yet it has no binding of the schema-definition suite or of the sets section of tests/dbspec/cases.json, so its parse and set results are not compared with the other five (found by T77's declaration). | Acceptance: the extension runs both suites from its own package, red first if it diverges, and the parity check reports no missing binding for it. | Evidence: the bindings were added in commit f0449c01 (#T148-3-3): config/parity.json binds the schema-definition suite to packages/php-ext/tests/CoverageSchemaDefinitionTest.php and the dbspec sets to packages/php-ext/tests/DbspecSetsTest.php::test_sets. Today on main: make dbspec-php-extension-check/sets exits 0 (7 tests, 106 assertions) and make dbspec-php-extension-check/schema-definition exits 0 (7 tests, 27 assertions); parity-declarations-check reports no missing binding for the extension. The missing wiring of the umbrella target is a new row (see T188). | [o] |
| T112 | Every shared vector case has a name each runner can report. Cause: protocol-defect fix. tests/codec/vectors.json names cases with / (json/null), which a Go subtest cannot report, and tests/codec/aes-vectors.json has no case name, so the Go codec binding cannot be declared and the AES cases are named by their key (found by T77's declaration). | Acceptance: case names without / or white space, an explicit name in every AES vector, all six implementations' readers follow, and the parity declaration binds Go to the codec suite. | Evidence: commit 9797c81c (main; fix 06f7713d before the rebase): the AES vectors gain an explicit name (aes.bench_salt) and every reader (Go, PHP, TypeScript, Python, Rust) reads name; the Go reader refuses an empty name or one with a slash or white space. The / names of tests/codec/vectors.json were already dot-joined by 4b2f1a25 (T148-3-3-1). Linked red→green: with the fix stashed, go test -run TestCodecAESVectors fails with "vector name "" is empty or holds a slash or white space"; with it, the test passes and the four other readers pass. | [o] |
| T113 | Python reads a database datetime as UTC on every machine. Cause: protocol-defect fix. PyMySQL and psycopg return datetime and timestamp columns as naive values, and _time_form (packages/orm-python/src/polyspec/orm/database.py) calls astimezone(utc) on them, which Python reads as the machine's local time; on a +0900 machine a loaded updated_ts comes back nine hours early, so the optimistic update of write_cycle matches no row and fails OPTIMISTIC_LOCK on MySQL and PostgreSQL while SQLite (text) passes, and CI on a UTC machine never shows it (found while verifying T74). | Acceptance: the conformance runners run with a non-UTC TZ (red first in Python on MySQL and PostgreSQL), every client reads a naive database datetime as UTC, and the five clients agree on every conformance case under that TZ; docs/spec/dialects.md (EN and ko) states it. | — | [o] |
| T116 | PHP binds a datetime without an offset in the connection zone. Cause: protocol-defect fix. bindTimeValue (packages/orm-php/src/Db.php:902, and :889) parses an offset-less datetime with createFromFormat and no zone, so it uses the date.timezone setting; under a zone with daylight saving a valid UTC wall clock in its gap is refused (2026-03-08 02:30:00 under America/New_York gives CODEC_ENCODE), while the other clients bind it (found in the review of T113). | Acceptance: a unit case under a daylight-saving date.timezone, red first; both sites pass the connection zone; a shared bind vector all five clients run agrees. | Evidence: commit 4911eda0 (main; fix e7648377 before the rebase): both createFromFormat sites in packages/php/src/Db.php (datetime and date) parse the offset-less wall clock in the connection zone. The row path is stale; the function is bindTimeValue in packages/php/src/Db.php. RED (unlinked run before the fix): test_bind_offsetless_datetime_uses_the_connection_zone_under_dst fails with CODEC_ENCODE at Db.php:1192. GREEN: test_bind_ passes (3 tests, 97 assertions); the new case passes alone. Shared vector datetime-dst-gap (tests/bind/vectors.json) is in bind-check for all five. Linked red→green: stashing the fix turns the case red again. Residual difference: a connection zone with a DST gap is still refused by PHP (see T116-1). | [o] |
| T116-1 | Refuse an offset-less datetime that falls in a daylight-saving gap of the connection zone, in every client (follows T116). Cause: T116 passes the connection zone, but a wall clock in the gap of a non-UTC zone (America/New_York, 2026-03-08 02:30:00) names a time that does not exist; PHP refuses it with CODEC_ENCODE, while Go normalizes it silently (the no-fallback rule). Red: Go binds it. | Acceptance: the shared bind vector gets a case with a non-UTC zone and a gap time, expecting CODEC_ENCODE naming the slot in all five clients; an ambiguous (overlap) wall clock is checked in the same vector, and any client that picks one silently fails it. | Evidence: closed as unreachable, not implemented: every connection is UTC by the ruled decision in docs/spec/dialects.md ("Time zone decision"), and the DSN refuses other zones (packages/go/orm/dsn_test.go), so a daylight-saving gap or overlap of a connection zone cannot occur; the bind vector requires the +00:00 zone (packages/go/orm/markdown_param_test.go, bind_check_test.go). The earlier gap decision was made without that rule in view; the owner decided on the UTC rule (a). | [o] |
| T117 | Python reads time and date columns on MySQL and PostgreSQL. Cause: protocol-defect fix. PyMySQL returns TIME as timedelta and psycopg as datetime.time, which _time_form (packages/orm-python/src/polyspec/orm/database.py:161-175) does not handle, and read_time builds its error message with json.dumps, so a read raises TypeError instead of an OrmError; the bench schema has no time or date column, so no conformance vector covers it (found in the review of T113). | Acceptance: time and date columns in the bench schema with conformance vectors every runner runs on the three databases, red first in Python; Python reads both; error messages never fail to format a value. | Evidence: commit d788f2cd (main, ff-merged; from 2e2a0b19 before the rebase): Python reads time and date cells on MySQL and PostgreSQL (timedelta and datetime.time, through a native time reader); RED: test_values.py NativeTimeReadTest, 4 subtests CODEC_DECODE; GREEN: 23 tests OK. The bench schema gains day_cell (label, day date, at_time time(0)) with conformance vectors date_roundtrip and date_null in the five runners; the conformance run records 50 vectors identical across 5 languages, and the clean re-run passes (make run-databases TARGETS=conformance-check exit 0). The date setter takes text (T189). Pre-commit checks pass on the rebased tree. | [o] |
| T118 | Every implementation's tests run through kit's test runner in their tool's native output. Cause: kit's run-tests and its parity check (kit K47) read each tool's native run — node:test TAP, go test JSON, cargo test, PHPUnit TeamCity, Python unittest — while orm's tests report through its own harness lines and orm's PHP tests do not use PHPUnit, so orm keeps a second test interface that every polyspec repository would otherwise share. | Acceptance: each implementation's tests and parity bindings run under run-tests with their native tool (PHPUnit pinned for PHP), the harness-line format is removed, orm syncs the kit release with K47, and parity-check reads every binding; red first, the parity check of K47 reporting orm's bindings as not executed. | — | [o] |
| T118-1 | The Go tests run through kit's runner. Cause: the Go tests (114 of the T118 red) are not declared in config/parity.json and report through the harness lines of packages/go/testcase. | Acceptance: every Go test file is declared, its output is the native go test JSON that kit's runner reads, and make -s parity-declarations-check reports no undeclared or misplaced Go test. Red: the 114 undeclared Go test files of parity-declarations-check (R5, go). | Evidence: commit dc992eec (main, ff-merged; from 582bc8a7 before the rebase): the Go suites of docs/spec, config/parity.json and the 118 feature units. RED: make -s parity-declarations-check reports 133 Go findings (114 undeclared-test, 19 misplaced) and 6 R10 docs-count lines. GREEN: 0 Go findings and 0 R10 lines; 17 gates exit 0. Benchmarks are under tools (packages/go/bench) and the analyzers under tools (tests/analyzers). Repo-wide parity-check is red for MySQL date_roundtrip in Go and Rust, a main defect tracked as T121-1, not this row. | [o] |
| T118-2 | The Rust tests run through kit's runner. Cause: 106 Rust test files are undeclared (R5, rust) and report through the harness of packages/rust/testcase. | Acceptance: every Rust test is declared and its output is cargo test's native output that kit reads; parity-declarations-check reports no Rust finding. Red: the 106 undeclared Rust test files. | Evidence: commit 6f067f5d (main; the row's run-through-runner and declare-roots commits, rebased from cee962d6 and 7ce2e0c0): Rust R5 and R6 are 0 in make -s parity-declarations-check; rust-check (check, clippy, clippy-live-db, clippy-test-faults) passes. The binding for insert_email_is_an_aes_hex_envelope is run as kit's runner runs it, cargo test --locked --offline --bin native insert_email_is_an_aes_hex_envelope -- --test-threads=1 in benchmarks/rust, with the pass line test tests::insert_email_is_an_aes_hex_envelope ... ok and test result: ok. 1 passed; 0 failed (exit 0, no database needed). The native.rs file is a rust root; benchmarks/rust is a tools root only for the rest. | [o] |
| T118-3 | The Python tests run through kit's runner. Cause: 89 Python test files are undeclared (R5, python) and report through packages/python/tests/testcase.py. | Acceptance: every Python test is declared and its output is Python's unittest native output that kit reads; parity-declarations-check reports no Python finding. Red: the 89 undeclared Python test files. | Evidence: commits 9d8a5abb, e39f7d42 and 11910889 on main (rebased from 74a34236, 99ae86a7 and ccfbd1ba): Python coverage programs are unittest classes (34 methods, each a python-feature unit with the resolver's items in subTest(db=...)); Python R5 is 15 before and 0 after in make -s parity-declarations-check; python-unit-check and lint-python pass; the owner/python targeted runs of the 15 features give the same PASS set as main (30 lines each). interface-check and client-unit-check pass after make install-php in the worktree; rust-fmt-check exits 2 on main as well (not this row's). feature-unit-check/owners fails on main too (T193). | [o] |
| T118-4 | The PHP tests run through kit's runner and the PHPUnit pin. Cause: 73 PHP test files are undeclared (R5, php) and tests/testcase.php is a harness; many PHP tests already run through the PHPUnit pinned in packages/php's composer manifest and lock. | Acceptance: every PHP test is declared, the remaining non-PHPUnit tests move to that pinned PHPUnit, and the output is PHPUnit's TeamCity output that kit reads. Red: the 73 undeclared PHP test files. | Evidence: commit ca7a5f6a on main (rebased from 8602f233; the config/parity.json conflict kept both the python-internal and php-internal units, JSON valid, no duplicate unit ids): PHP R5 is 31 before and 0 for the php implementation after (make -s parity-declarations-check; the 6 remaining R5 are php-extension, T118-6); PHP R6 is 0. Targeted PHPUnit with servers up: ModelMethodsTest 3 tests pass, SchemaSetTest 16 pass, DecimalModelDbTest 9 pass, ModelTest 69 pass (1 skipped). The full PHP suite is verified at the final batch, per the user rule. feature-unit-check/owners fails only on the T193 path (main too). | [o] |
| T118-5 | The TypeScript tests run through kit's runner. Cause: 62 TypeScript test files are undeclared (R5, typescript) and report through tests/testcase.mts. | Acceptance: every TypeScript test is declared and its output is node:test TAP that kit reads; parity-declarations-check reports no TypeScript finding. Red: the 62 undeclared TypeScript test files. | — | [o] |
| T118-5-1 | The feature resolver gives each coverage test its own database and DSN. Cause: the coverage runner injects ORM_FEATURE_DATABASE and ORM_FEATURE_DSN per item (scripts/features/coverage.mts:609-612) and reads each DSN from process.env[command.dsn_env] (:573), so one feature binding cannot resolve its own database; about 30 TypeScript coverage files fail R3 under one env. | Acceptance: scripts/features/coverage.mts exports a resolver that returns the database and DSN of a test file's coverage item from the manifest, called by each coverage test file at startup and by the feature-check runner; feature-check stops injecting the env; the go, php, python and rust feature readers take the same resolver; feature-check results are unchanged. Red: the about 30 R3 failures under one env. Green: they pass, and feature-check's results are the same as before. | Evidence: commits 2c3706d1 and 5cb177d3 on main (rebased from 0ba6cb99 and 199dda42): the coverage resolver gives each coverage test file its own database from the manifest item (per-file run, split by database, CASE <id> [<db>] PASS), Python bare cases are called, and feature-check results are the same as the baseline. Decision 25, the CASE … PASS parsers and the check that proved each: (1) scripts/features/coverage.mts observedCases, the only feature-coverage parser, proved by scripts/features/coverage.test.mts and the targeted runs of 12 features for owner/typescript and owner/python (24 parts, each 1 contracts executed); (2) scripts/features/coverage.test.mts fixtures, updated and run (18 pass, 3 pre-existing failures outside this change); (3) packages/python/tests/coverage_case_test.py, run (5 pass); (4) packages/ts/tests/coverage-case-unit.mjs, run (pass); (5) scripts/group-rows-physical-check.mts:32, its own exact match outside feature coverage, unchanged; (6) kit has no CASE parser, only the CASE_DIRECTORIES constants in tests/kit/parity.test.mts; Go, PHP and Rust read their own formats. Full feature-check on the tree: FC_EXIT 2, PASS 348, FAIL 11 (the schema_definition failures of T191 only, reproduced on clean main 3417edde). | [o] |
| T118-5-2 | The TypeScript tests are declared and run through kit's runner. Cause: 62 TypeScript test files are undeclared (T118-5) and about 33 call the harness runCase directly, so they report no named case. | Acceptance: the 17 runCase call sites become native node:test cases; a ts-internal profile cites docs/spec/parity.md#implementation-internal-tests; 62 feature units bind one TypeScript test each; helper files that report no case are support globs; make -s parity-declarations-check and make -s parity-check report no TypeScript finding. Waits for T118-5-1. Red: the 62 undeclared TypeScript test files. | Evidence: commit bcd31484 on main (the declarations, rebased from 30897c45 and 11964059): TypeScript R5 is 26 in the WIP state and 0 now (62 at HEAD before the row); packages/ts R6 is 0 (the two type-only fixtures are renamed to decimal_model.types.ts and group_rows.types.ts, kept in the tsc input, and their feature scope paths updated). npm exec --workspace packages/ts -- tsc -p tsconfig.json --noEmit exits 0. Targeted node tests: 27 of 46 units pass; the 19 DB-dependent units are verified at the final batch. feature-unit-check/owners fails only on the T193 path (main too). R7 (T77-4) and R10 (T118-9) are outside this row. Evidence: all sub-rows are closed (T118-1 to T118-9, T118-5-1, T118-5-2). Evidence on main: make -s parity-declarations-check reports R6 0 and R4 0, and each runner's own tests carry declared units (the 5 R5 left are T169-4's plan cache files, not the runners'). The K47 parity reading is each layer's native output (T118-7). | [o] |
| T118-6 | The PHP extension tests run through kit's runner. Cause: 6 php-extension test files are undeclared (R5, php-extension). | Acceptance: they are declared and their output is the native output kit reads. Red: the 6 undeclared php-extension test files. | Evidence: commit fe1e7790 on main (php-extension declarations): php-extension R5 6 → 0 in make -s parity-declarations-check; the php-extension-internal profile (reason and spec anchor docs/spec/parity.md#implementation-internal-tests) binds 16 PHPUnit test methods, and the bootstrap and declarations helpers are support. Targeted: DbspecNamesTest 9 pass, DbspecApplyCleanupTest 5 pass; DeclarationsTest test_mirrors fails (not this row; filed as T195). DbspecApplyTest needs servers and is verified at the final batch. documents-check, docs:rules-check and layout-check pass. | [o] |
| T118-7 | The harness lines are removed from every test layer, with the T24 checks rewritten. Cause: the RUN, STEP, PASS and FAIL harness lines of tests/testcase.mts, packages/go/testcase, tests/testcase.php, packages/python/tests/testcase.py and packages/rust/testcase are the second test interface; the T24 testcase-check asserts that format. | Acceptance: no test layer prints harness lines, and the T24 checks assert the native output. Red: the T24 testcase-check fails on the format once the harness lines are gone from one layer, and the parity check still reads them. | Evidence: commit feaeb955 on main (rebased from 03225869, after 7ad390a6): the five test layers print their native output (node:test TAP, go test events, unittest lines, libtest), and the T24 checks assert it. Red then green per layer: Node scripts/harness/testcase.test.mts exit 1 then 6 of 6; Go packages/go/testcase --- FAIL: TestReportForm then ok; Python 3 of 5 failing then 4 of 5 (the remaining one is T169-4's test_plan_cache_bound.py); Rust report_form FAILED then 6 passed; runners 1 of 11 failing then 11 of 11. On main: documents-check, docs:rules-check and the three harness test files (17 pass). scripts/check/report.mts still parses the harness lines of the check runner's reports; it is a check runner, not a test layer, and is tracked as its own row. | [o] |
| T118-8 | The misplaced tests and undeclared suites are declared. Cause: 76 tests are misplaced (R6) and 4 suites are undeclared (R4) in parity-declarations-check. | Acceptance: each misplaced test is moved to its owning feature and each suite is declared; the check reports no R4 or R6 finding. Red: the 76 R6 and 4 R4 findings. | Evidence: commit 592f2f74 on main (declarations, rebased from 0f30300e; the layout fix moved the three harness tests from tests/harness to scripts/harness, the tools root that already exists, and the S7 rule is unchanged): R6 misplaced-test 29 → 0 and R4 undeclared-suite 4 → 0 in make -s parity-declarations-check; the scripts, packages/dbspec-* and tests tools roots carry their reasons; the four vector files are suites (ir-answers, mysql-session, dbspec-set-decimal, dbspec-set-bench). The three moved harness tests run 17 pass (their own unit tests). layout-check, documents-check and docs:rules-check pass. The suites' case ids are verified at the final batch (R3 is not run per row). R5 php-extension, R7 and R10 are other rows. | [o] |
| T118-9 | The documented test counts match the declarations. Cause: 6 R10 findings say the test counts in docs/features.md and docs/operations/usage.md do not match the declared tests. | Acceptance: the counts are generated from the declarations, or are corrected, in both EN and KO documents; the check reports no R10 finding. Red: the 6 R10 findings. The R7 error-catalog findings belong to T77-4 and are not repeated here. | Evidence: commit d7a298f5 on main (the counts sentence of docs/spec/feature-coverage.md and .ko.md split so each list names declared profiles; KO restamped): R10 docs-count is 2 in make -s parity-declarations-check before and 0 after (main gives 2 too, so the row's earlier '6' was stale; the actual 2 are the ones fixed). documents-check, docs:rules-check and layout-check pass after make install-tools and make install-packages in the worktree. Only the declarations check ran (no suites, no servers). R7 remains for T77-4. Evidence: all sub-rows are closed (T118-1 to T118-9, T118-5-1, T118-5-2, and T118-5 just closed). Evidence on main: make -s parity-declarations-check reports R6 0 and R4 0, and each runner's own tests carry declared units (the 5 R5 left are T169-4's plan cache files, not the runners'). The K47 parity reading is each layer's native output (T118-7). | [o] |
| T119 | dbspec's store setting takes a time shard clause. Cause: protocol-defect fix of a setting orm already defines: store (T62) is parsed by all six implementations and counted in the manifest hash, and polyspec/flowmark's T12.1-35 needs a table that no row owns and that accumulates in time (an event log) to declare a time shard, which no SQL backend renders but every implementation must parse, emit and hash alike. | Acceptance: the clause names a non-null date or datetime column and a period (year, month, day); parse, emit (emit(parse(s)) == s), the setting errors (nullable or non-time column, a table with parent or order, a block table) and the manifest hash, pinned by shared cases in tests/dbspec/cases.json that all six implementations pass in make dbspec-compare-check, each from its own package; docs/spec/dbspec.md and its twin state that SQL backends ignore it; red first in all six. | — | [o] |
| T120 | Rows load with their identity values, and compiled statements execute through a public call. Cause: cross-backend standard. Loading existing rows into a database (polyspec/flowmark's export, T12.7) must keep identity keys: the planner refuses an assigned identity value ("cannot set identity column"), orm has no statement that moves a PostgreSQL identity past the loaded keys, and the Go client binds a compiled statement's parameters only in the private (*DB).args. | Acceptance: a load mode of insert that assigns identity values and, after the load, restarts each identity past its largest key on MySQL, PostgreSQL and SQLite; a public call in every client that executes a compiled statement with its parameters; shared cases all six implementations run from their own packages, red first; docs (EN and ko). | Evidence: already satisfied on main, checked clause by clause (main at c1fb4d3e): (a) insert with load mode assigns the identity values; the plain insert refuses them: packages/go/engine/planner/planner.go:1058-1065, cases load_insert, load_insert_rows, load_needs_identity_value, insert_refuses_identity pass in Go; (b) restart past the largest key on MySQL, PostgreSQL and SQLite: planner.go:185 and restartIdentityStep (planner.go:1449), cases execute_load_then_restart and execute_restart_after_delete pass on all three drivers; (c) a public execute of a compiled statement with its parameters in every client: Go DB.Execute (packages/go/orm/exec.go:332), TypeScript execute (packages/ts/src/database.ts:1119), PHP execute (packages/php/src/Db.php:1064), Python execute (packages/python/src/polyspec/orm/database.py:1467), Rust execute (packages/rust/orm/src/db.rs:899); (d) shared cases from contracts/fixtures/identity_load.json (16 cases) run in each package: Go TestIdentityLoad, Python python-identity-load-check (16 tests OK), TypeScript identity-load.mjs (16 pass), Rust identity_load (16 passed), PHP IdentityLoadTest (16 tests, 93 assertions). Landed in 606510f7 and bbf4b7d2 (T120 itself), with tests in 229dc8aa and 964f9949. Parity lists five implementations for this suite; the sixth of the row is the PHP extension, whose query layer is T182. | [o] |
| T121 | Every client writes one temporal form in toArray. Cause: protocol-defect fix. Go and PHP write a date or datetime as YYYY-MM-DD HH:MM:SS.ffffff in toArray while Python and TypeScript keep the value-model text YYYY-MM-DD, so one row compares differently by client; the markdown executor's relation_many case could not pass in Python without a README note (found in T65). | Acceptance: protocol states one temporal form for each of date, datetime and time in toArray; a shared vector all clients run from their own packages, red first in the diverging ones; docs EN and ko. | Evidence: commit ae1e180d (main, ff-merged): toArray gives six fraction digits for datetime and time in every client, independent of column precision (protocol section 5). RED: time_p3_roundtrip and datetime_p0_roundtrip DIFF in all five clients against the six-digit expectations (conformance-check exit 2). GREEN: make run-databases TARGETS=conformance-check exit 0, 48 vectors across 5 languages identical. Rust ir-answers still fails to compile (E0308 at packages/rust/tests/tests/markdown_conformance.rs:374); that waits on T190. | [o] |
| T121-1 | MySQL date_roundtrip passes in every client. Cause: the conformance vector date_roundtrip fails in Go and Rust on MySQL (MySQL returns a time form that the vector does not expect, with 49 of 50 passing); it is main's red and blocks the repo-wide parity-check (found in T118-1). | Acceptance: make run-databases TARGETS=conformance-check exit 0 on MySQL, PostgreSQL and SQLite in all five runners, 50 of 50; the cause is found from the commit that introduced the vector or its read path, and the side that disagrees with protocol section 5 (six fraction digits for time) is the one fixed. Red: Go and Rust fail date_roundtrip on MySQL. Green: 50 of 50 on all three databases in all five runners. | Evidence: commit 3417edde (main; the vector fix from be86ab39, rebased): the protocol's six-digit time form is canonical (section 5), so the vector date_roundtrip was the wrong side; the five runners already write 10:11:12.000000. RED: make run-databases TARGETS=conformance-check exit 2, date_roundtrip DIFF in all five runners on MySQL, PostgreSQL and SQLite (49 of 50 per runner). GREEN: the same target exit 0, 50 of 50 in five runners on three databases. | [o] |
| T122 | A markdown group_count that matches no rows answers an empty result. Cause: the Go markdown executor's validateGroupColumns sees no columns when no row matches, so the answer fails INTERNAL: group result has no row_count; the PHP client had the same fault until T64 fixed it there, and its request_message case failed red on it first (found in T64). | Acceptance: a shared case in tests/markdown/cases.json with a group_count matching no rows, red first in Go; every client answers it alike. | Evidence: commit bd6aab37 (main, ff-merged; from 3c99b41c before the rebase): a markdown group_count that matches no rows answers an empty result. RED: group_count_no_rows answered {"error":"INTERNAL"} in Go and Python (the readers took the first row's outputs, which does not exist). GREEN: make -s markdown-conformance-check exit 0 in go, php, python, rust and typescript (the new case expects []); the Go and Python readers take the outputs from the request. | [o] |
| T123 | install-python installs the wheel lock with hashes. Cause: T115 wrote packages/orm-python/pylock.toml (9 packages with hashes) by hand-run uv, while install-python still installs the pinned packages without hashes, and nothing rewrites or checks the lock when pyproject.toml or the constraints change (found in T115). | Acceptance: after kit 0.0.26 (its K54), make python-lock writes the lock from pyproject.toml and the constraints, an offline check refuses a lock that differs from what it would write, the lock is among kit's pythonLocks, and install-python installs from it with --require-hashes; red first on a stale lock; docs (EN and ko) state it. | Evidence: commit f83d2636 (main, ff-merged): install-python installs the lock with hashes (pip install --require-hashes -r packages/python/pylock.toml, then the package with --no-deps); the lock is packages/python/pylock.toml (the row's packages/orm-python path was stale). RED: make -s python-lock-check exit 2 ('declares no pythonLocks'), a stale pin (rapidyaml 0.15.2 to 0.15.1 in constraints.txt) exit 2 naming pylock.toml and constraints.txt, and the new install test failing on the old Makefile (no --require-hashes). GREEN: python-lock-check exit 0 ('1 lock(s) match their sources'), the install test passes, python-unit-check 1328 tests OK. The test extra (jsonschema-rs) is not in the lock and installs without hashes; a follow-up row is needed if it must be hashed. | [o] |
| T125 | Every client takes a request hook on a connection handle. Cause: a program that adds the same predicates, assignments and parameters to every request of a connection had to change each model call, and the clients had no generic place before planning to do it. | Acceptance: withRequestHook in Go, PHP, Rust, TypeScript and Python returns a handle on the same connection (pool or PDO, caches, subscribers, transaction frames) whose hook receives each request of a read, count, page, insert (single and bulk), update, delete, restore and getQuery right before planning, adds parameters through the client's add-parameter call and is validated by the existing planner; rows and transactions of the handle keep the hook; the client gives the hook no meaning; docs/operations/usage.md "Request hook" (EN and ko) states it; the shared SQLite cases of contracts/fixtures/request_hook.dbs (without_hook, read_filter, insert_assign, loaded_row_writes, invalid_param) run in every client, red first; the PHP extension holds no query connection and has no hook. | Evidence (local, SQLite; MySQL and PostgreSQL run in CI): RED: TypeScript node packages/orm-npm/tests/request_hook.mjs 5 of 5 fail with db.withRequestHook is not a function; Go go test ./packages/orm-go/orm -run '^TestRequestHook$' fails to build with db.WithRequestHook undefined; PHP php packages/orm-php/tests/request_hook_test.php 5 of 5 fail with Call to undefined method Polyspec\Orm\Db::withRequestHook(); Rust cargo test -p polyspec-orm --test request_hook fails to build with E0599 no method named with_request_hook; Python unittest -p test_request_hook.py 5 errors 'Db' object has no attribute 'with_request_hook'. GREEN: the same five commands pass 5 of 5 cases each. make interface-check passes (manifest, go, php, rust, typescript, python, php-extension) after --record of the five snapshots and their hashes; go test ./contracts passes; npm run typescript:check passes; the server-free TypeScript tests (dsn 16, engine 2, generate 1, generate-scan 3, codec-vector 2, styled-value 15, dbspec 300, dbspec-rules 48, package-declarations 4, param-uses 1, markdown 4) pass; PHP dsn 13, markdown 6, relation_keys 1, hostcodec 1, engine 1, runtime_model 9, orm_gen 6, perf_extensions 2 pass; Go go test ./packages/orm-go/... passes 101 top-level tests and fails only the 71 that require the server DSNs; Python unit discovery runs 135: 129 pass, 5 need the server DSNs and test_markdown cannot import polyspec.orm.markdown, which main does not track; Rust cargo test -p polyspec-orm --features test-faults --lib 29 pass, 11 need the server DSNs, and cargo clippy -p polyspec-orm --lib --test request_hook -- -D warnings is clean. | [o] |
| T125-3 | The development rules let a standard tool keep its own behavior. Cause: AGENTS.md forbade every symbolic link, so the symbolic link that Composer creates for the path repository packages/orm-php broke the rule although this repository does not create it, and the rules did not say how a path is resolved. | Acceptance: AGENTS.md and AGENTS.ko.md forbid symbolic links in what this repository creates, require the standard tool of each ecosystem as it is (uv for Python interpreters, environments and locks, Composer for PHP dependencies) with its own behavior, and resolve a path declaratively or by discovery, never as a relative path. | Evidence: make documents-check passes 39 pairs and make docs-rules-check passes. | [o] |
| T125-4 | The request hook is a feature of contracts/features.json with database coverage. Cause: T125 verified the hook on SQLite only, and the language-parity check requires every test file of a language to belong to a feature. | Acceptance: the feature request_hook declares the Go, PHP, Rust and TypeScript owners with the five shared cases on MySQL, PostgreSQL and SQLite, each case on a case database of its own so the checked database stays unchanged, and the unit commands as its verification; Python stays planned because the coverage checker has no Python runner, and its unit test runs in make python-unit-check. | Evidence: node scripts/features/check.mjs passes 19 feature contracts; node scripts/features/coverage.mjs --feature request_hook under the environment of scripts/check/databases.sh create passes the 24 runs (go, php, rust, typescript × mysql, postgres, sqlite × 2) with equal state before and after. | [o] |
| T125-7 | make python-unit-check runs only the server-free Python tests. Cause: the identity load test was named test_identity_load.py, so the unit discovery unittest discover -p 'test_*.py' ran its MySQL and PostgreSQL cases without the server environment and failed five cases, while make python-identity-load-check runs the same file with that environment; T78-2 named the statement fault test statement_fault.py for the same reason. | Acceptance: the file is packages/orm-python/tests/identity_load.py, and the Makefile, config/parity.json and contracts/features.json name it. | Evidence: RED: make python-unit-check runs 139 tests and fails 5 with ORM_TEST_MYSQL_DSN is required for the mysql identity load cases; GREEN: it runs 138 tests OK, make python-identity-load-check passes its 6 cases on SQLite, MySQL and PostgreSQL, and node scripts/features/check.mjs passes. | [o] |
| T125-8 | The request hook passes the repository checks of make check. Cause: the full check of T125 found three gaps: the Go request hook tests did not start their cases with internal/testcase (make repo-check), the Python interface test counted 14 connection and tool rules while Db.withRequestHook is the fifteenth (make python-check), and docs/features.md did not list the feature request_hook (make feature-unit-check). | Acceptance: each Go test starts its case with testcase.Start, the Python test counts 15 rules, and the feature documents are generated again. | Evidence: RED: make check fails repo-check with six tests that do not start their case with internal/testcase, python-check with AssertionError: 15 != 14 and feature-unit-check with generated document is stale: docs/features.md; GREEN: node scripts/repo/check.mjs passes, make python-check passes, and make feature-docs writes 19 feature rows. | [o] |
| T132 | The model_generation coverage case of Go finishes within its deadline. Cause: TestCoverageModelGenerationCheck runs orm-gen --check in place and in a fresh copy of the module, and the Go generator scan reloaded every scanned package once more for each build-tag configuration it found (featurecoverage in the model package), with go list -export recompiling them in the copy; under the full suite the second run passed the 120 s deadline of the coverage checker (model_generation/owner/go/none run 2: timeout after 120000 ms, twice). | Acceptance: the default configuration loads every scanned package once; a build-tag or platform configuration loads only the packages that hold its files (generator/gen_go_scan.go), with the same generated models; docs/spec/dsl.md (EN and ko) states it; the deadline is unchanged. | Evidence: RED: the new TestScanLoadsTaggedConfigurationsOnlyForTheirPackages fails with the integration configuration loads ["./..."], want only ["<dir>/tagged"]. GREEN: it passes and TestScanGeneratesUsedMethods passes (tags and GOOS); go test ./generator passes. Times under a load average of 40-60: the coverage case node scripts/features/coverage.mjs --feature model_generation --part owner/go took run 1 1m38s and run 2 1m14s before, and 31.7 s and 33.6 s after; orm-gen --check of the model package used 27 s of CPU before and 21 s after. | [o] |
| T115-1 | The Python package declares the Python it is tested with. Cause: packages/orm-python/pyproject.toml declares requires-python >=3.11 and a CI leg runs 3.11, but its dependency polyspec-ordered-json 0.0.8 requires 3.14, so the package cannot install on 3.11–3.13; ordered-json's T1.32-2 rule is that requires-python states the minor the CI tests (the pinned .python-version), not an untested floor (found in T115). | Acceptance: a test that compares requires-python with .python-version, red first on >=3.11; requires-python >=3.14; the 3.11 CI leg, its repo-check test and the ci.yml comment removed; README and docs (EN and ko) state the supported Python. | Evidence (local): RED: python3.11 -m unittest discover -s packages/orm-python/tests -p 'test_requires_python.py' fails with AssertionError: '>=3.11' != '>=3.14' on the floor (1 test). GREEN: the same test passes after requires-python = ">=3.14" (1 test OK). make repo-check/unit runs 89 tests: 87 pass, and the python matrix case and the leg suffixes are gone; the 2 failures (every workflow job runs on the runner of .github/runner and every workflow that runs on push cancels the run of the push before it) read only .github/workflows/release.yml, which this item does not touch. make version-check passes (25 declarations), make docs-rules-check passes (39 pairs), and the Python unit discovery in a scratch venv of the pinned dependencies with Python 3.14.2 runs 131 tests: 126 pass, and 5 need the database servers of make client-db-check or fail in the SQLite NOWAIT lock case of test_driver_errors, which this item does not touch. make owner-check cannot select on this checkout: contracts/check-inputs.json declares parity-check, which is not in CHECK_TARGETS (commit 15f5faf3), so the owner selector stops before it selects. | [o] |
| T115-2 | The Python venv is built from the pinned interpreter. Cause: install-python (Makefile) runs python3 -m venv from PATH, which on a machine with Xcode's Python 3.9 builds a 3.9 venv, so python-check imports the client on a Python the package does not declare (T115-1), while config/toolchain.json declares pythonBuild and kit's make python-venv builds from it (found in T115). | Acceptance: install-python uses kit's python-venv with the pinned packages, and python-venv-check refuses a venv whose interpreter differs from .python-version; red first with a PATH python3 of another minor. | Evidence (local): RED: python-venv-check passes on a venv of Python 3.9.6 (a scratch venv of /usr/bin/python3, exit 0), and make install-python PYTHON_VENV=<scratch> with a PATH python3 of 3.9.6 builds a 3.9.6 venv (pyvenv.cfg home Xcode), then pip fails on psycopg[binary]==3.3.6 with pip 21.2.4. scripts/repo/python-venv.test.mjs fails 2 of 3 cases (the refusal case and the dry run of install-python, which shows python3 -m venv). GREEN: the 3.9.6 packages/orm-python/.venv is removed and make install-python builds it from var/tools/bin/python3 (Python 3.14.8, home var/tools/python-build) and installs the pinned packages, which import; make python-venv-check passes; scripts/repo/python-venv.test.mjs 3 tests pass; make repo-check/unit 93 tests pass; node scripts/repo/check.mjs passes; make -k python-check: python-unit-check 131 tests, 126 pass, 4 fail and 1 error, all needing the database DSNs of TEST_ENV (statement-fault cases, not set here) or the SQLite fault frame of test_driver_errors (a TypeError in that test's own code, not this item); python-interface-unit-check 13 pass; python-model-check and conformance-result-check/python pass. No database server was started or stopped. | [o] |
| T115 | The Python package releases as a wheel. Cause: config/release.json releases packages/orm-python with the kind python, not kit's wheel kind (K29), so no release carries the PEP 427 wheel that polyspec/fsm installs. | Acceptance: packages/orm-python/pyproject.toml pins each Requires-Dist as name==version; config/release.json declares the wheel kind; the release proof installs the wheel offline and imports it; docs (EN and ko) state the release form; red first, a release-proof run that finds no wheel. Done: the acceptance's name==version pin applies to registry packages; kit 0.0.25 (K49), synced in T115, requires a polyspec dependency as a direct reference to its release asset with its sha256, so polyspec-ordered-json is @ https://github.com/polyspec/ordered-json/releases/download/v0.0.8/polyspec_ordered_json-0.0.8-py3-none-any.whl#sha256=db98…645a, and config/release.json declares consumers.wheel.lock (packages/orm-python/pylock.toml, 9 packages with sha256, written by kit's pinned uv 0.12.11) and consumers.wheel.polyspec. Red: release.mjs assets v0.0.7 at main 9ddc9665 built no wheel (proof.gitTag.packages/orm-python/pyproject.toml.kind is "python"). Green on the rebased tree: assets built polyspec_orm-0.0.7-py3-none-any.whl; the release-consumer path installed the lock (9 requirements, --require-hashes), the wheel (--no-index), passed pip check and imported polyspec.orm; kit-check, release coverage, repo-check, interface-check, documents-check and docs-rules-check pass. The repeatable make python-lock and its offline check come with kit 0.0.26 (K54). | — | [o] |
| T114 | Release 0.0.8. Cause: polyspec/fsm waits on a released Python wheel of orm (its S-34-3), while the release that carries the markdown executors (T69) is far off; the user chose an intermediate release of what main holds. | Acceptance: T73, T74, T75, T113 and the other session's T78 are on main with no item in progress; the release procedure of the previous release items (T52, T56) for 0.0.8, the Python wheel among the assets, ending with the maintainer's tag. Requires T73, T74, T75, T113, T78 and T115. State: the release commit sets 0.0.8 in every manifest, lock and quoting document, records the dependency review of 2026-10-11 (ordered-json 0.0.8 in every client) and holds the consumer locks of 0.0.8. With a local tag deleted afterwards, make version-check, make release-versions TAG=v0.0.8, make release-go-tags TAG=v0.0.8, make release-coverage, make release-assets TAG=v0.0.8, make dependency-policy-check, make documents-check, make docs-rules-check, make repo-check and make release-consumer TAG=v0.0.8 exit 0; the assets hold polyspec_orm-0.0.8-py3-none-any.whl, and the consumer installs it with its 9 locked requirements, passes pip check and imports polyspec.orm, and installs the Composer package and finds Polyspec\Orm\Db. The maintainer's tag is the last step. The maintainer's tag is the last step. Impossible: the bump commit e49cea09 was pushed in a batch whose head was a later commit, so it carries no check runs and make release-verify, which reads the check runs of the tagged commit, cannot pass; main has since moved past it, and no other commit holds the 0.0.8 declarations. 0.0.8 is superseded: its prepared declarations and this dependency review ship in 0.0.9 (T126), which also carries T61, T62 and the T83–T102 series that flowmark's T12.12 pins. Impossible: the bump commit e49cea09 was pushed in a batch whose head was a later commit, so it carries no check runs and make release-verify, which reads the check runs of the tagged commit, cannot pass; main has since moved past it, and no other commit holds the 0.0.8 declarations. 0.0.8 is superseded: its prepared declarations and its dependency review ship in 0.0.9 (T126), which also carries T61, T62 and the T83–T102 series that flowmark's T12.12 pins. | — | [-] cause: polyspec/fsm waits on a released Python wheel of orm (its S-34-3), while the release that carries the markdown executors (T69) is far off; the user chose an intermediate release of what main holds |
| T126 | Release 0.0.9. Cause: flowmark's T12.12 requires a released orm tag that carries T61, T62 and the T83–T102 validator series, and T114's 0.0.8 was never tagged (its bump commit has no check runs), so its section and every later change ship as 0.0.9; polyspec/fsm's wheel wait (S-34-3) is met by the same release. | Acceptance: the release procedure of the previous release items (T52, T56, T114) for 0.0.9: the manifests, the locks, VERSION, the version of contracts/features.json, the version of the PHP extension, the documents and the consumer locks of tests/release-consumer declare 0.0.9; with a local tag deleted afterwards, make version-check, make release-versions TAG=v0.0.9, make release-go-tags TAG=v0.0.9, make release-coverage, make release-assets TAG=v0.0.9, make dependency-policy-check, make documents-check, make docs-rules-check, make repo-check and make release-consumer TAG=v0.0.9 exit 0, the assets hold polyspec_orm-0.0.9-py3-none-any.whl, and the consumer installs it with its locked requirements, passes pip check and imports polyspec.orm, and installs the Composer package and finds Polyspec\Orm\Db; then the maintainer tags the release commit v0.0.9 (annotated) and pushes it, and the release workflow is green. Requires main green. State: the release commit ddcdca4f sets 0.0.9 everywhere (its ancestors carry the version set, the consumer locks written against the local tag, and the checklist), its CI run concluded success after one rerun of a timing-sensitive test-server case, and the maintainer's tag v0.0.9 on it published the release: the workflow built the four archives (npm tgz, two Composer zips, the Python wheel), proved the draft — the consumer installs of npm, Composer and the wheel, the git-tag installs, the Go module tags — and made it public; gh release view v0.0.9 lists the four assets. The release needed five fixes along the way, each recorded where it belongs: kit's install-go downloads the toolchain in a scratch module (K65), the release workflow puts the installed tools on PATH (K66) and sets up the PHP of .php-version with coverage none, release-tools warms the Go modules and the node modules of the checkout, and go.mod keeps the minor Go the repository rule states. polyspec/fsm's wheel wait (S-34-3) is met by polyspec_orm-0.0.9-py3-none-any.whl. | — | [o] |
| T126-1 | Vendor kit from its released tag, and build the release assets on declared installs. Cause: to release 0.0.9, orm vendored kit from a kit branch whose fixes (K65, K66) were not on kit main, so a sync of kit main would break the release again; and the branch's release-tools guessed orm's build needs (Go modules, node modules) with a GOPROXY default value, which is a fallback. | Acceptance: kit main carries K65 and K66 and is released (v0.0.31, which also carries their corrections K66-1, K66-2 and the consumer-layout run K67); orm syncs KIT_TAG=v0.0.31 and make kit-check passes; orm's release-assets declares install-go and install-node before typescript-build, so the release builds on modules it installed itself; CI is green. | Evidence: kit main carries K65, K66, K66-1, K66-2 and K67 and is tagged v0.0.31; make kit-sync KIT_TAG=v0.0.31; config/toolchain.json moved to the uv declaration and make install-tools installed uv (sha256 checked) and Python 3.14.8; the venv of the old Python build was refused by name and remade; make kit-check repo-check toolchain-check dependency-policy-check documents-check release-coverage docs-rules-check feature-unit-check kit-test pass (kit-test 576 passed; v0.0.31 adds K66-2 and K67). | [o] |
| T126-2 | Vendor kit v0.0.35 (follows T126-1). Cause: kit v0.0.31's python-lock tests create their trees under the checkout's var/, which CI's static group lacks, so kit-test failed in run 38413640279 with ENOENT: mkdtemp …/var/python-lock-check-XXXXXX; kit v0.0.34 creates them under the system temporary directory. | Acceptance: make kit-sync KIT_TAG=v0.0.35, run twice as a sync from an older kit needs; the K79 switch stays T137. | Evidence: make kit-check reports the vendored files match the lock, make kit-test passes 575 tests, and make documents-check reads 42 document pairs with PRINCIPLES.md; scripts/docs/rules.mjs skips the files that .kit/kit.lock.json declares vendored, as AGENTS.md rules, after docs.translation-shape failed on PRINCIPLES.ko.md, and npm run docs:rules-check passes. | [o] |
| T126-2-1 | Judge only the vendored workflows in the workflow test (follows T126-2). Cause: kit v0.0.35 vendors PRINCIPLES.md and its twin besides release.yml, and the workflow test required the vendored files to be exactly release.yml, so repo-check failed in CI run 38421077014. | Acceptance: the test requires the vendored workflows to be release.yml. | Evidence: the case passes in make repo-check/unit. | [o] |
| T128 | The dbspec editor stack as a standard of every runtime. Cause: the owner asked for a highlighter and formatter like polyspec/template; the first attempt (Go-only cmd/orm-fmt, cmd/orm-lsp, cmd/orm-tokens and a regex classifier engine/dbspec/tokens.go) duplicated the grammar and followed neither template nor a standard. | Acceptance: the rulings and items of the editor stack plan, verified adversarially until a round found nothing, are all done: one grammar per runtime, analyze and format in all six implementations, the declared set file, declaration positions, the browser-safe parser entry, the language service with dbspec-fmt, the LSP, CodeMirror and VS Code adapters, the docs fence, and their release; the template, kit and flowmark items the plan names are tracked in those repositories' checklists. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Retry: start once that orm release is published. Decision (user, 2026-10-18): development is local and linked, with no release; a retry that waits on an orm release is met when the change is on orm main. State (2026-10-18): every sub-item except T128-R is done on main; the item waits only on T128-R. | — | [!] cause: the owner asked for a highlighter and formatter like polyspec/template; the first attempt (Go-only cmd/orm-fmt, cmd/orm-lsp, cmd/orm-tokens and a regex classifier engine/dbspec/tokens.go) duplicated the grammar and followed neither template nor a standard.; retry: start once that orm release is published |
| T128-1 | Remove the Go editor tools. Cause: they are the wrong design of T128 (plan Context) and turn main red: feature-unit-check finds cmd/orm-*/main.go without a behaviour test, and the docs build's go build ./cmd/orm-tokens finds no toolchain. | Acceptance: new removal commits (history is not rewritten) delete cmd/orm-fmt, cmd/orm-lsp, cmd/orm-tokens, engine/dbspec/tokens.go and its test, their feature entries, the Makefile tools target, the docs bridge (dbs.ts, the config fence and import, the style classes, the build step) and the dbs fences; the Go symbols are re-recorded with -record and the diff reviewed to be exactly the removed lines; ci and docs-pages are green. | Evidence: the four commits are reverted by a new commit; -record of the interfaces check changes nothing, and the computed Go symbol hash equals the recorded sha256:a65cb65f…7bed, the diff of contracts/symbols/go.json being exactly the 8 removed lines; make feature-unit-check repo-check dbspec-go-check docs-rules-check docs-verify-idempotent and npm run docs:check pass. | [o] |
| T128-2 | Write the rulings into the specifications. | Acceptance: docs/spec/dbspec.md (+ko) gains Syntax analysis and Formatting as ruled in the editor stack plan, and docs/spec/editor.md (+ko) states the layers, the import rules, the results and the fixtures. | Evidence: docs/spec/dbspec.md (+ko) "Syntax analysis" and "Formatting" state rulings 1 to 3, with the token classes defined from the lexer's four token kinds; the plan's lexical-only rule is corrected there, since a number is a word to the lexer; docs/spec/editor.md (+ko) states the layers, the import rules, the results, the ecosystem mechanisms and the fixtures, linked from the site's sidebar; make documents-check, npm run docs:rules-check and npm run docs:check pass. | [o] |
| T128-2-1 | Define a number by its form in the syntax analysis rules (follows T128-2). Cause: T128-2 stated from the TypeScript lexer alone that the lexer reads a number as a word, so a number on a lexical-only line would be word; Go's lexer reads a word of digits with an optional fraction as a number, so the statement was an implementation detail of one runtime, and it reversed the plan's own rule. | Acceptance: docs/spec/dbspec.md (+ko) defines a number by its form and keeps its class on a lexical-only line; the plan (+ko) states the correction. | Evidence: make documents-check and npm run docs:rules-check pass. | [o] |
| T128-2-2 | Ground the regex-grammar ruling on a name that is valid (follows T128-2). Cause: docs/spec/dbspec.md "Syntax analysis" and the plan's ruling 1 said table is a valid column name, but table is reserved (docs/spec/dbspec.md "Names"). | Acceptance: both name non-reserved keyword words that are valid column names. | Evidence: a Go parse of a table with columns key, text, references and codec succeeds and one with table reports name.format; make documents-check and npm run docs:rules-check pass. | [o] |
| T128-2-3 | Place the editor packages by the folder standard (follows T128-2). Cause: docs/spec/editor.md and the plan placed the editor stack in packages/dbspec/<part>/, reading S5 as allowing a component's parts in nested folders; S5 (kit PRINCIPLES, K70) nests only a runtime port in packages/<component>/<runtime>/, and kit's component-ports rule refuses any other nested folder. | Acceptance: docs/spec/editor.md (+ko), the plan (+ko) and the checklist name packages/dbspec-language, packages/dbspec-lsp, packages/dbspec-codemirror and packages/dbspec-vscode, each a component of its own without the repository prefix. | Evidence: packages/dbspec/ appears only in the statements of this correction; make documents-check and npm run docs:rules-check pass. | [o] |
| T128-2a | The set file dbspec-set.json in every runtime. Cause: the document set is declared twice, by command arguments of five generator forms; the owner ruled one declared file. | Acceptance: the file and its validation are specified with vectors; Dbspec.readSet exists in every runtime with its interface rule, symbols, parity unit and tests; every generator takes --set (Rust Builder::set) and the old forms are removed; internal callers, docs and a breaking changelog entry follow; Python's orm-gen parses arguments with argparse; parseSet is exported from the same @polyspec/orm/dbspec subpath as the T128-4 functions, with no node: module, Node global or driver. | Evidence: its sub-items T128-2a-1 to T128-2a-7 and their follow-ups are done: the set file is specified with 61 vectors, every runtime reads it and passes them, every generator takes only the set file, and the documents, the interface rule and the parity unit follow. | [o] |
| T128-2a-1 | Specify the set file with its vectors (follows T128-2a). | Acceptance: docs/spec/dbspec.md (+ko) "Document sets" states dbspec-set.json, its normal path form, the reader's four steps, the stated limit of a changing tree, the result and the set.* rules; tests/dbspec/set-files.json holds accepting and rejecting file-tree cases of every rule and of the error order. | Evidence: three adversarial review rounds left no finding (27 findings applied, the identity race solved by one handle per file with its limit stated); make documents-check, npm run docs:rules-check and npm run docs:check pass; the vectors hold 61 cases. | [o] |
| T128-2a-1-1 | Examine the root without its trailing separator (follows T128-2a-1). Cause: the root is the prefix of the given set path, which ends in a separator, and lstat("alias/") follows a symbolic link on POSIX, so the vector root-symlink would pass a linked root; the Rust agent found it. | Acceptance: step 1 examines the root without its trailing separator, a root of / staying /. | Evidence: docs/spec/dbspec.md (+ko) step 1 states it; make documents-check and npm run docs:rules-check pass. | [o] |
| T128-2a-1-2 | Decide the depth of duplicate keys and the reading of schema (follows T128-2a-1). Cause: the Python reader found two readings left open: whether "no key appears twice" covers nested objects ({"schema": {"a": 1, "a": 2}} is set.key or set.schema), and whether 1.0000000000000000001 is 1. | Acceptance: step 2 checks every object at any depth and reads schema as an IEEE 754 double, as RFC 8259 advises. | Evidence: docs/spec/dbspec.md (+ko) step 2 states both; make documents-check and npm run docs:rules-check pass. | [o] |
| T128-2a-2 | The Go set reader and orm-gen gen --set (follows T128-2a-1). | Acceptance: dbspec.ReadSet runs every case of set-files.json; runtimemodel.LoadFileSet and --document/--use are removed with every caller. | Evidence: before the change go test ./engine/dbspec -run TestReadSetVectors failed with undefined: ReadSet; after it all 61 cases pass with real links, TestGoGenerationCommandReportsSetErrors checks exit 1 and no output on a set error or file diagnostic, go generate of the three Go model packages with --set leaves the models byte-identical, GOOS=windows go build ./engine/dbspec compiles the Windows reader, and the Go interface check passes. | [o] |
| T128-2a-3 | The TypeScript set reader, parseSet and --set (follows T128-2a-1). | Acceptance: parseSet(bytes, path) on @polyspec/orm/dbspec and readDbspecSet run set-files.json (parseSet the cases without a tree); --schema/--use are removed. | Evidence: before the change the runner failed because the browser entry exported no parseSet; after it packages/orm-npm/tests/dbspec-set.mjs passes all 61 cases through readDbspecSet on real trees and the tree-free cases through parseSet from both entries, the browser entry test passes with parseSet exported, the generator tests refuse --schema and --use and print every set error, and the TypeScript interface check passes. | [o] |
| T128-2a-4 | The Python set reader and --set with argparse (follows T128-2a-1). | Acceptance: read_dbspec_set runs set-files.json; the hand-written argument loop becomes argparse; --schema/--use are removed. | Evidence: before the change the vector runner failed importing read_dbspec_set; after it all 61 cases of set-files.json pass with real symbolic and hard links, test_orm_gen.py checks that --schema and --use are refused and that a set error or a document diagnostic prints every message and writes nothing, make python-model-check passes with --set schema/dbspec-set.json, and the Python interface check passes. | [o] |
| T128-2a-4-1 | Generate the Python test models from set files (follows T128-2a-4). Cause: T128-2a-4 changed the test helper generate to pass --set, but 20 test modules still passed schema/bench.dbs or a fixture document, so CI's python group failed on orm-gen gen --set …bench.dbs. | Acceptance: callers pass schema/dbspec-set.json or the corpus set file, and a one-document fixture goes through generate_document, which copies it beside its own set file. | Evidence: before the change python -m unittest test_utils failed with the orm-gen exit status 1; after it the 18 changed modules and test_sqlite pass 69 tests, and the two server-run modules compile. | [o] |
| T128-2a-5 | The PHP and extension set readers and --set (follows T128-2a-1). | Acceptance: Dbspec::readSet and its native twin run set-files.json; the positional files are removed. | Evidence: before the change all 61 cases failed with Call to undefined method Dbspec::readSet(); after it make dbspec-php-check passes 69 vector and focused set cases, make dbspec-php-extension-check requires the native results to equal the PHP client's in all 69, orm_gen_test.php refuses positional files and --use and prints every set error with nothing written, and the php and php-extension interface checks pass. | [o] |
| T128-2a-5-1 | Run each PHP test once (follows T128-2a-5). Cause: the set targets dbspec-php-check/set and dbspec-php-extension-check/set also ran dbspec_show_create_test.php, which the show-create targets own, so one recipe ran two independent tests, which the repository check refuses. | Acceptance: each set target runs its set test alone. | Evidence: before the change node scripts/repo/check.mjs reported the recipe; after it the check passes and make dbspec-php-check/set passes. | [o] |
| T128-2a-6 | The Rust set reader and Builder::set (follows T128-2a-1). | Acceptance: dbspec::read_set runs set-files.json; Builder::new([..]) and uses are removed; cargo:rerun-if-changed names the set file and every listed document. | Evidence: before the change the vector test failed to compile with cannot find function read_set; after it all 61 cases pass with real links, the orm-build tests check that every set error and diagnostic is reported with nothing written and that cargo:rerun-if-changed names the set file and each document, make rust-150-check and clippy pass (the Windows code clippy-clean for x86_64-pc-windows-msvc), and the Rust interface check passes. | [o] |
| T128-2a-6-1 | Keep the Rust set tests within the test rules (follows T128-2a-6). Cause: dbspec_set.rs failed a test on a measured CPU time, which the repository rules forbid (a measurement warns), and dbspec-rust-check/set ran two independent tests in one recipe. | Acceptance: the measurement prints a warning as dbspec.rs does, and the target splits into the parts vectors and unit. | Evidence: before the change node scripts/repo/check.mjs reported both; after it the check passes and make dbspec-rust-check/set passes both parts. | [o] |
| T128-2a-7 | The callers and documents of the set file (follows T128-2a-2 to T128-2a-6). | Acceptance: the Makefile targets, scripts, docs/operations/usage.md, docs/spec/schema.md, docs/spec/dsl.md and docs/operations/packaging.md (+ko) use the set file; the interfaces rule Dbspec.readSet with symbols and parity; a breaking changelog entry. | Evidence: docs/operations/usage.md, docs/spec/schema.md, docs/spec/dsl.md and docs/operations/packaging.md (+ko) use the set file for all five generators, and each changed command ran once and exited 0 (Go, PHP and TypeScript with and without --check, Python, Rust through make rust-150-check); the rule Dbspec.readSet declares the reader of every language and the extension, and make interface-check passes; the parity unit dbspec-set-files removed its R4 undeclared-suite finding; node scripts/features/build.mjs --check passes with the set tests owned by schema_definition. | [o] |
| T128-2a-8 | Generate the coverage models from the set file (follows T128-2a). Cause: CI's feature_coverage group failed TestCoverageModelGenerationCheck: the Go test copied schema/bench.dbs without its set file (set.file: … lists no dbspec-set.json) and the PHP check passed the document path (unexpected argument: schema/bench.dbs). | Acceptance: both generate through schema/dbspec-set.json. | Evidence: the hyper session's agent reproduced both messages first; go test -tags featurecoverage -run TestCoverageModelGenerationCheck$ ./packages/orm-go/model and the PHP model_generation_check pass. | [o] |
| T128-3s | Reject repeated state-machine lines in every runtime. Cause: the parser silently accepts a repeated initial, terminal or <from> -> <to> line of one machine, even with different require lists (parse.ts:2249-2284). | Acceptance: such a repeat is a setting error at the later line's keyword, stated in docs/spec/dbspec.md (+ko), checked in every parser, with three rejecting vectors in cases.json and a changelog entry. | Evidence: three rejecting vectors (state-machine-initial-repeated, -terminal-repeated with another require list, -transition-repeated) failed first in every runtime (Go, TypeScript, PHP, the PHP extension against the PHP client, Python, Rust), each parsing the document; after the change each runtime reports a setting error at the later line's keyword, a line of another column keeps only its one-machine error, and the shared vectors pass in all six; docs/spec/dbspec.md (+ko) states the line identity; the Go and TypeScript symbols record machineLine with the computed hashes. | [o] |
| T128-3p | Declaration positions in the parse result of every runtime. Cause: tools such as flowmark locate declarations by re-scanning text, with a line 1, column 1 fallback. | Acceptance: the successful parse result carries positions, the source-order list of {kind, path, line, column} defined in the editor stack plan (setting identities from the repeat rule, the primary key, the document name), outside DbspecDocument; stated in docs/spec/dbspec.md (+ko) with vectors for every kind and parity in every runtime. | Evidence: all six runtimes return the positions of the positions cases (Go 156e07fe, TypeScript 91ffbc4b, Python 16015424, Rust 520877b3, PHP 4d1da64c, the extension ecabd61e); make dbspec-compare-check compares them across the clients and the extension (12 runs agree); the parity unit dbspec-positions and the schema_definition coverage case dbspec_parse_positions declare them; make interface-check passes for every language. | [o] |
| T128-3 | analyze and format in every runtime. | Acceptance: the six implementations return the standard tokens and the canonical format with the invariants of the editor stack plan; tests/dbspec/syntax.json runs in every runtime check and in dbspec-compare-check; the interface rules, symbols, parity units, features, the php-extension rules and docs/spec/interfaces-model.md (+ko) record them. | Evidence: every runtime's test reads tests/dbspec/syntax.json strictly and matches it; make dbspec-compare-check gives identical tokens, analysis diagnostics, formats and refusals for all 304 syntax cases across the six implementations (12 runs agree); the parity unit dbspec-syntax, the coverage cases dbspec_analyze_tokens, dbspec_format and dbspec_format_refusal, the extension's method sentence and the Dbspec row of docs/spec/interfaces-model.md (+ko) record them; make interface-check passes. | [o] |
| T128-3-1 | Parse the lines before an encoding stop in every runtime (follows T128-3). Cause: Python stops at an encoding error before parsing any line, while Go parses the complete lines before it and reports their diagnostics first; no vector held such a case. | Acceptance: cases.json encoding-after-a-syntax-error (a syntax error on line 4, a bare CR on line 8) gives both diagnostics in every runtime. | Evidence: Python (c785848a), TypeScript (0abd4c06) and PHP with its extension (0b7bb4f6) failed the case before their change, reporting only the encoding diagnostic, and pass it after; the Go and Rust vector tests (go test ./engine/dbspec, cargo test --test dbspec) pass it; make dbspec-php-check and make dbspec-php-extension-check pass. | [o] |
| T128-3-2 | Regenerate the syntax vectors for the T129 cases (follows T128-3). Cause: T129 added nine cases to tests/dbspec/cases.json and removed three, and its landing did not regenerate tests/dbspec/syntax.json, so the vectors named cases that no longer exist and lacked the new ones. | Acceptance: node scripts/dbspec/syntax-vectors.mjs writes the vectors of the current cases; no token of an existing case changes; the TypeScript, Go, Python and Rust syntax tests pass on them. | Evidence: before the change node --test packages/orm-npm/tests/dbspec-analyze.mjs failed on the case ids and their order; after it the generator added exactly the nine cases and removed the three, and the TypeScript (897 pass), Go (go test ./engine/dbspec), Python (test_dbspec_syntax_vectors.py) and Rust (cargo test --test dbspec syntax) tests pass. | [o] |
| T128-3-3 | Start the editor stack test cases through the case harness (follows T128-3). Cause: the Go tests of engine/dbspec added by T128-3p and T128-3, the Rust dbspec_analyze.rs tests and packages/orm-npm/tests/browser-entry.mjs of T128-4 declared their cases directly, so they ran without a deadline or a RUN line, against the rule that make repo-check enforces. | Acceptance: each starts through internal/testcase, polyspec_orm_testcase or caseTest. | Evidence: before the change node scripts/repo/check.mjs reported 11 problems in these files; after it the check passes, and the Go, Rust and TypeScript tests pass with their RUN and PASS lines. | [o] |
| T128-4 | The browser-safe parser subpath of @polyspec/orm. | Acceptance: parse, emit, analyze, format and readDbspecBytes are exported from modules with no node: module, Node global or driver, checked by a module-graph test, and recorded in the interface rules. | Evidence: @polyspec/orm/dbspec exports parseDbspec, emitDbspec, analyzeDbspec, formatDbspec and readDbspecBytes from node-free modules (document.ts, read_bytes.ts, entry.ts); packages/orm-npm/tests/browser-entry.mjs follows the subpath's module graph and failed first naming node:crypto, node:fs, Buffer and the drivers, then passes; make dbspec-ts-check 1335 pass; the typescript interface check passes with the moved symbols. | [o] |
| T128-4-1 | Keep the browser-safe entry free of Node modules after T129 (follows T128-4). Cause: T129 made dbspec/names.ts, which the parser reaches from @polyspec/orm/dbspec, hash with node:crypto and measure with Buffer, so the browser-safe entry reached a Node module; the landing ran no browser entry test. | Acceptance: names.ts takes SHA-256 from @noble/hashes (synchronous, written for browsers and Node, no dependencies) and the byte length from TextEncoder; the browser entry test allows a declared browser package and follows its modules by the same rules; the dependency has its review record. | Evidence: before the change node --test packages/orm-npm/tests/browser-entry.mjs reported names.js importing node:crypto and using Buffer; after it the test passes with the modules of @noble/hashes followed. | [o] |
| T128-5 | packages/dbspec-language. | Acceptance: openDocument gives ranged diagnostics, tokens, folding and format; tests cover cases.json, syntax.json, the invariants, idempotence and the editor fixtures; the module graph reaches no Node module, Node global or driver. | Evidence: make dbspec-language-check passes 635 cases: the canonical, normalize and invalid cases, the hashes kept by format, idempotence, the tokens of every syntax vector in UTF-16, the five editor fixtures of tests/editor and the module graph test, whose one bare import resolves to the parser entry that packages/orm-npm/tests/browser-entry.mjs checks; folding pairs the { and } tokens (docs/spec/editor.md "Results"), so it reads no keyword; the two sets/repeated-document-name vectors have no Record form and are asserted excluded, and T128-5-1 runs them through resolveSet. | [o] |
| T128-5-1 | The dbspec-fmt CLI and resolveSet (follows T128-5, after T128-2a). | Acceptance: resolveSet takes an injected host; the CLI follows template-fmt's rules with exit codes 0, 1 and 2 and make install-cli installs it without a symlink; the sets/repeated-document-name vectors run through resolveSet. | Evidence: before the change the resolve-set and vector tests failed with no resolveSet, the 20 CLI cases with no bin/dbspec-fmt.mjs, and make install-cli had no rule; after it make dbspec-language-check passes 710 cases: 41 set-file vectors through resolveSet with a node host and the other 20 asserted unreachable with their reasons, both repeated-document-name vectors through resolveSet, the CLI modes, exit statuses and stderr format; make install-cli installs without a symbolic link; docs/spec/editor.md (+ko) states set resolution and the command line. | [o] |
| T128-5-2 | Hold the language service's typescript at 6.0.3 by a stated exception (follows T128-5). Cause: the review of 2026-10-13 found typescript 7.0.2 released, and packages/dbspec-language locks 6.0.3; its module graph test reads the syntax tree with the typescript API, which typescript 7, the native compiler, does not have. | Acceptance: config/dependency-policy.json holds the exception with its reason, removal condition and verification, as packages/orm-npm's does. | Evidence: make dependency-policy-check no longer reports the package; make dbspec-language-check passes. | [o] |
| T128-6 | packages/dbspec-lsp. | Acceptance: a vscode-languageserver server with utf-16 positions, full sync, semantic tokens of the standard classes, folding, formatting and set-file resolution with watched files; stdio session tests against the fixtures. | Evidence: make dbspec-lsp-check passes 12 stdio sessions against the built server: the declared capabilities and legend; the five editor fixtures through diagnostics, semantic tokens, folding and formatting; a document's set from the nearest set file; a document alone reporting its use line; a set error shown with the document analyzed alone; an open buffer winning over the disk (failing when the buffer is ignored); a watched file change; and another file's diagnostic published on its URI. | [o] |
| T128-7 | The docs fence highlights dbs blocks through the language service. | Acceptance: the markdown-it fence hook renders the tokens with light and dark colours, fences declare their set (dbs set=…), and the docs build fails on any fence diagnostic. | Evidence: the fence of docs/spec/dbspec.md (+ko) renders the shop document with the core document of its set; npm run docs:check builds the site with the class spans; make docs-dbs-check passes 7 cases, among them a fence with a use diagnostic failing with dbspec.md:24:5: use: …, a declared name that differs from its header failing through the use rule, a malformed info string and escaping; docs/spec/editor.md (+ko) states the document= attribute. | [o] |
| T128-8 | packages/dbspec-codemirror and packages/dbspec-vscode. | Acceptance: CodeMirror 6 marks, lint, fold and a format command tested through EditorState; a VS Code extension with semantic token scopes, no grammar file, semantic highlighting enabled by default and the bundled LSP client, tested by integration; the CI job installs VS Code through the kit tool. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Retry: start once that orm release is published. Decision (user, 2026-10-18): development is local and linked, with no release; a retry that waits on an orm release is met when the change is on orm main. | Evidence: its children T128-8-1, T128-8-2 and T128-8-3 are done on main. | [o] |
| T128-8-1 | packages/dbspec-codemirror (follows T128-8). | Acceptance: a CodeMirror 6 extension dbspec({set}) with token marks, lint diagnostics, folding and the format command, reading only the language service; tests read the editor fixtures through an EditorState. | Evidence: make dbspec-codemirror-check passes 8 cases, the five fixtures among them, giving the fixture's tokens as marks, its diagnostics, its folding and its formatted text; without the marks extension 5 cases fail; the package builds with typescript 7.0.2, and make dependency-policy-check reports no finding with the @codemirror/* reviews recorded. | [o] |
| T128-8-2 | packages/dbspec-vscode (follows T128-8). | Acceptance: a VS Code extension with the language dbspec, no grammar, semantic token types in the order of TOKEN_CLASSES with a scope each, semantic highlighting enabled by default, the bundled language server through vscode-languageclient and a .vsix build. | Evidence: make dbspec-vscode-check passes 6 cases (the contribution, the bundles, and the bundled server answering TOKEN_CLASSES as its legend over stdio); npm run --workspace polyspec-dbspec package builds the .vsix; the dependency review records its packages; .gitignore ignores every package's node_modules. | [o] |
| T128-8-3 | Run the VS Code integration test (follows T128-8-2, after T137 and T165). Cause: the test of vscode.provideDocumentSemanticTokens over the editor fixtures needs kit's VS Code installer, which orm receives with T137. | Acceptance: make dbspec-vscode-integration passes the five fixtures, red first by breaking the contribution, then runs in CI through the kit installer. It is the editor job of ci.yml, as template runs it, not a target of make check: it needs VS Code installed and a display (xvfb-run), which make check does not provide. | — | [o] |
| T128-9 | Wire the editor packages into the checks and the release configuration. | Acceptance: the kit import check with orm's rules, feature owners, check inputs, CI group, workspace build and test scripts, release manifests and consumer smokes, dependency review records, and the AGENTS.md (+ko) package layout. | — | [o] |
| T128-R | Release the editor stack. | Acceptance: a chore(release) bump commit after green CI, the maintainer's tag, the release workflow and release-verify of every npm package. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Retry: start once that orm release is published. Decision (user, 2026-10-18): development is local and linked; nothing is released until the batch release, which this item waits on. | — | [!] cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it.; retry: start once that orm release is published |
| T129 | Fit every renderer-generated name in 63 bytes by one hash rule (follows T8.1.5). Cause: the renderer writes <table>$<column> CHECKs and <table>$<event> triggers and functions, and a history table named service_module_board_comment_history with the column service_module_board_post_seq gets a 66-byte CHECK name, so the audited table cannot be declared; a declared name is still refused, never shortened. | Acceptance: docs/spec/dbspec.md "Names" and docs/spec/dialects.md state the rule: the plain form when at most 63 bytes, else its first 54 bytes, a $, and the first 8 lowercase hex digits of SHA-256; names are ASCII, so bytes are characters; generated names are unique together with declared names, and a collision is name.duplicate naming both sources. tests/dbspec/cases.json holds the exactly-63-byte plain case and the 64-byte hashed case as accepted, the plain-vs-hashed and the hashed-vs-hashed collisions as refused, and a user-chosen 64-byte name as refused; tests/dbspec/ddl.json holds the 66-byte history case with its exact generated name; tests/dbspec/introspect.json reads that document back in MySQL, PostgreSQL and SQLite, recomputing the names. Every runtime runs these vectors in its compare check (Go, PHP, PHP extension, TypeScript, Python, Rust), and the database checks make dbspec-ddl-check and dbspec-introspect-check pass. | — | [o] |
| T129-1 | Record the interface symbols of the renderer names (follows T129). Cause: T129 added and removed symbols (rendererName and renderedColumn, the generated-name validator and the Names classes), and its interface snapshots were not recorded when it was approved; the full make conformance-check found the missing record. | — | Evidence: the interfaces check, run with --record on the T129 tree (6cfcbac4), wrote the five snapshots under contracts/symbols, and contracts/interfaces.json holds the computed hashes: go sha256:35e0d986f546547aeeb6070e9c2dc85382a6f4026c1a73536f57f7376b6ae8cc, php sha256:b011a44b9e3acfbca2f0bf1f358b7fc5e9a099bf1caac2fc4968c70af3f422b3, rust sha256:8cdc879e4e245937b87b3bc679bf67b0dab32c41c4d707ffd159b00278a65dbc, typescript sha256:6a7fccd4fe61e011534bf9cb221a086d7a4fa60415e43ea697a08ec54fd51da1, python sha256:1b21698d94db68f4b28193bafbd6f7a2fff5a066facbaa0bf3074fc66d0ae5ef. The non-record interface check passes on all five languages and the PHP extension. | [o] |
| T129-2 | Declare the renderer names in the PHP extension (follows T129). Cause: T129 added Polyspec\Orm\Dbspec\Names with render, ofTable and column to the PHP client, and the extension, which has every class of the dbspec surface (docs/spec/dbspec.md "PHP extension"), had no Native\Names, so make dbspec-php-extension-check failed at its declarations. | Acceptance: Native\Names in C behaves as the PHP class, with stub, arginfo and a test against the PHP class. | Evidence: before the change the declarations check reported Native\Names missing; after it make dbspec-php-extension-check passes 789 cases, among them names of 63 and 64 bytes, ofTable on parsed tables and column over arrays, iterators and a generator not read past its match; the php and php-extension interface checks pass. | [o] |
| T130 | Recognize the immutable and audit settings of one table by their own trigger names (follows T129). Cause: a table that is both audited and immutable has five triggers, but every client recognized immutable only with exactly two triggers and audit only with exactly three, so introspection reported the five as unsupported (CONFIG: the database differs from the document set: unsupported_trigger …$audit_*/immutable_*), reported by the consumer member (S-284). | Acceptance: docs/spec/dialects.md "Triggers" states the rule: each setting is recognized by its own recomputed trigger names, never by a count; a table with both settings has the union of the two renderer outputs, which its trigger set must equal exactly by name and by statement; an extra, missing or different trigger makes every trigger of the table unsupported. tests/dbspec/introspect.json holds an audited immutable table in MySQL, PostgreSQL and SQLite, read back as the same document, and every client's introspection check runs it. | Evidence: before the change make dbspec-introspect-check failed the three cases on MySQL 8.4, PostgreSQL 17 and SQLite. After the change the Go, TypeScript, PHP, PHP extension, Python and Rust introspection checks pass on the three databases, and the unit suites of Go, TypeScript, PHP, Python and the PHP extension compare pass. | [o] |
| T130-3 | State the finder rule and verify it in every runtime with one shared vector (follows T130-2). Cause: T130-2 was marked done without a vector, which lowered the criterion, and docs/operations/usage.md said every chain works in every language, which the generators do not do. The T130 change also altered five declared symbols, so each T130 code commit records the symbols that it changed. | Acceptance: docs/spec/interfaces.md "Finder chains" ({#finder-chains}) states the rule: a chain is any combination of column keys, operators and connectors; a static client (Go, Rust, TypeScript) declares the chains that the generation scan finds, and a dynamic client (PHP, Python) resolves the call name when the call runs; the same chain gives the same rows. docs/operations/usage.md and docs/usage.ko.md point to it. The shared vector terminal_by runs getsByServiceSeqAndIsClose(7, false) on the non-unique columns service_seq and is_close in all five runtimes; it passed in every runner on MySQL in the conformance run. | Evidence: fix(dbspec): Recognize settings by name in Go (#T130) records catalog.triggerSetting, fix(dbspec): Recognize settings by name in TypeScript (#T130) records triggerSetting and triggerModel, and fix(dbspec): Recognize settings by name in Rust (#T130) records model and trigger_setting, and each of them holds its hash in contracts/interfaces.json; the non-record interface check passes at every commit of the branch; the hashes at the tip are go sha256:f821b72f2e3f202dfb6ea744a7c1006471f75851da1d61f2b6fbd816fa6368b7, rust sha256:5556d40413039166d9f1437b015c0070a90b6271aebcb55f0a893b525f5ed309 and typescript sha256:2d9235f2b4660f07cae430d6269c72a776f5dc0a6fb94500c4494e96c016bb43 (php and python are unchanged). | [o] |
| T130-2 | Decide whether the TypeScript finders are scanned or generated for every column, then make the clients equal (follows T130). Cause: the S-284 report says the node generated models create finders only for unique keys, while php creates them for other columns too, so node code reads whole rows and filters in JS. Checked in this repository: the TypeScript generator declares a finder for each chain the scanned code calls, and a non-unique column gets getByStatus, getsByStatus and getCountByStatus when scanned; PHP generates no finder and resolves any chain at call time in Model::__call. docs/spec/interfaces.md states both designs, and docs/operations/usage.md states that finders "accept any column chain" in every language, which a TypeScript chain that the scan did not see does not do. Decision: TypeScript follows the scan (a finder for each chain that the scanned code calls) and PHP resolves at call time; no generator change. | — | Evidence: docs/operations/usage.md and docs/usage.ko.md state that exactly, with the TypeScript rule that a chain works after the model generation scans the code that calls it. | [o] |
| T131 | Translate the code comments to English. Cause: AGENTS said code comments are Korean, against the owner's rule that comments are English (kit PRINCIPLES, K84); 10,059 Korean comment lines remain in 728 files (2026-10-13). | Acceptance: no Hangul in a comment of a code file, held by a check that fails on one and runs among the check targets; AGENTS states the English rule. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. | Evidence (T131): all 10,469 Hangul comment lines of the tracked code files are translated (T131-2..T131-7), and make comment-language-check passes on the whole tree. Resumed: link-owner ruling (2026-10-18): start now in local-only development; translating comments does not collide with the protocol items. Evidence (T131-1): the check of scripts/comments/check.mts passes its 23 unit cases; make comment-language-check is red on the tree with 10,469 Hangul comment lines, each named by file and line. | [o] |
| T131-1 | The comment language check: a thin tool that takes the comment tokens of every code file from each ecosystem's own tokenizer (Go go/scanner, TypeScript's scanner, Python tokenize, PHP token_get_all; for a language without one, a rebuild record under S1 first) and fails on Hangul, naming file and line; red on the current tree. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Resumed: link-owner ruling (2026-10-18): start now in local-only development; translating comments does not collide with the protocol items. | — | Evidence (T131-1): the check of scripts/comments/check.mts passes its 23 unit cases; make comment-language-check is red on the tree with 10,469 Hangul comment lines, each named by file and line. | [o] |
| T131-1-1 | Read each ecosystem's comments with its own tokenizer, not the regex lexer (follows T131-1, coordinator review). Go: go/scanner (scripts/comments/tokens/go-comments.go). TypeScript: the TypeScript parser of the pinned interface project (packages/ts/testing/interfaces/typescript, TypeScript 6.0.3). Python: tokenize, with ast for docstrings (tokens/python-comments.py). PHP: token_get_all (tokens/php-comments.php). Rust, C and the hash-comment files keep scripts/comments/own-lexer.mts under the rebuild records docs/rebuild/rust-comments.md, c-comments.md and hash-comments.md. Cause: the regex lexer reimplemented the tokenizers and missed 295 Hangul comments, which are translated here. | — | Evidence: scripts/comments/check.test.mts passes 25 cases, red first (the module lacked commentsByFile); node scripts/comments/check.mts passes over the tree; the code of every translated file is identical to cf01f139 without its comments (tokenizer and AST comparison, one-off). | [o] |
| T131-2 | Go comments in English. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Resumed: link-owner ruling (2026-10-18), local-only development. | — | Evidence (T131-2): the Go comments under packages/go, examples and the Go examples are English, and the Go unit commits of T131 each pass the comment check of their files. | [o] |
| T131-3 | TypeScript and JavaScript comments in English. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Resumed: link-owner ruling (2026-10-18), local-only development. | — | Evidence (T131-3): the TypeScript comments of packages/ts/src, packages/ts/tests and packages/ts/testing are English, and node scripts/comments/check.mts packages/ts passes. | [o] |
| T131-4 | PHP and PHP extension comments in English. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Resumed: link-owner ruling (2026-10-18), local-only development. | — | Evidence (T131-4): the PHP and php-ext comments (packages/php, packages/php-ext, the PHP examples) are English, and each unit commit passes the comment check of its files. | [o] |
| T131-5 | Python comments in English. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Resumed: link-owner ruling (2026-10-18), local-only development. | — | Evidence (T131-5): the Python comments of packages/python are English, and each unit commit passes the comment check of its files. | [o] |
| T131-5-1 | Restore the three Python docstrings that the translation broke (follows T131-5). Cause: the translation of a one-line docstring lost its closing triple quote in packages/python/src/polyspec/orm/dbspec/emit.py, dbspec/parse.py and testing/init.py, so the three modules did not compile (found by the tokenizer check of T131-1-1). | — | Evidence: each file compiles with python3 -c "compile(...)"; the code of every translated Python file is identical to cf01f139 with docstrings removed (AST comparison). | [o] |
| T131-6 | Rust comments in English. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Resumed: link-owner ruling (2026-10-18), local-only development. | — | Evidence (T131-6): the Rust comments of packages/rust, benchmarks/rust and the Rust examples are English, and each unit commit passes the comment check of its files. | [o] |
| T131-7 | Makefile, scripts and the remaining files in English; the check of T131-1 passes and joins the check targets. Cause: owner ruling (2026-10-15): the release that flowmark takes completes the protocol items first; this item waits until after it. Resumed: link-owner ruling (2026-10-18), local-only development. | — | Evidence (T131-7): the Makefile, scripts, tests and examples comments are English; comment-language-check is a CHECK_TARGETS and CI static-group target, and its check passes on the whole tree. | [o] |
| T131-8 | The Rust interface snapshot matches the translated doc comments (follows T131). Cause: the T131 translation changed the doc text inside 40 Rust signatures recorded in contracts/symbols/rust.json, so interface-check's interfaces/rust failed on 833af162 while cebf00eb passed. | — | Evidence: make interface-check/self-test exit 0 after the re-record (var/ic-after.log, interfaces/rust ok); the re-record changed only the recorded signature text of those 40 symbols (41 lines). | [o] |
| T131-8-1 | Generate the Unicode word tables with English text (follows T131-8). Cause: T131 translated the five generated tables (php-ext, php, python, rust, ts) by hand but not the generator tests/dbspec/unicode/main.go, which still emitted Korean headers, so go run ./tests/dbspec/unicode -write rewrote the tables in Korean and TestCommittedTablesAreGenerated failed on the five files (exit 1, unit go-internal-tests-dbspec-unicode-main of the full parity run on bccfc382). | Acceptance: the generator emits the English text, the committed tables equal its output, and the unicode test passes. | Evidence: RED: go test -run TestCommittedTablesAreGenerated ./tests/dbspec/unicode failed on the five tables. GREEN: the same test passes after the generator's 14 literals became English and the tables were regenerated (20 comment lines changed in five files, no code line). | [o] |
| T131-9 | Expect comment-language-check in the owner selections (follows T131). Cause: T131 added comment-language-check, whose inputs are every source file, so a source change selects it, but the two selection cases of scripts/features/owners.test.mts still expected the targets without it. | Acceptance: both cases expect comment-language-check and feature-unit-check passes. | Evidence: RED: make -s feature-unit-check failed on main with a change across the four clients selects no full-suite target and a generator source selects the check that regenerates the models, each with + 'comment-language-check'. GREEN: with both expectations naming it, make -s feature-unit-check exits 0. | [o] |
| T131-10 | Regenerate the extension's arginfo header after the stub translation (follows T131). Cause: T131 (4168b88d) translated the comments of packages/php-ext/stubs/orm_dbspec.stub.php, which changes the stub hash that gen_stub.php writes into src/orm_dbspec_arginfo.h, but did not regenerate the header. | Acceptance: the header is regenerated with gen_stub.php and its check passes. | Evidence: RED: make -s dbspec-php-extension-check/arginfo failed on main with src/orm_dbspec_arginfo.h differs from the stub. GREEN: after php packages/php-ext/scripts/arginfo.php .runtime/bin/gen-stub/gen_stub.php write (only the stub hash line changes) the check exits 0. | [o] |
| T131-11 | Keep the extension's mirrored tests equal to the client's after the translation (follows T131). Cause: T131 translated the comments of the five mirrored tests (DbspecPlanTest, DbspecMermaidTest, DbspecIntrospectTest, DbspecApplyCleanupTest, DbspecApplyTest) differently in packages/php/tests and packages/php-ext/tests, while DeclarationsTest::test_mirrors requires their bodies to be identical. | Acceptance: each extension copy has the client test's body after its header, and test_mirrors passes. | Evidence: RED: make -s dbspec-php-extension-check/declarations failed on main with packages/php-ext/tests/DbspecPlanTest.php differs from packages/php/tests/DbspecPlanTest.php after its header; all five differed in comment and docblock lines only. GREEN: with the client bodies copied (53 comment lines) the target passes twice, 5 tests each. | [o] |
| T133 | Read every section of the shared dbspec vectors strictly in every runtime. Cause: the TypeScript, Python and Rust readers of tests/dbspec/cases.json read only the new positions section strictly; the other sections ignore unknown fields and read by index, so a misspelt field is dropped silently (found by the T128-3p agents). | Acceptance: each runtime's reader refuses an unknown or missing field in every section, naming it, with a rejecting test per runtime; the Go reader too (corrected: the item named Go's reader the model, but it also accepted unknown fields). | Evidence: before the change go test ./engine/dbspec -run TestVectorLoadersRejectMalformedVectors failed 11 new mutations and make dbspec-compare-check/runners failed 120 of 319 cases (20 mutations × 6 runners), each runner reading past the bad field; after it one strict reader per runtime (tests/dbspec/cases.mjs, tests/dbspec/cases.php, dbspec_cases_reader.py, common/dbspec_cases.rs, the Go fields helper and compare/go/cases.go) refuses it with <file>: <location> <problem>, the runners pass 319 of 319, and each runtime's own rejecting tests pass. | [o] |
| T134 | Run the Python tests against the installed package. Cause: 54 test files insert src into sys.path, which S3 forbids (tests run against installed packages, never a changed path). | Acceptance: the tests import the package installed into the venv, no sys.path change remains, and a check refuses one. | — | [o] |
| T134-1 | Remove the sys.path insert of packages/orm-python/bin/orm-gen (follows T134). Cause: the script still inserts src, while the venv installs the orm-gen console script. | Acceptance: the wrapper is removed or imports the installed package, and every caller uses the console script. | — | [o] |
| T135 | Keep the test server sockets within the Unix socket path limit. Cause: scripts/test-servers.sh puts sockets under the checkout, and a long worktree path made proxysql-admin-pgsql.sock 104 bytes, past macOS's 103, so the database tests could not start (found by the T128-3p Python agent). | Acceptance: socket paths come from a short declared place whose length is checked with a named error, red first on a long path. | — | [o] |
| T135-1 | Write the comments of scripts/test-servers.sh and its tests in English (follows T135). Cause: check_socket_paths and the test comments are Korean, while AGENTS rules English code comments. | Acceptance: every comment there is English. | — | [o] |
| T136 | Reproduce and fix make install in a fresh worktree. Cause: a T128-3p agent reported make install failing at the Python tooling in a new worktree, saying uv is not declared; uv is declared (config/toolchain.json), so the reported cause is wrong and the real one is unknown. | Acceptance: the failure is reproduced in a fresh worktree with its exact output, its cause is fixed or the report is shown wrong, red first. | Evidence: the cause is kit v0.0.31 (T126-1), whose install-tools installs Python through uv and refuses a python asked for without uv, while install-python asked for TOOLS=python; CI run 38413640279 failed the step Python client dependencies in every group that needs Python. Before the change make install-python in a fresh checkout failed in install-tools; after it (TOOLS="uv python") it builds packages/orm-python/.venv from the uv Python. | [o] |
| T136-1 | Pin uv before Python in the install-python test (follows T136). Cause: scripts/repo/python-venv.test.mjs still matched install-tools.mjs python, so CI run 38421077014 failed repo-check after T136. | Acceptance: the test asserts install-tools.mjs uv python. | Evidence: make repo-check/unit passes the case; make -n install-python prints install-tools.mjs uv python. | [o] |
| T137 | Take kit's current release and follow its standards. Cause: orm vendors kit v0.0.31, before kit carried PRINCIPLES.md with the standards S1–S9 and the TypeScript tools; orm AGENTS.md:31 still sets packages/orm-<kind>, which the folder standard S5 refuses (no repository prefix, runtime ports in packages/<runtime>/). | Acceptance: make kit-sync to kit's latest release; AGENTS.md (+ko) links the vendored PRINCIPLES.md and keeps only orm's own rules; config/layout.yml maps orm over kit's common rules, each exception with its reason; the runtime packages move to packages/<runtime>/ (go, ts, php, php-ext, python, rust) with every path, manifest, module path and workflow updated and no compatibility path; make layout-check and make kit-check pass. | — | [o] |
| T137-1 | Make the folders follow S5, S7 and S9 of kit's layout (follows T137; red recorded by kit K79 on kit 0.0.38). Cause: after the client folders took their runtime names, make layout-check reports 223 findings, the first bench: error: layout-root-folders: S5: the root holds only packages/, contracts/, docs/, scripts/, tests/, examples/, benchmarks/ and config/; the root Go module (engine/, generator/, cmd/, internal/) breaks S5 and needs a decision on where the engine, the generator and the Go programs of tests/ live. | Acceptance: make layout-check passes, every exception of config/layout.yml with its reason. | Evidence: every sub-item is done (T137-1-1 to T137-1-9 and T137-1-4 with its sub-items): the Go module is packages/go, the root holds only the folders and files of S5, tests/ only its declared runners, and every S9 exception carries its reason; make layout-check passes with no finding. | [o] |
| T137-1-1 | Declare packages/dbspec-vscode in config/layout.yml and drop the PYTHONPATH that T134 left in codec-check/python and tests/conformance/result_python.py (follows T137-1). | Acceptance: layout-check reports neither, and the Python codec vectors and the conformance runner test pass. | — | [o] |
| T137-1-2 | Map the PHP client by PSR-4 alone (follows T137-1). Cause: lint-composer-policy (S2) refuses the classmap of packages/php/composer.json, which eleven source files (Codec, Markdown, Engine, GroupRows, Model, AesKeyring, Db, Planner, Collection, Utils, Orm) need because each holds several classes, and the library maps no autoload-dev. | Acceptance: one class per file under PSR-4, no classmap, an autoload-dev by PSR-4, and the PHP checks pass. | — | [o] |
| T137-1-3 | Take the Rust lints of the workspace (follows T137-1). Cause: lint-cargo-lints refuses every Cargo.toml (11): a workspace denies unsafe_code and clippy::allow_attributes_without_reason and each package takes them; orm has 5 unsafe uses (case-clock, the SQLite authorizer, the planner scope) and 20 allow attributes without a reason. | Acceptance: the lints are declared, each unsafe use is removed or allowed with its reason, every allow states its reason, and rust-check passes. | — | [o] |
| T137-1-5 | Move the Go module to packages/go (follows T137-1). Cause: the root Go module (go.mod, go.sum, engine/, generator/, cmd/, internal/) broke S5; ordered-json 0.0.9 moved its Go module to packages/go for the same layout. | Acceptance: the module is github.com/polyspec/orm/packages/go with release tags packages/go/vX.Y.Z; engine, generator, cmd/orm-gen and internal/testdb live under it, and testcase and procevent are public packages there because the Go programs of tests/ import them; layout-check reports none of those paths. | Evidence: go build and go vet pass in packages/go; go test of the engine, its six subpackages, the generator and testcase passes without a database, and the model generation coverage test passes; make interface-check passes with the Go symbols re-recorded (only the path prefix changed); repo, feature, release-coverage, dependency-policy and document checks pass. | [o] |
| T137-1-6 | Give the Go programs of tests/ and examples/ their own modules in the workspace (follows T137-1-5). Cause: once the root module moved, the Go programs and tests under tests/ and examples/ belonged to no module, and kit's Go lint forbids replace directives. | Acceptance: tests/go.mod and examples/go.mod are modules of the root go.work that config/layout.yml declares, they need no replace, and the Go tests of contracts/ live in tests/interfaces/contracts. | Evidence: go build and go vet pass in tests and examples, the tagged vet (physical, bench, markdown, decimalphysical, examples, featurecoverage, ormtest) passes over the three modules, go test of tests/interfaces/contracts, tests/dbspec/unicode, tests/conformance/runner_go and tests/dialects passes without a database, and make go-vet-check, go-fmt-check and testcase-check/go pass. | [o] |
| T137-1-7 | Move the bench folders and the root files into the layout (follows T137-1). Cause: layout-check reported bench/ and schema/ at the root (S5), tests/schema (S7) and .composer-version (S5), which repeats the Composer release that mise.toml declares. | Acceptance: bench/ is benchmarks/, the shared bench schema and its set are contracts/fixtures/bench/, the binaries schema of the cargo runner is scripts/schema/binaries.schema.json, .composer-version is removed and the repository check reads the Composer release of mise.toml through smol-toml; make layout-check passes. | Evidence: make layout-check passes with no finding; python-unit-check, the PHP engine, runtime-model and orm-gen-scan units, the TypeScript engine, generate and generate-scan tests, the Rust dbspec_runtime and ir_validate tests, cargo check of benchmarks/rust, go test of engine and generator and the model generation coverage test pass with the moved paths; scripts/repo and scripts/check pass 143, and dependency-policy-check passes with smol-toml 1.9.1 reviewed. | [o] |
| T137-1-8 | Pass CI after the layout moves (follows T137-1-5 and T137-1-7). Cause: CI of e7d8b309 failed in six jobs: the introspection tests of TypeScript, PHP, the PHP extension and Rust scanned the removed schema/ folder, the Python native rule test read contracts/validate.go from its old place, rust-fmt-check found two unformatted tests, and the conformance unit tests ported in T137-1-4-6 left 17 temporary folders. | Acceptance: the tests read contracts/fixtures/bench and tests/interfaces/contracts, cargo fmt --check passes, and the conformance unit tests leave no temporary folder. | Evidence: the CI records name each failure; after the change python-interface-unit-check passes, cargo fmt --all --check passes in packages/rust and benchmarks/rust, and the main, state, state-run and lock tests of tests/conformance/check pass leaving 0 folders where they left 7 and more; scripts/repo and scripts/check pass 143 and make layout-check passes. | [o] |
| T137-1-9 | Declare the owner of the Go analyzers (follows T137-1-4-2). Cause: CI of d14cb25c failed in feature-unit-check/owners: the files of tests/analyzers, which T137-1-4-2 added, selected no behaviour test and declared no scope; the merge ran neither owners check. | Acceptance: contracts/check-inputs.json names tests/analyzers/** as an input of go-vet-check, which builds and runs the analyzers, and feature-unit-check/owners passes. | Evidence: the CI record lists the 8 analyzer files and 11 testdata files; after the change make feature-unit-check/owners passes, and the repository and feature checks pass. | [o] |
| T137-1-3-1 | Generate Rust models without allow attributes (follows T137-1-3). Cause: orm-build wrote #[allow(unused_mut)] and #[allow(clippy::new_without_default)] into every model, so a consumer that denies allow attributes (fsm on kit's workspace lints) failed. | — | Evidence: the build test generated_code_carries_no_allow_attribute fails before the fix and passes after; rust-check, rust-fmt-check and interface-check pass. Evidence: make layout-check reports no lint-cargo-lints finding (11 before); rust-check, rust-fmt-check, dbspec-rust-check, interface-check and dependency-policy-check pass. | [o] |
| T137-1-4 | Write the cross-language infrastructure in TypeScript (follows T137-1). Cause: S9 (layout-infrastructure-typescript, layout-infrastructure-shebang) refuses 198 scripts of other interpreters under scripts/ and tests/. | Acceptance: each is ported to TypeScript or declared as an S9 exception with its reason in config/layout.yml. | Evidence: every sub-item is done: the infrastructure scripts and the cross-language orchestrators are TypeScript (T137-1-4-1, -5, -6, -9), the readers are parsers (-2, -2-1, -2-2), the vectors are read through shared schemas in every runner (-3, -3-1), and kit 0.0.47 lint passes (-7, -7-1); make layout-check reports no S9 finding. | [o] |
| T137-1-4-1 | Port the JavaScript infrastructure to TypeScript (follows T137-1-4). Cause: the 82 .mjs scripts and tests under scripts/ and tests/ (kit's copies aside) were untyped JavaScript that no compiler checked, read their JSON files with JSON.parse and hand-written field checks, and re-implemented argument parsing, globbing, path joining, version comparison and a JSON Schema validator. | Acceptance: each is a .mts file that tsc checks under the strict options of kit (make scripts-typecheck, a CHECK_TARGETS target that owns tsconfig.json); every JSON file they read (the feature manifest, the check inputs, npm and Composer manifests and locks, go test and cargo messages, the run record, the rule registry, the dbspec and conformance vectors) is read through a JSON Schema by ajv (scripts/json/schema.mts), with the rules a schema cannot state left in code; node:util parseArgs, minimatch, path.posix and semver replace the re-implementations; behaviour and output are unchanged. | Evidence: tsc -p tsconfig.json reports no error; the TypeScript compare runner prints the same 27524 lines before and after and rejects the 53 mutated vectors in the shared form; the interface reader writes the same 3507 lines of symbols; the repository, feature, version, documents and rules checks and the tests of scripts/ and tests/ pass, except the local failures known before the change. | [o] |
| T137-1-4-2 | Replace the hand-written readers of the infrastructure (follows T137-1-4-1). Cause: scripts/repo/ci.mts reads the GitHub workflows line by line instead of with the yaml package orm already depends on; scripts/repo/testcases.mts splits shell commands with its own segments; scripts/repo/gosource.mts and the Go rules of scripts/repo/check.mts read Go source with regular expressions, where S9 makes a language-specific check a plugin of that ecosystem's analyzer (go/analysis). | Acceptance: the workflows are read as YAML documents, the shell commands with a maintained shell parser whose results equal the current ones on every recipe and script, and the Go checks are go/analysis analyzers run by go vet; each with its red case. | Evidence: the workflows are YAML documents (scripts/repo/workflow.mts, yaml parseDocument with unique keys), the shell commands are read by unbash 5.1.0 (scripts/repo/shell.mts), and the Go rules are five go/analysis analyzers run by go vet -vettool (tests/analyzers, make go-vet-check/orm). Over 638 inputs of the repository the new segments equal the old on 631; the 7 that differ are the shell keywords and fragments the old reader took for commands. RED: an escaped quote that hid a go vet, a comment read as a command, a flow-style step missed, a with: input read as timeout-minutes and broken YAML read as nothing, each fixed; the old Go regexes missed or falsely reported a case of each analyzer's testdata, which the analyzers report exactly. scripts/repo and scripts/check pass 143, go-vet-check, the analyzer tests, layout, dependency-policy and document checks pass. | [o] |
| T137-1-4-2-1 | Read the remaining run texts and Makefile rules by parsers (follows T137-1-4-2). Cause: after T137-1-4-2, ciDuplicateCommandErrors compares the lines of a step's run text with the feature commands as strings, and runs() of scripts/repo/toolchains.mts and ciServerErrors apply regular expressions to a step's run value and to the Makefile text. | Acceptance: each reads the commands through scripts/repo/shell.mts and the Makefile through its recipe reader, with a red case for each. | Evidence: scripts/repo/shell.mts gives each simple command (commands) and every output redirect target (writtenFiles) from the unbash tree; ciDuplicateCommandErrors reads the commands of a step and the recipes of makeRecipes, ciServerErrors counts a variable as provided only when a command assigns it or writes it to GITHUB_ENV and finds env writes and symbolic links from the parsed commands, and runs() of toolchains.mts tests parsed commands. RED: a comment naming ln -s, an echo of a redirect, a comment naming the DSN variables, set -e before a duplicate command, a duplicate after &&, an echo of go test or rustup and a commented install were each judged wrong by the old readers and right now; scripts/repo and scripts/check pass 146, and the repository, owners and lint checks pass. | [o] |
| T137-1-4-2-2 | Read the Makefile targets and rules by one reader (follows T137-1-4-2-1). Cause: makeRecipes names no target that holds a slash (testcase-check/go), so a duplicate run through make a/b is not checked and scripts/features/owners.mts cannot look such a target up; makeRules of scripts/repo/ci.mts still reads target lines with a regular expression. | Acceptance: one Makefile reader names every target with its prerequisites and recipe, the checks use it, and typescriptHolderErrors accepts a target that holds the build through a prerequisite such as ts-check/hold; each with a red case. | Evidence: makeTargets of scripts/repo/testcases.mts is the one reader: it names every target, slash and pattern targets included, with prerequisites (continuations and order-only) and recipe, and makeRecipes, makeRules, independentTestErrors, recipe(), usesTestEnv and offlineMakeErrors use it; typescriptHolderErrors follows prerequisites. RED on the old code: usesTestEnv of a slash target, a duplicate through make perf-check/run, a prerequisite on a continuation line, a slash target building without the hold, and downloads of fetch/a and fetch/b were each missed; all pass now; scripts/repo and scripts/check, the repository, feature, owners and lint checks pass. | [o] |
| T137-1-4-3 | Read the dbspec vectors through the shared JSON Schemas in every compare runner (follows T137-1-4-1). Cause: the TypeScript runner reads tests/dbspec/*.json through tests/dbspec/schema/, while the Go, PHP, PHP extension, Python and Rust runners keep their own field checks, so the format is stated six times. | Acceptance: each runner validates with its ecosystem's JSON Schema validator against the same schemas and reports a refusal in the shared form of tests/dbspec/compare/runners.test.mts; its own field checks are removed. | Evidence: the Go (santhosh-tekuri/jsonschema 6.0.3), PHP and PHP extension (opis/json-schema 2.6.0), Python (jsonschema-rs 0.58.6) and Rust (jsonschema 0.58.6) runners validate against tests/dbspec/schema/ and write the shared form; their field checks are removed. RED: 8 of 10 new mutations of runners.test.mts were accepted by all five non-TypeScript runners (40 failures, each an exit 0), and pass after the change. dbspec-compare-check passes (runners 379/379, 12 runs agree), and the Go, TypeScript, PHP, PHP extension, Rust and Python dbspec checks pass. | [o] |
| T137-1-4-3-1 | Report every schema refusal of a vector file in the PHP runners (follows T137-1-4-3). Cause: opis/json-schema 2.6.0, in its mode that collects every error, stops the properties keyword at the first refused property before it records the checked ones, so additionalProperties then names every later property as unknown; the PHP and PHP extension runners therefore use its default mode and report the first refusal of a file, where the other runners report all. | Acceptance: the fork min-0-max/json-schema, the base orm depends on, records the properties it checked when one fails and runs every keyword group in the mode that collects every error, in a release orm pins; the PHP and PHP extension runners report every refusal in the shared form; no step waits on upstream. | Evidence: the fork (v2.6.0-p1) keeps a failing property checked and lets unevaluatedProperties run after a failing group; 3 of 4 new library cases were red and the library suite passes 6281 of 6281; orm requires opis/json-schema 2.6.0-p1 from the fork's vcs repository (no-api) and tests/dbspec/vectors.php validates with stop_at_first_error false; the new refusals case of runners.test.mts failed on php and php-extension (one line) and passes in every runner; dbspec-php-check, dbspec-php-extension-check, dbspec-compare-check and dependency-policy-check pass. | [o] |
| T137-1-4-4 | Declare the S9 exceptions with their reasons (follows T137-1-4-1). Cause: make layout-check reports the language-specific harnesses (the per-language runners of tests/conformance, tests/dbspec and tests/interfaces), the Go tests of the engine's dialect rules (tests/dialects), the Go programs that need operating-system calls Node lacks (tests/lease, tests/new-session, tests/stop-process) and the PHP generator of the codec vectors, each a test or tool of its own ecosystem by S9. | Acceptance: config/layout.yml declares each as an exception with its reason in a comment above it, and layout-check reports only the scripts that T137-1-4-5 and T137-1-4-6 port. | Evidence: make layout-check reports S9 only for the 19 scripts of scripts/ and the two orchestrators tests/conformance/check and tests/interfaces/check; tests/codec/gen-aes.sh, which only printed where the AES vector is and which nothing ran, is removed instead of declared. | [o] |
| T137-1-4-5 | Port the shell and PHP scripts of scripts/ to TypeScript (follows T137-1-4-1). Cause: 18 shell scripts and 2 PHP scripts under scripts/ are infrastructure in other interpreters (S9). | Acceptance: each is a .mts script with the same behaviour, its callers run it, and layout-check reports none of them. | Evidence: layout-check reports no file under scripts/; make test-servers starts the six servers from scripts/test-servers.mts, a second start reports them running, test-servers-tls reloads TLS, make databases-create creates and seeds the bench and decimal databases of a run and databases.mts drop drops them and purges the binary logs; test-servers-check passes 14 cases (the migration on real servers included), scripts/repo and scripts/check pass 143, and the startLogged case, which failed locally with the shell reader, passes. | [o] |
| T137-1-4-6 | Port the cross-language Go orchestrators to TypeScript (follows T137-1-4-1). Cause: tests/conformance/check and tests/interfaces/check drive and compare every language, which S9 makes TypeScript infrastructure, while they are Go programs. | Acceptance: each is a .mts program with the same outputs and records, and layout-check reports neither. | Evidence: tests/conformance/check and tests/interfaces/check are .mts programs; make layout-check reports neither. Before the Go programs were removed, the TypeScript interface checker gave the same output, exit status and byte-identical symbol records as the Go one for every language, --self-test and --results; make run-databases TARGETS=conformance-check passed with 27 vectors identical in 5 languages on MySQL, PostgreSQL and SQLite. The unit tests of both pass 46, scripts/repo and scripts/check 143, and make interface-check passes. internal/stmtdiff, which neither reads any longer, is removed with its case kept in stmtdiff.test.mts. The rule validation of contracts/validate.go still runs in its Go test. | [o] |
| T137-1-4-7 | Pass the TypeScript lint rules of kit 0.0.46 in the infrastructure (follows T137-1-4-1). Cause: kit 0.0.46 makes no-explicit-any, the no-unsafe rules, no-non-null-assertion and the type assertion rules errors, and the tests still use JSON.parse values and two any parameters. | Acceptance: make kit-sync KIT_TAG=v0.0.46 and the lint of scripts/ and tests/ pass. | Evidence: make kit-sync took kit 0.0.47 (kit-check passes; config/release.json drops buildBackend, config/layout.yml names its root files by vars.root_files per K145); oxlint of make lint-ts reports 0 findings in scripts/ and tests/, down from 154, each fixed in the code without a disable or a cast; tsc of the root project, scripts/repo and scripts/check (143) pass. | [o] |
| T137-1-4-7-1 | Pass the TypeScript lint of kit 0.0.47 in the packages and the site (follows T137-1-4-7). Cause: after T137-1-4-7, make lint-ts reported about 1,270 findings in packages/ and docs/.vitepress (non-null assertions, needless and unsafe type assertions, unsafe any) and knip findings, and lint judged the packages without the strictness of the root (noUncheckedIndexedAccess, exactOptionalPropertyTypes) and the dbspec-language CLI and the site by inferred projects. | Acceptance: every linted file is judged by the project its build uses at the root's strictness, make lint-ts passes, the builds and the client tests pass, and no exported signature of a package entry point changes. | Evidence: make lint-ts exits 0 (oxlint 0 findings, knip passes; its only hints name kit's copied knip.json, which kit K176 removes); the packages take noUncheckedIndexedAccess and exactOptionalPropertyTypes, the dbspec-language CLI and docs/.vitepress have their own projects, and packages/ts builds through project references so knip maps dist to src; typescript-build, editor-build, ts-check/types and ts-check/test, the dbspec-language, lsp, codemirror and vscode checks, docs-dbs-check, docs-build, scripts-typecheck, interface-check and the TypeScript lane of client-db-check pass; the 81 changed symbol keys are non-exported helpers and exports that no entry point re-exports. | [o] |
| T137-1-4-8 | Pass the CI of main after the TypeScript port (follows T137-1-4-1). Cause: on 297b6afd the CI failed in four ways that T137-1-4-1 left: the mutation of packages/ts/tests/dbspec.mjs for a main document outside the documents still expected the pointer of the earlier draft (ts-check, ts-min-check, schema-typescript); make scripts-typecheck ran before the build of packages/dbspec-language, which docs/.vitepress/dbs.ts imports (conformance); and the MySQL migration case copied scripts/test-servers-mysql.mts into a temporary root where the modules it imports do not resolve (static). | Acceptance: the mutation expects the refusal that cases.mts gives, scripts-typecheck builds dbspec-language first, the migration case runs the script of the checkout with the paths of its temporary root, and the three checks pass. | Evidence: packages/ts/tests/dbspec.mjs passes 310 cases; make scripts-typecheck passes from a checkout without packages/dbspec-language/dist; node --test scripts/test-servers-mysql.test.mts with LEASE passes its 5 cases against real MySQL servers. | [o] |
| T137-1-4-9 | Port bench/install and bench/seedaes to TypeScript (follows T137-1-4-5). Cause: scripts/bench-db.mts installs and seeds the bench database through two Go programs written when Go was the only executor (2a640e7c), with no S9 exception; the TypeScript client now installs a schema and holds the drivers. | Acceptance: scripts/bench-db.mts installs schema/bench.dbs through the TypeScript bench model and fills the AES and blind-index columns through the TypeScript codecs; bench/install and bench/seedaes are removed, and the databases/create setup step needs no Go. | Evidence: make databases-create on real servers installs and seeds the MySQL, PostgreSQL and SQLite bench databases through scripts/bench-db.mts; the TypeScript client decodes user4242@example.com from aes_hex_email, its blind index matches, and no author lacks an AES value; make decimal-bench-sqlite builds its database through the same seed; scripts/repo and scripts/check pass 143. | [o] |
| T137-1-4-10 | Name the server sockets by a hash of node:crypto and stop the servers of a worktree before its removal (follows T137-1-4-5). Cause: scripts/test-servers.mts carried a TypeScript copy of POSIX cksum only to keep the socket folder names of the shell script, an algorithm Node's crypto replaces; the migration case removed its fixture's socket folder by the logical /tmp path while the script names it by the real path, leaving one folder per run; and the servers of the removed worktree orm-phpunit-T148-1 ran for 8 hours because nothing stopped them before the removal (reported by kit). | Acceptance: paths() names the folder by SHA-256; the migration case leaves no socket folder; AGENTS.md states make test-servers-stop before a worktree's removal. | Evidence: the leftover servers of orm-phpunit-T148-1 were stopped (the replica, whose data directory was gone, by KILL) and removed the four socket folders whose checkouts no longer exist; after the change make test-servers-check passes 14 cases and leaves no /tmp/orm-servers-* folder; test-servers.test passes 9 and scripts/repo check.test 91. Servers started before this change keep the old socket folder name, so a checkout stops them before it takes the change. | [o] |
| T137-2 | Review the locks of the kit switch (follows T137). Cause: T137 changed package-lock.json and composer.lock and added tests/interfaces/typescript/package-lock.json without a review record, left composer.lock behind composer.json, dropped the TypeScript 6 exception of the interface reader, and kit's dependency on @microsoft/api-extractor brings the sprintf-js advisory 1241202 into orm's lock; make dependency-policy-check failed. | Acceptance: the review records every lock, the exceptions state their grounds as kit's do, and dependency-policy-check passes. | — | [o] |
| T137-3 | Return orm main CI to green after T137 (follows T137). Cause: run 38517144969 failed: CI installed no tests/interfaces/typescript, so the interface reader ran on TypeScript 7 (ts.NewLineKind undefined), and packages/php-ext/src/orm_dbspec_arginfo.h kept the stub hash of the old paths. | Acceptance: the CI step installs the interface TypeScript and the arginfo check passes, red first. | — | [o] |
| T137-4 | Run the interface TypeScript install as its own make (follows T137-3). Cause: run 38519188361 stopped every job at its Node step: make install-packages install-interfaces-typescript reads kit.mk before install-packages installs it (kit.mk is missing). | Acceptance: the step runs two makes; without node_modules the joined form fails and the split form passes. | — | [o] |
| T138 | Lock mermaid at its latest release. Cause: the dependency review of 2026-10-13 found mermaid 12.2.0 released while the root package locked 12.1.0, which the dependency policy refuses. | Acceptance: mermaid is locked at 12.2.0 and the review records it. | Evidence: make dependency-review RECORD=1 reported the finding before the change and not after it; npm run docs:check passes with the 20 Mermaid sources. | [o] |
| T139 | Read and write the YAML codec with one pure-Rust YAML library. Cause: the review of 2026-10-13 found RUSTSEC-2024-0449 (unsafe-libyaml unmaintained) in packages/orm-rust and bench/rust, reached through serde_yaml_ng, which the Rust codec uses to decode and encode while yaml-rust2 already validates the same text; the fork serde_norway has had no release since 2024-12. | Acceptance: the yaml stage decodes and encodes with yaml-rust2 alone, under the rules of docs/spec/codec.md (the common value model, the refused forms with CODEC_DECODE, CODEC_ENCODE on an encode failure); serde_yaml_ng and unsafe-libyaml leave both locks; red first on the codec vectors where behaviour changes; the Rust codec tests and the cross-client codec checks pass; make dependency-policy-check passes. | Evidence: before the change the new Rust codec tests failed on 11 inputs the old decoder read against the YAML 1.2 core schema (07 as a string, 0b101 as 5, u64 integers accepted, among others); after it cargo test --lib codec::tests passes 11 tests, make codec-check passes the Go, Rust, TypeScript and PHP vectors, make dependency-policy-check reports no advisory, and serde_yaml_ng and unsafe-libyaml are gone from both locks. | [o] |
| T139-1 | Write every string with the Rust YAML codec (follows T139). Cause: yaml-rust2's YamlEmitter leaves 0o17 and some control characters plain and does not escape C1 characters, U+FEFF, U+FFFE or U+FFFF, so the Rust encoder's read-back check refuses those strings with CODEC_ENCODE, a capability the other clients have. | Acceptance: the Rust codec writes every string of the common value model, through yaml-rust2 (an upstream fix or its documented options), red first on each refused string. Finding (2026-10-14): yaml-rust2 0.13.0 is the latest release, and its successor saphyr 0.2 has the same emitter: need_quotes omits 0o, U+0007, U+0008, U+000B, U+000C, U+001B, U+007F, C1, U+FEFF, U+FFFE and U+FFFF, and escape_str escapes only bytes below U+0020 and U+007F, so no option of either meets it; the path is an upstream fix to need_quotes and escape_str. | — | [o] |
| T139-2 | Record the Rust symbols of the saphyr YAML codec (follows T139-1). Cause: run 38504701356 failed with missing ...YamlReader as MarkedEventReceiver::on_event: T139-1 changed the reader's trait without recording. | Acceptance: the Rust record holds the new symbols and interface-check passes. | — | [o] |
| T140 | Restore follows the request hook rule of update. Cause: restore cannot carry a request hook condition: the restore key accepts only key equality predicates, so a hook's added condition is IR_INVALID, and the model picks the key from the set values before the hook runs, so a module that no longer writes a scope column cannot pick a unique key containing it. | Acceptance: the planner picks the restore key after the hook from the set assignments and the and-joined equality conditions (the primary key when covered, else the first unique key by name); Model::restore sends only the set; restore's where may carry further and conditions beyond the key, and at most one row is still guaranteed; in all six clients, the protocol (+ko) and conformance, red first in each client. | Evidence: built by the hyper session red first in each client and reviewed there (the read-back of a key a hook rewrote, the Rust SetValue match, the eq wording and four shared cases were fixed after the review); one function per client holds the rule; make interface-check passes in every language, go test ./engine/ir/... ./engine/planner/... passes, and make markdown-conformance-check passes its restore case against the flowmark commit that follows the rule (flowmark T14). | [o] |
| T140-1 | Mirror the flowmark table at the new pin (follows T140). Cause: T140 pinned the flowmark commit 5e065f4 without rewriting the table "Markdown IR executor" of docs/spec/dialects.md (+ko), which mirrors flowmark's docs/spec/60-dialect.md at the pinned revision, so CI run 38444557164 failed markdown-table-check. | Acceptance: the tables mirror flowmark at the pin. | Evidence: node scripts/docs/markdown-table.mjs --write rewrote them, and the check reports both tables mirror flowmark spec 60 at v0.0.0-20261013153052-5e065f4d1b58. | [o] |
| T141 | Name the Python byte check and file reader in docs/spec/dbspec.md "Files". Cause: the section lists the byte check and file reader of Go, PHP, TypeScript and Rust, and Python has read_dbspec_bytes (packages/orm-python/src/polyspec/orm/dbspec/file.py). | Acceptance: the section (+ko) names Python's functions. | Evidence: the section (+ko) names read_dbspec_bytes and read_dbspec_file, the functions of packages/orm-python/src/polyspec/orm/dbspec/file.py; make documents-check passes. | [o] |
| T142 | Read YAML by the core schema in Go and PHP. Cause: the T139 probe found Go's yaml.v3 reading 0b101 as 5, +0x1 as 1, 1_000 as 1000 and U+0085 as a line break and refusing 2014-12-31, and PHP reading 1_000, timestamps and U+0085 alike, while docs/spec/codec.md requires a YAML 1.2 document; strings that the Rust or TypeScript client writes plain then read back differently. | Acceptance: Go and PHP decode by the YAML 1.2 core schema, with shared codec vectors for each form, red first. | — | [o] |
| T142-1 | Read YAML by the core schema in PHP (follows T142). Cause: symfony/yaml 7.4 has no schema flag and no scalar hook (Inline::evaluateScalar is private), and Yaml::parse reads 1_000 as 1000, 2014-12-31 as 1419984000, 2001-12-14t21:59:43.10-05:00 as 1008385183.1 (PARSE_DATETIME gives a DateTime, still not the text) and 0777 as the string 0777 (core: 777); tests/codec/check.php skips the vectors yaml_core/underscore_int_like, date_like, timestamp_like, leading_zero_int and map by name, printing each. Retry: choose a PHP YAML 1.2 core reader from the ecosystem (survey ext-yaml callbacks and other packages), or record the ruling to resolve scalars from a parser event API; then remove the exclusion list. | — | Evidence: codec-check, client-unit-check, php-min-check, interface-check and dependency-policy-check pass. | [o] |
| T142-1-1 | Remove the PHP exclusions of yaml_core/signed_hex_like and yaml_core/leading_zero_int (follows T142-1). Cause: purrphp/yaml 1.0.0 CoreSchema reads +0x1 as 1, -0o7 as -7, 0777 as the float 777.0, and nULL or tRUE as null or true, against YAML 1.2.2 section 10.3.2. | Acceptance: the fork min-0-max/yaml, the base orm depends on, fixes core resolution by the YAML 1.2.2 table in a release orm pins, and the PHP exclusion lists of the codec vectors are empty; no step waits on upstream. | Evidence: the fork's CoreSchema resolves scalars by section 10.3.2 (v1.0.0-p1, 12 of 17 new library cases red on 1.0.0, the library suite 406 green); orm requires purrphp/yaml 1.0.0-p1 from the vcs repository of the fork (root composer.json, the consumer manifest and docs/operations/packaging.md, since Composer reads repositories from the root manifest only); with the exclusions of CodecVectorsTest and HostcodecTest removed, signed_hex_like, leading_zero_int (asserted as the int 777) and the mixed-case keys failed on 1.0.0 and pass on the fork; codec-check/compare, client-unit-check/hostcodec and dependency-policy-check pass. | [o] |
| T142-2 | Read U+0085 as content in Go and Python (follows T142). Cause: the yaml.v3 and PyYAML scanners read U+0085 as a line break (a<U+0085>b decodes to a b), below any Node API or resolver hook; packages/orm-go/orm/codec_test.go (goCodecExclusions), packages/orm-python/tests/codec_vector.py (PYTHON_EXCLUSIONS) and tests/codec/check.php skip yaml_core/next_line by name, printing it. Retry: a scanner option or a reader of each ecosystem that keeps U+0085, or the ruling recorded; then remove the exclusions. | — | Evidence: codec-check, python-unit-check, interface-check and dependency-policy-check pass; the Go codec tests pass. | [o] |
| T142-9 | Make the YAML refusals of docs/spec/codec.md hold in every client (follows T142-2). Cause: docs/spec/codec.md refuses anchors and explicit tags, but Go and Python accept an anchor without an alias and the core tags (!!str, !!int), while PHP refuses them; an empty stream is an error in Go and null in Python and PHP. | Acceptance: one rule per form in docs/spec/codec.md, a shared vector for each, and every client passes them. | Evidence: six shared refusal vectors in tests/codec/decode-vectors.json fail before the fix and pass in all five clients; codec-check, python-unit-check and interface-check pass. | [o] |
| T142-10 | Refuse non-string YAML map keys in TypeScript by the parsed node (follows T142-9). Cause: packages/ts/src/codec.ts decodeYaml refuses plain boolean, null and float keys with a regex on the key's source text, a second reading of the core schema beside the yaml library. | Acceptance: the key's type comes from the library's resolved scalar, the regex is removed, and the key-refusal vectors pass. | Evidence: the TS cases tRUE, nULL and .InF fail before the fix; four shared key-refusal vectors pass in all five clients; codec-check passes. | [o] |
| T142-11 | Compare non-string values in the shared decode vectors and share the accepted YAML keys (follows T142-10). Cause: the decode-vector runners compare value as a string, so the accepted keys tRUE, nULL, .InF, 1 and "true" are TS-only cases, and whether Go, Python, Rust and PHP read them as the core schema says is unchecked. | Acceptance: every runner compares a JSON value, the accepted-key vectors are shared, and all five clients pass them. | Evidence: Go, Python, Rust and TypeScript pass every shared decode vector; PHP passes 60 and skips the three mixed-case keys by name, tracked by T142-1-1; codec-check passes. | [o] |
| T142-3 | Run the Python codec vectors in codec-check (follows T142). Cause: no make target runs packages/orm-python/tests/codec_vector.py (python-unit-check matches test_*.py), so Python's YAML defects went unnoticed. | Acceptance: codec-check runs it, red first with a broken vector. | — | [o] |
| T142-4 | Make codec-check provision the lease tool (follows T142). Cause: in a fresh checkout make codec-check fails with .runtime/bin/lease ENOENT until make lease-tool runs; codec-check/rust lacks the prerequisite. | Acceptance: codec-check depends on lease-tool, red first in a fresh worktree. | — | [o] |
| T142-5 | Make codec-check install the PHP dependencies it runs (follows T142-4). Cause: in a fresh checkout codec-check/compare stops with a PHP fatal error from packages/orm-php/tests/autoload.php until make install-php runs. | Acceptance: the target has the prerequisite, red first in a fresh worktree. | — | [o] |
| T142-7 | Check the PHP vendor in every PHP target (follows T142-5). Cause: testcase-check/php, the dbspec-*-php checks and client-unit-check load vendor-php without php-vendor-check, so a fresh checkout ends in a PHP fatal error instead of the named step. | Acceptance: each takes the prerequisite, red first in a fresh worktree. | — | [o] |
| T142-8 | Check the PHP vendor in the node scripts that run PHP (follows T142-7). Cause: scripts/case-database-check.mjs (model_test.php), scripts/unselected-column-physical-check.mjs and scripts/decimal-physical-check.mjs run PHP that loads vendor-php, while their make targets lack php-vendor-check. | Acceptance: each target takes the prerequisite, red first in a fresh worktree. | — | [o] |
| T143 | Detect every Windows name surrogate in the TypeScript and PHP set readers (follows T128-2a-5). Cause: docs/spec/dbspec.md step 3 treats any reparse point whose tag is a name surrogate as a symbolic link; Node's lstat and PHP's lstat report only symbolic links and junctions, so other name-surrogate tags (the WSL symlink tag) are followed, while Go, Rust and the PHP extension check IsReparseTagNameSurrogate. | Acceptance: the TypeScript and PHP readers reach the reparse tag through a declared mechanism, or the specification states the supported tags with its grounding; never a silent difference. | — | [o] |
| T144 | Run packages/orm-php/bin/orm-gen from the checkout. Cause: php packages/orm-php/bin/orm-gen stops with Class "Polyspec\Orm\OrmGen" not found because its autoload lookup does not find vendor-php, so the command line of the README fails; vendor-php/bin/orm-gen works. | Acceptance: the bin finds the autoloader the repository installs, red first. | Evidence: run without Composer's proxy, the bin found no autoloader and died with Class "Polyspec\Orm\OrmGen" not found; it now takes the autoloader by Composer's documented lookup ($_composer_autoload_path, with composer-runtime-api ^2.2 required) and exits 2 naming the missing autoloader instead of guessing a path, and the documents run vendor-php/bin/orm-gen, the bin Composer installs; make client-unit-check/orm-gen passes both new cases. | [o] |
| T145 | Lock mysql2 at its latest release. Cause: the dependency review of 2026-10-13 found mysql2 3.24.6 released while packages/orm-npm locked 3.24.5, which the dependency policy refuses. | Acceptance: mysql2 is locked at 3.24.6 and the review records it. | Evidence: make dependency-review RECORD=1 reported the finding before the change and none after it; make dependency-policy-check and make typescript-build pass. | [o] |
| T146 | Format orm-schema's module list. Cause: cargo fmt --all --check fails on main at packages/orm-rust/orm-schema/src/dbspec/mod.rs (mod names; and mod plan_diff out of order). | Acceptance: rustfmt passes on the crate, red first. | Evidence: the hyper session's agent reproduced the red at 69f7748d (mod names and pub mod model out of order at orm-schema/src/dbspec/mod.rs:20); c0e189e9 (T128-2a-6) reordered the modules, and make rust-fmt-check passes all three parts. | [o] |
| T147 | Refuse a component method that no interface rule declares. Cause: make interface-check passes with a Dbspec component method that has no rule (found by T128-2a-7), so a missing rule never shows red. | Acceptance: the manifest check refuses a component method without a rule, red first. Cause (bypass): the refusal, red first on a fixture, finds 25 component methods without a rule on the real manifest, so committing it to main would turn make interface-check red. Retry: when T147-1 to T147-4 are done, merge branch fix/rules-T147 (commit of the check, kept until then and deleted with its worktree orm-rules-T147 when it merges). | Evidence: the retry condition holds (T147-1 to T147-5 done), so the check of b817ba9e is merged: make interface-check passes in all five languages and the extension with every public component method declared, and the branch fix/rules-T147 and its worktree are deleted. | [o] |
| T147-1 | Declare the rules of nine public component methods (follows T147). Cause: Db.close, Collection.get, first, keys, connect, delete, toArray, Utils.setLocal and Utils.local exist in all five languages and have no rule. | Acceptance: each has a rule with its exact symbol per language and its input and output types, recorded. | Evidence: 233fc7b4 declares the nine rules with their symbols, read from contracts/symbols, and their types in contracts/validate.go; make interface-check passes in every language. | [o] |
| T147-2 | Give Python the six public methods it lacks (follows T147). Cause: Db.stats, Utils.privileges, Utils.stats, SchemaUtils.exists, SchemaUtils.installed and SchemaUtils.empty are documented in docs/spec/interfaces.md and absent in Python. | Acceptance: Python implements them with the shared tests, red first, and each gets its rule in every language. | Evidence: before the change the Python tests failed with AttributeError and ImportError for the six methods, and PHP's model_test failed with an undefined Db::stats; Python implements them (fe7070a5), PHP gains Db::stats (7b770346), the six rules are declared (a4aa5769), and make python-unit-check, make client-unit-check and make interface-check pass. | [o] |
| T147-3 | Rule on the internal components of the manifest (follows T147). Cause: Core.build, Request.shape, Planner.validate, Planner.plan, TransactionFlow.begin, savepoint, commit, rollback and Generator.generate are private or internal with no common signature. | Acceptance: the manifest marks a component internal with its grounding and the check covers only public components, or the methods leave the diagram; stated in docs/spec/interfaces-model.md (+ko). | Evidence: 207b98ed marks Core, Request, Planner, TransactionFlow and Generator internal with their grounding (internal, internal_ko), the check skips their methods and refuses a rule on them (red first on Planner.plan), and docs/spec/interfaces-model.md (+ko) shows them <<internal>>. | [o] |
| T147-4 | Put every rule's method in its component (follows T147). Cause: the rule SchemaUtils.addTablesAndColumns exists but its component lists no such method, so docs/spec/interfaces-model.md leaves it out. | Acceptance: the manifest check refuses a rule whose method its component lacks, red first, and the gap is closed. | Evidence: before the change the fixture test accepted the rule SchemaUtils.addTablesAndColumns without its component method; cd6d4d84 refuses it and lists the method in SchemaUtils. | [o] |
| T147-5 | Give Rust's AesKeyring versions and declare its rule (follows T147). Cause: the component check finds AESKeyring.versions without a rule; Go (AESVersions), PHP, TypeScript and Python have it and Rust's AesKeyring has only new, current and key. | Acceptance: Rust implements it as the others behave, red first, and the rule declares it in every language. | Evidence: before the change a cargo test of keyring_tests failed with no method named versions; 576087a9 adds AesKeyring::versions in ascending order and declares the rule in every language; the test and make rust-check/clippy pass. | [o] |
| T148 | Make the parity binding-run rule (R3) hold for the dbspec units. Cause: T128-3 and T128-2a-7 found that the dbspec units' bindings report other case names (TypeScript, Python), no cases (Rust) or cannot run (PHP needs a phpunit that does not exist, the extension a library no target builds). | Acceptance: every dbspec unit's binding run reports its case ids in every runtime, or kit's rule is corrected with its grounding (kit K42 changes the Go / rule), red first. Bypassed. Cause: R3 needs each case to run on its ecosystem's standard runner under its case id. orm runs its PHP tests on its own script runner (packages/php/tests/*.php), a reinvented test runner, and its shared-vector tests loop over the vectors in one test in Go, TypeScript, Python and Rust, so no case id reaches the runner. Retry: when kit 0.0.43 (K136, K137) is out and T148-1 to T148-3 are done. | — | [!] cause: T128-3 and T128-2a-7 found that the dbspec units' bindings report other case names (TypeScript, Python), no cases (Rust) or cannot run (PHP needs a phpunit that does not exist, the extension a library no target builds).; retry: when kit 0.0.43 (K136, K137) is out and T148-1 to T148-3 are done |
| T148-1 | Run the PHP client's and the extension's tests on PHPUnit (follows T148). Cause: packages/php/tests and packages/php-ext/tests run on orm's own script runner instead of PHPUnit, the PHP ecosystem's standard runner. | Acceptance: the tests are PHPUnit test cases, the shared vectors run through a data provider whose data set names are the case ids, the make targets run PHPUnit, and the hand-written runner is removed. | Evidence: client-unit-check, codec-check, dbspec-php-check, dbspec-php-extension-check, php-min-check, interface-check and dependency-policy-check pass; PHPUnit names each shared vector case by its id. | [o] |
| T148-1-1 | Install the PHP development tools at the repository root (follows T148-1). Cause: T148-1 gave packages/php and packages/php-ext their own Composer projects for PHPUnit, so their released composer.json declared repositories (the ordered-json package and the path of packages/php), require-dev and platform settings, and make release-assets of v0.0.16 refused them: a release zip declares no repository. | Acceptance: the development root (vendor-php) installs PHPUnit with packages/php by its path repository, the tests of both packages run vendor-php/bin/phpunit with their phpunit.xml.dist, the two package locks are removed, the released manifests declare only what a consumer installs, and make release-assets builds the archives. | Evidence: client-unit-check and dbspec-php-extension-check pass on vendor-php/bin/phpunit; make release-assets TAG=v0.0.16 on a local tag of the commit writes every archive. | [o] |
| T148-2 | Report every shared-vector case under its id in Go, TypeScript, Python and Rust (follows T148). Cause: each runs a vector file inside one test. | Acceptance: Go t.Run(<id>), TypeScript one top-level test per id, Python unittest's load_tests with a TestCase per case whose id is the case id, and Rust libtest-mimic trials named by the id. No method names are generated. Bypassed. Cause: Go, TypeScript, Python and Rust report every shared-vector case under its id except the Rust show-create cases, which wait on an orm design decision (T148-2-1). Retry: when T148-2-1 is decided. | Evidence: the retry condition holds: with T148-2-1, Go (t.Run), TypeScript (one test per id), Python (load_tests) and Rust (libtest-mimic trials) report every shared-vector case under its id; T148-2-2 runs the moved Rust vectors. | [o] |
| T148-2-1 | Report the Rust show-create cases under their ids (follows T148-2). Cause: Rust reads tests/dbspec/show-create.json through the crate-private shown_checks, tested in one lib unit test; libtest cannot name a unit test by a hyphenated id, and the public path read_catalog turns the read body into a dbspec document, so it cannot state the vector (kit's ruling: the spec unit is wider than the public path, a design question for orm). | Acceptance: orm decides where the SHOW CREATE reader belongs in its API, and the Rust binding reports each case under its id. | Evidence: the SHOW CREATE reader stays private to the MySQL introspection, as in Go, TypeScript and Python; tests/dbspec/show-create.json states each case through the public path (catalog_queries, catalog_followups, read_catalog), which the new libtest-mimic target packages/rust/schema/tests/dbspec_show_create.rs runs as trials show_create_vectors::<id>. The old binding was R3 "not executed" for all 9 cases (0 passed, 6 filtered out); kit's cargo command on the new binding passes 9; dbspec-rust-check/show-create, the repository, feature, owners and layout checks pass. | [o] |
| T148-2-2 | Run the moved Rust vectors on the workspace and warn on a measured time (follows T148-2). Cause: CI failed on main: the verification commands of T148-2 used -p polyspec-orm with the workspace features of ORM_RUST_TEST_FEATURES, and packages/rust/schema/tests/dbspec_plan.rs failed a case on its CPU time. | — | Evidence: planner-rust-validate, codec-vectors-rust and encryption-codec-rust pass; the repository check passes. | [o] |
| T148-3 | Bind every parity unit and pass parity R2 and R3 (follows T148-1, T148-2). Cause: the parity check reports 15 R2 missing bindings and 196 R3 lines across every unit. | Acceptance: make parity-check passes, run once at the end. Progress: the four test files that the T77-9-4 landings added are bound (go-internal subtest names, php-internal plan reader, ts-internal vector count, and the php support list); make parity-declarations-check holds. The full parity-check (R2, R3) runs once in the final batch. | Commands: make parity-declarations-check | [!] cause: needs the database and Rust runs of CI that this sandbox cannot provide; retry: after the parity bindings run green in CI |
| T148-3-1 | Run the PHP and PHP-extension parity bindings with the PHPUnit of the root Composer project (follows T148-3). Cause: kit's PHP runner looks for vendor/bin/phpunit in each package, while orm installs PHPUnit once in the root Composer project (vendor-php, T148-1-1), so 59 R3 bindings fail to start. | Acceptance: parity runs them after kit 0.0.50 (K178), which finds PHPUnit in the nearest Composer project. Cause of the bypass: a kit tool decides it. Retry: when kit 0.0.50 is released (T181). | Evidence: with kit 0.0.50 (K178, K184) parity finds PHPUnit in the root Composer project (vendor-php): all 62 PHP and PHP extension bindings start through vendor-php/bin/phpunit and none reports a missing phpunit; their remaining R3 findings are test failures and case names, T148-3-8. | [o] |
| T148-3-2 | Bind the Python features that parity R2 finds unbound (follows T148-3). Cause: aggregate-numeric, authenticated-encryption, decimal-model, planner, styled-column-states, install-changed-database, sqlite-paths and statement-events have no Python binding, most of them no Python test. | Acceptance: each has a Python test bound in config/parity.json, or the feature is implemented in Python first. | Evidence: T77-3 bound each of the 8 Python features to a test that runs its cases by fixture id; make parity-declarations-check reports no python R2 line. | [o] |
| T148-3-3 | Bind the Go, PHP, PHP extension and Rust features that parity R2 finds unbound (follows T148-3). Cause: go codec, codec-aes and dbspec-syntax, php and rust codec-aes, and php-extension schema-definition and dbspec-sets have no binding. | Acceptance: each is bound to the test that runs it. | Evidence: after T148-3-3-1 parity R2 reports 0: the Go, PHP, PHP extension and Rust units the item named are bound. | [o] |
| T148-3-3-1 | Name the shared vector cases without a slash (follows T148-3-3). Cause: 91 cases of tests/codec/vectors.json (yaml_core/...) and 304 of tests/dbspec/syntax.json (canonical/..., invalid/...) hold a slash, which Go nests as a subtest, so kit refuses a Go binding of the codec and dbspec-syntax units; the other 5 units of T148-3-3 are bound (codec-aes in Go, PHP and Rust, dbspec-sets and schema-definition in the PHP extension). | Acceptance: the shared case ids hold no slash in every vector file and runner, the Go codec and dbspec-syntax units are bound, and parity reports no R2 line for them. | Evidence: the 91 codec and 304 syntax case ids join their parts with "." in both generators and every runner, the Go codec and dbspec-syntax units bind as symbol/id subtests, parity R2 went from 2 to 0, and make codec-check and dbspec-compare-check pass. | [o] |
| T148-3-4 | Split the binding files that run several suites (follows T148-3). Cause: 63 R3 lines report cases not in the suite: the dbspec cases, mermaid, plans and codec files of every language run other suites' cases or extra tests in one file. | Acceptance: each binding file runs one suite under its case ids. | Evidence: one Go function, Rust trial module, PHP data-provider method, node file or Python module per suite, the extra tests moved beside them and still run in their make targets; every R3 line of a shared file is gone (R3 161 to 38 before the next items). | [o] |
| T148-3-5 | Name the cases of the feature-coverage bindings (follows T148-3). Cause: the coverage_* bindings of Go, TypeScript and Rust report no case names (R3 not executed), and the Rust codec and styled-state cases are lib tests that cannot carry an id. | Acceptance: each case runs under its id. | Evidence: Go, TypeScript, Rust and the Python schema-definition binding run every fixture case under its id, conformance has one binding per dialect comparing each vector with its recorded expectation, and the Go implementation args carry the featurecoverage tag. The Go styled-state fixture now runs its unselected and nonnull_sql_null cases, which it had skipped. | [o] |
| T148-3-6 | Run the Python conformance runner as a test (follows T148-3). Cause: tests/conformance/runner_python.py exits 5 (no tests ran) under parity and lies outside the Python roots (R6). | Acceptance: the Python conformance cases run under their ids from inside the Python package. | Evidence: packages/python/tests/conformance_mysql.py, conformance_postgres.py and conformance_sqlite.py run the Python conformance runner as tests, through the new conformance-check/python target. | [o] |
| T148-3-6-1 | Run the Python conformance target through the harness and the run databases (follows T148-3-6). Cause: conformance-check/python ran python -m unittest … without $(WITH_TEST_ENV), so python-unit-check refused the bare line and the target failed with "BENCH_MYSQL_DSN is required" under make run-databases. | Acceptance: the target runs packages.python.tests.testcase with the run environment, python-unit-check passes and the full conformance-check passes. | Evidence: python-unit-check failed on the bare line and the target failed without BENCH_*; the target now runs $(WITH_TEST_ENV) … -m packages.python.tests.testcase after lease-tool and conformance-python-models, python-unit-check passes, and make run-databases TARGETS=conformance-check passes with the Python part (135 tests) and the three vector files unchanged. | [o] |
| T148-3-7 | Pass parity R4, R5, R6, R7, R9 and R10 (follows T148-3). Cause: the parity check reports 396 R5 (unbound test files), 120 R10 (documents that count four clients), 78 R6 (tests outside their roots), 4 R4 (undeclared suites), 4 R9 (stale client lists) and 2 R7 (TypeScript and Python declaring no error codes). | Acceptance: each rule passes, split further where a group is large. | Evidence: make parity-declarations-check on main prints "every declaration holds" (R4 to R10 report no finding), so each rule passes. | [o] |
| T148-3-7-1 | Pass parity R5: every test file is bound (follows T148-3-7). Cause: R5 reports 420 test files with no binding, mostly single-language tests that no parity unit names. | Acceptance: each such test belongs to a unit bound in the five implementations, or is moved into a unit's existing binding; R5 reports 0. | Evidence: commit 39276d1a on main (config/parity.json, 10 units): the five collection identity tests of T77-6 are declared as units of their language's internal profile, one binding each, with their native ids. make -s parity-declarations-check exits 0 in the worktree linked to the kit checkout (every declaration holds, R5 0). The earlier R5 1 came from a run before the link. | [o] |
| T148-3-7-2 | Pass parity R6: tests outside the declared roots (follows T148-3-7). Cause: R6 reports 76 tests under scripts/, tests/dialects, tests/dbspec, the tests/conformance runners, examples/, benchmarks/rust and packages/dbspec-*. | Acceptance: each is moved under a root or declared in a tools root with its reason; R6 reports 0. | Evidence: commit 1f7e8a53 on main: make -s parity-declarations-check reports R6 misplaced-test 0 (the check line recorded here; the same run reports 5 R5 findings from T169-3, tracked as T169-4, which are not R6). No R6 finding remains on main. | [o] |
| T148-3-7-3 | Give TypeScript and Python a generated code catalog (follows T148-3-7). Cause: R7 reports that TypeScript and Python have no generated error-code catalog, while Go, PHP and Rust use theirs at every error site. | Acceptance: orm-gen generates the catalog in both, every error site of both clients uses it, a drift test fails on a stale catalog, and R7 reports 0. | Evidence: every OrmError, CodecError and error-code literal of packages/ts/src and packages/python/src is now a codes.<NAME> constant of the generated catalog; the sources differ from main only in those constants (checked mechanically, quotes normalized), and the drift tests of T77-4 check the catalog against errors.yaml. | [o] |
| T148-3-7-4 | Close the Python feature gaps of parity R9 and R10 (follows T148-3-7). Cause: with Python in scripts/features/check.mts, 15 features marked implemented do not pass in Python and 2 lack a Python test; Python has no decimal physical test, no pool, timeout or VERIFY_IDENTITY config, no point codec type, no typed model fields and no database tests in client-db-test. | Acceptance: each is implemented and tested in Python as in the other clients, with the shared vectors, and R9 and R10 report no Python finding. | — | [o] |
| T148-3-7-4-1 | Check TLS, pool and timeout config (follows T148-3-7-4). | — | Evidence: commit e41a6450 on main: Python rejects an unknown ssl-mode, applies VERIFY_IDENTITY with an SSLContext, refuses pool options with CONFIG naming PgBouncer or ProxySQL, applies statementTimeoutMs, and honours the MySQL socket parameter. RED: VERIFY_CA was accepted and a wrong CA connected. GREEN: mysql_tls.py, connection_config.py and replica.py pass. | [o] |
| T148-3-7-4-2 | Drop the stale point type from codec (follows T148-3-7-4). | — | Evidence: commit 6ad53395 on main: The stale point paragraph and its case were removed from codec.md; dbspec.md has no point type. Checks pass on main. | [o] |
| T148-3-7-4-3 | Type the generated model fields (follows T148-3-7-4). | — | Evidence: commit 26e129a4 on main: Getters and setters are typed; a getter whose value has another type raises TypeError. RED: test_model_fields.py failed 14 times. GREEN: it passes, and the Python physical conformance runners pass on all three databases. | [o] |
| T148-3-7-4-4 | Run Python in the decimal physical check (follows T148-3-7-4). | — | Evidence: commit e5b1ac66 on main: make decimal-physical-check runs decimal_model_db.py for Python per database. RED: parity R9 reported that decimal-physical-check.mts had no python. GREEN: it passes twice per database. | [o] |
| T148-3-7-4-5 | Pass every implemented feature (follows T148-3-7-4). | — | Evidence: commit 3b525263 on main: Python is a client of scripts/features/check.mts, with 15 coverage modules and their owners. RED: 17 errors. GREEN: the Python lane passes directly and through the poolers. | [o] |
| T148-3-7-4-6 | Keep a connection pool like the other clients (follows T148-3-7-4). | — | Evidence: commit e28c1499 on main: Db keeps a thread-safe pool with poolSize, poolIdleSize and poolLifetimeMs as docs/spec/config.md states. RED: the default pool size was 1 and poolSize was refused. GREEN: connection_pool.py passes, through PgBouncer and ProxySQL too. | [o] |
| T148-3-7-4-7 | Drop the stale point functions (follows T148-3-7-4). | — | Evidence: commit 417b1d10 on main: distance, pointX and pointY were removed from the Go generator's scan list and from docs/spec/dsl.md; no client implements them, and the examples use year(). | [o] |
| T148-3-7-5 | Close the remaining R10 gaps (follows T148-3-7). Cause: Rust has no generator --check, examples/thin-slice covers only Go, PHP and Rust, and the styled value type docs lack TypeScript and Python. | Acceptance: each is added for every client, and R10 reports 0. | Evidence: red: cargo test --test generate check_ in packages/rust/build failed to compile with no method named check found for struct polyspec_orm_build::Builder (tests/generate.rs:449 and :476); node --test tests/docs/styled-value-sections.test.mts failed docs/spec/codec.md lacks the TypeScript StyledValue section and the Fields bullet lacks the TypeScript styled value type; go test -run TestExamplesRequireBenchDSN ./examples failed the ts and python thin-slice subtests, with stderr lacking the DSN requirement and can't open file .../examples/thin-slice/python/main.py. green: cargo test --test generate 26 passed (Builder::check, with its two tests); node --test tests/docs/styled-value-sections.test.mts 2 passed; go test -run TestExamplesRequireBenchDSN ./examples passed all 12 subtests (go, php, ts and python of thin-slice; go and php of complex); make ts-model-check passed, with examples/thin-slice/ts/main.mts in the scan list that the models script scans; make python-model-check passed; tsc --noEmit of examples/thin-slice/ts/main.mts and go vet -tags examples ./examples report no error; make parity-declarations-check reports no R10 finding (its six R5 findings belong to the plan-cache test files of main). The Python scanner follows only named receivers, so the chained calls of the Python example are not scanned and the regenerated models are unchanged. Not run: the bench-database output comparison (TestExampleOutputsAreIdentical, make example-check) and the full suite. | [o] |
| T148-3-7-6 | Keep set manifests out of the vector suites (follows T148-3-7). Cause: R4 counts contracts/fixtures/dbspec-set.json and contracts/fixtures/bench/dbspec-set.json, which are set manifests, as vector suites, and the parity suites glob has no exclude form. | Acceptance: the suites declaration selects vector files only (a kit question if the form is missing, reported to kit), and R4 reports 0. | Evidence: commit e8b0e079 on main (the suites glob excludes the two set manifests, contracts/fixtures/**/!(dbspec-set).json, which kit's glob reader already accepts; the set manifests stay declared as set-consumers units): R4 undeclared-suite is 0 in make -s parity-declarations-check. The new test count is 0 (only config/parity.json changed). documents-check, docs:rules-check and layout-check pass. The declarations check exits 2 for R5 only (T169-4). | [o] |
| T148-3-8 | Pass the PHP and PHP extension parity bindings (follows T148-3-1). Cause: under kit 0.0.50 the 62 PHP bindings start, and 36 php R3 findings remain (identity-load and SchemaSet cases that need the databases, CodecVectors go_output, data-set names that do not match the case ids), and config/parity.json loads .runtime/parity/php-extension/orm_dbspec.so, which no make target builds. | Acceptance: a make target builds that extension before parity, and every PHP and PHP extension binding passes its cases under their ids. | Evidence: parity-check depends on the new parity-prepare target, which builds the PHP extension and the Rust conformance runner and exports ORM_PARITY_PHP_EXTENSION and ORM_PROGRAM_CONFORMANCE (config/parity.json reads the extension from it), PHP conformance and statement events are named per case, and every PHP and PHP extension binding passes; packages/php-ext/scripts/build.sh now makes a relative output path absolute, which had failed the publish copy. | [o] |
| T148-3-ts | TypeScript parity bindings (group of T148-3-ts-1 to T148-3-ts-6) | — | — | [o] |
| T148-3-ts-5 | Make the TypeScript code-catalog file report its cases under their ids (follows T148-3). Cause: code-catalog.mjs ran its cases through cases().run, which writes plain TAP lines that the node parity reporter does not read, so R3 reported code-catalog/matches-errors-yaml as not executed. | Acceptance: each case of the file is a node:test test named by its id, and make run-databases TARGETS=parity-check UNITS=ts-code-catalog exits 0 with the id passed and no R3 line for the unit. | Evidence: RED: the parity run of the six units reported R3 not-executed ts-code-catalog typescript ...: not executed: code-catalog/matches-errors-yaml (exit 2). GREEN: the run of ts-code-catalog exits 0 with ts-code-catalog typescript: exit 0, 1 of 1 ids passed and no R3 line. | [o] |
| T148-3-ts-6 | Make the TypeScript collection-key file report its two cases under their ids (follows T148-3). Cause: collection_identity.mjs ran its cases through cases().run, which writes plain TAP lines that the node parity reporter does not read, so R3 reported collection-key/identity-from-fixture and collection-key/null-is-codec-decode as not executed. | Acceptance: each case is a node:test test named by its id, and make run-databases TARGETS=parity-check UNITS="ts-internal-collection-identity-from-fixture ts-internal-collection-null-is-codec-decode" exits 0 with both ids passed and no R3 line for either unit. | Evidence: RED: the parity run of the six units reported not executed: collection-key/identity-from-fixture and not executed: collection-key/null-is-codec-decode for the two units. GREEN: the same run exits 0 with ts-internal-collection-identity-from-fixture, ts-internal-collection-null-is-codec-decode typescript: exit 0, 2 of 2 ids passed and no R3 line. | [o] |
| T148-3-ts-4 | Run the TypeScript statement-fault entry-point check as its own test (follows T148-3). Cause: statement_fault.mjs reported statement-fault-entry/entry_point beside its four fixture cases, so R3 reported that id as not in the suite. | Acceptance: the entry-point check is in a file of its own, bound as the feature unit ts-statement-fault-entry, and make run-databases TARGETS=parity-check UNITS="statement-fault ts-statement-fault-entry" exits 0 with no R3 line. | Evidence: RED: the parity run of the six units reported R3 not-executed statement-fault typescript ...: not in the suite: statement-fault-entry/entry_point. GREEN: the run exits 0 with zero R3 lines, statement-fault typescript: exit 0, 4 of 4 cases passed in both runs, and ts-statement-fault-entry typescript: exit 0, 1 of 1 ids passed. | [o] |
| T148-3-ts-1 | Bind the TypeScript install-changed-database case to its own file (follows T148-3). Cause: the binding ran schema-set.mjs, which reports its several-schemas, unregistered-schema, edited-manifest, external-document and register cases beside the install case, so R3 reported those fifteen ids as not in the suite. | Acceptance: the install fixture runs in packages/ts/tests/install_changed_database.mjs, bound to the unit, and make run-databases TARGETS=parity-check UNITS=install-changed-database exits 0 with no R3 line. | Evidence: RED: the parity run of the six units reported R3 not-executed install-changed-database typescript packages/ts/tests/schema-set.mjs: not in the suite: schema-set/several_schemas/sqlite, ... (exit 2). GREEN: the run of install-changed-database exits 0 with install-changed-database typescript (1 of 2): exit 0, 1 of 1 cases passed and the same for (2 of 2), and no R3 line. The fifteen schema-set cases stay in schema-set.mjs, the subject of T148-3-ts-2. | [o] |
| T148-3-ts-2 | Bind the TypeScript dbspec-set-decimal case to a file that reports the decimal document, and the schema-set cases to feature units (follows T148-3). Cause: the binding ran schema-set.mjs, which never reports decimal_schema.dbs, and reports fifteen schema-set cases that the suite does not hold. The decision: the decimal document must run in a TypeScript file, since the set-consumers profile says the TypeScript tests run over the decimal set, and a suite file reports only its suite's names, so the document case moves to dbspec-set-decimal.mjs and the schema-set cases become the feature units ts-schema-set-*. | Acceptance: dbspec-set-decimal.mjs reports decimal_schema.dbs on SQLite, MySQL and PostgreSQL, and the TypeScript binding of the unit shows no R3 line. | Evidence: the TypeScript binding of dbspec-set-decimal reports decimal_schema.dbs (1 of 1, exit 0, twice) and the fifteen ts-schema-set-* units pass as 15 of 15 in the run of T148-3-php-1, with no TypeScript R3 line. The PHP binding that kept the unit bypassed is closed by T148-3-php-1. | [o] |
| T148-3-php | PHP parity bindings (group of T148-3-php-1) | — | — | [o] |
| T148-3-php-1 | Bind the PHP dbspec-set-decimal case to its own data set (follows T148-3-ts-2). Cause: the PHP binding of dbspec-set-decimal ran all of packages/php/tests/SchemaSetTest.php with no symbol, so its fifteen data sets were not in the suite and decimal_schema.dbs was not executed (R3, exit 2). | Acceptance: the binding names the method SchemaSetTest::test_decimal_document, whose one data set is decimal_schema.dbs and which installs the decimal set on SQLite, MySQL and PostgreSQL and reads two amounts back exactly; make run-databases TARGETS=parity-check UNITS="dbspec-set-decimal ts-schema-set-*" exits 0 with no R3 line. Red: the run exited 2 with R3 not-executed dbspec-set-decimal php ...: not executed: decimal_schema.dbs; not in the suite: several_schemas with data set "sqlite", .... Green: the run of dbspec-set-decimal plus the fifteen ts-schema-set-* units exits 0, with dbspec-set-decimal php (1 of 2) and (2 of 2) at exit 0, 1 of 1 cases passed, the TypeScript binding at 1 of 1 cases passed twice, and 3 bindings ran twice, 17 cases passed, with no R3 line. The suite binds the method by symbol, which a suite binding takes, not id. | Evidence: RED: the parity run of the six units reported R3 not-executed dbspec-set-decimal typescript packages/ts/tests/schema-set.mjs: not executed: decimal_schema.dbs; not in the suite: schema-set/several_schemas/sqlite, ... (exit 2). GREEN for TypeScript: the run dbspec-set-decimal ts-schema-set-* reports dbspec-set-decimal typescript (1 of 2): exit 0, 1 of 1 cases passed (and 2 of 2) and ts-schema-set-* typescript: exit 0, 15 of 15 ids passed, with no TypeScript R3 line; the run still exits 2 on the PHP R3 line of the same unit. | [o] |
| T148-3-ts-3 | Run the IR-answer cases in their own TypeScript file under their ids (follows T148-3). Cause: the ir-answers binding ran markdown_conformance.mjs in its flowmark mode, so R3 reported its ids not executed and the flowmark case ids not in the suite; the file's --ir-answers mode, which a node binding cannot pass, ran its cases through runner.run (plain TAP lines) under the names ir-answers/<id>. | Acceptance: the IR answers run in packages/ts/tests/ir_answers.mjs as node:test tests named by the fixture case ids, the flowmark cases stay in markdown_conformance.mjs, and make run-databases TARGETS=parity-check UNITS="ir-answers markdown-conformance" exits 0 with no R3 line. | Evidence: RED: the parity run reported R3 not-executed ir-answers typescript packages/ts/tests/markdown_conformance.mjs: ... not executed: relation_many_empty, relation_one_null, commit_write_conflict, commit_deadlock; not in the suite: insert_one, ... (exit 2). GREEN: the same run exits 0 with ir-answers typescript (1 of 2): exit 0, 16 of 16 cases passed (and 2 of 2), markdown-conformance typescript (1 of 2): exit 0, 35 of 35 cases passed (and 2 of 2), and zero R3 lines. The case functions moved to markdown_cases.mjs, which the flowmark setup's orm-gen scan reads, and orm-gen gen --check still passes against the committed models. | [o] |
| T148-4 | Pass the four non-Rust causes of the full parity run that exits 2 (follows T148). Cause: the full parity run exits 2 on four causes outside Rust: py-decimal-model-db fails with three test failures, the two PHP performance gates and the five PHP extension declaration units exit 2 for missing environment that parity-check did not export (T148-5), and the TypeScript AesKeyring public symbols are not declared. | Acceptance: each child below is done. | Commands: vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/PerfGateTest.php, vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/CoveragePerformanceGateTest.php, make ts-check/types | [o] |
| T148-4-1 | Pass py-decimal-model-db in the parity run (follows T148-4). Cause: the python unit runs python -m unittest on packages/python/tests/decimal_model_db.py, which reports Ran 2 tests … FAILED (failures=3) when DECIMAL_MYSQL_DSN, DECIMAL_POSTGRES_DSN and DECIMAL_SQLITE_DSN are unset, because each round-trip subtest raises "is required". | Acceptance: make run-databases TARGETS=parity-check UNITS="py-decimal-model-db" exits 0 with no R3 line; the red is reproduced first, and the fix lands on the side that was wrong. | Evidence: RED: var/proof-red.log reports py-decimal-model-db python: exit 1 and R3 not-executed … FAILED (failures=3); the same three failures reproduce with the DSN variables unset in var/py-decimal-noenv.log. GREEN: var/py-decimal-red.log is the make command with exit 0 on cebf00eb, 1 of 1 ids passed, and no R3 line. The make command does not reproduce a red on this tip because T148-5 already exports the DECIMAL_*_DSN names through DECIMAL_ENV, so no code changed under this item. | [o] |
| T148-4-2 | Pass the two PHP performance gates (follows T148-4). Cause: CoveragePerformanceGateTest::test_hot_path_gate and PerfGateTest::test_perf_gate exit 2 with 0 of 1 ids passed, because parity-check did not export ORM_BENCH_MYSQL_DSN, which their guards require (T148-5 exports it). | Acceptance: both PHP bindings exit 0 with 1 of 1 ids passed in the parity run, with no R3 line for them. | Evidence: var/t148-4-23.log reports php-internal-coverage-performance-gate-hot-path-gate php: exit 0, 1 of 1 ids passed and php-internal-perf-gate-perf-gate php: exit 0, 1 of 1 ids passed, and the run ends check: result passed with exit 0. | [o] |
| T148-4-3 | Pass the five PHP extension declaration units (follows T148-4). Cause: the five units of packages/php-ext/tests/DeclarationsTest.php (required-modules, declarations, client, package, mirrors) exit 2 with 0 of 5 ids passed, because the binding loads the extension through php -d extension=${ORM_PARITY_PHP_EXTENSION} and the run found no built library (T148-5 exports the extension path). | Acceptance: the five bindings exit 0 with 5 of 5 ids passed in the parity run, with no R3 line for them. | Evidence: var/t148-4-23.log reports php-extension-internal-declarations-required-modules, …-declarations, …-client, …-package, …-mirrors php-extension: exit 0, 5 of 5 ids passed, and the run ends with exit 0. | [o] |
| T148-4-4 | Declare the TypeScript AesKeyring public symbols (follows T148-4). Cause: the full parity run exits 2 on the TypeScript AesKeyring public symbols, which the interface specification names (AesKeyring.versions in contracts/interfaces.json) but the declared symbol set does not cover in the run. contracts/symbols/typescript.json already declares AesKeyring, its currentVersion, key, rotateRow and versions as exported; the parity line that names the missing symbol is not in var/t148-4-23.log, so the exact missing symbol is confirmed by a red run before the fix. | Acceptance: the TypeScript symbol declarations match the public members of packages/ts/src/aes.ts that contracts/interfaces.json names, and the parity check that reports them exits 0. | Evidence (2026-10-18): make interface-typescript-check exits 0 and contracts/symbols/typescript.json declares AesKeyring, currentVersion, keys, key, rotateRow and versions; R11 is evaluated only by the full parity-check, so the row is verified in the single final run (T77-8-1-6). Evidence (2026-10-19): the full-run exit of this cause was interface-check failing before the units: T211 left five private PHP and Rust symbols undeclared, recorded in a06f6e30, after which make interface-check/self-test passes all ten languages and extensions (var/interface-symbols-green.log). RED: with AesKeyring.versions removed from contracts/symbols/typescript.json, the same check fails not ok - interfaces/typescript with unexpected packages/ts/src/aes.ts::AesKeyring.versions (var/t148-4-4-red.log). GREEN: with the declaration, UNITS="aes-keyring aes-keyring-versions" make run-databases TARGETS=parity-check exits 0, parity-check PASS (var/t148-4-4.log). | [o] |
| T148-4-5 | Make TestApplyStressPlan prove its property at the smallest size, with progress and its own timeouts (follows T148-4). Ruling: it is a correctness test at scale, so it stays a parity unit; it was redesigned instead of removed. Cause: go-internal-dialects-apply-stress-physical binds TestApplyStressPlan, the 2000-table benchmark of dbspec-apply-stress-bench (22000 MySQL steps, budgeted at 250 ms a step), as a feature unit; parity runs it with go test's default 10-minute timeout, so it ran 600 s, printed nothing until the timeout and failed with a goroutine dump (full parity run on bccfc382). A test that runs for minutes and reports only its start and end shows no progress. | Acceptance: the benchmark is declared as a bench, not a parity unit, and stays in BENCH_TARGETS; a unit test with its own timeout covers the apply path; every long-running test prints each step, its result and its elapsed time. | Evidence: RED: the unit printed no step for 600 s and failed on go test's default timeout (full parity run on bccfc382). GREEN: make dbspec-apply-stress-bench with 100 tables (more than max_locks_per_transaction 64), a second PostgreSQL session requiring 0 relation locks of the applying session after every step, a progress line every 200 steps with elapsed time and a 30 s stall guard: mysql 1100 steps 52.87 s, postgres 1100 steps 2.56 s, sqlite 600 steps 0.35 s, each verified with history applied <steps>, PASS in 56.8 s. | [o] |
| T148-5 | Export the bench and decimal DSNs that the parity bindings read (follows T148-3). Cause: parity-check exported only the BENCH_* DSNs, not ORM_BENCH_MYSQL_DSN, the DECIMAL_*_DSN names or ORM_DBSPEC_EXTENSION, so PerfGateTest and CoveragePerformanceGateTest refused with "ORM_BENCH_MYSQL_DSN is required", the decimal unit had no DECIMAL_<DIALECT>_DSN, and DeclarationsTest found no built library. | Acceptance: parity-check exports those names from the run's TEST_ENV and DECIMAL_ENV files and its PARITY_RUN_DIR, and the guards stay as written. | Evidence: var/proof-red.log (before the change) exits 2 with the decimal unit failing and every PHP binding exiting 2; var/proof-green.log (after) has py-decimal-model-db exit 0 with 1 of 1 ids passed, the two PHP perf-check bindings exit 0, and the DeclarationsTest binding exits 0. The run still exits 2 on R3 "not executed" for the PHP ids, a kit 0.0.53 id-matching defect reported to kit and not fixed here. | [o] |
| T148-3-9 | Run the Rust bindings of raw cargo tests through tests/cargo-test.mts (follows T148-3). Cause: 12 Rust units (the client bench, example, benchmark and seeded tests) bind a raw cargo test --manifest-path run, which gives the test no copied program, so polyspec_orm_testcase::program panics with "ORM_PROGRAM_<NAME> is unset". | Acceptance: each binding runs through the make target that runs tests/cargo-test.mts, and its units pass under their ids. | Evidence: RED on cf01f139 (full parity log of main, /private/tmp/claude-501/-Users-maxkwon-Projects-flowmark/c4b7f7df-0a6e-4130-a59c-96219442a2d7/scratchpad/full-main-parity.log): the twelve units panic with "ORM_PROGRAM_CLIENT_BENCH is unset" (and ORM_PROGRAM_COMPLEX, ORM_PROGRAM_NATIVE, ORM_PROGRAM_DRIVER_COMPARE), 0 of 12 ids passed. GREEN: make run-databases TARGETS=parity-check UNITS="<the 12 ids>" passes (parity-check PASS, 12 of 12 ids). The five bindings now run make feature-rust-unit, which runs the test target through tests/cargo-test.mts, so the copied programs reach the test. | [o] |
| T148-3-10 | Point the sqlite send-savepoint test at a directory that exists under parity (follows T148-3). Cause: parity-check exports ORM_SEND_SQLITE_DSN into .runtime/run/parity-check-<pid>, which nothing creates (only rust-send-savepoint-check/test runs mkdir for its own directory), so tx::send_tests::sqlite_send_savepoint fails with "unable to open database file", and 39 ids of the --lib Rust units fail with it. | Acceptance: the DSN names the run directory that parity-prepare creates, and those units pass under parity. | Evidence: RED on cf01f139: make run-databases TARGETS=parity-check UNITS="<the 39 ids>" fails with "failed outside the ids: tx::send_tests::sqlite_send_savepoint"; the test panics with "declared database must connect: ... unable to open database file" (SQLite error 14). The same test binary passes when its DSN names an existing directory and fails when the directory is missing. GREEN: the same run passes (parity-check PASS, every unit exit 0, 39 of 39 ids), with ORM_SEND_SQLITE_DSN in the step run directory that parity-prepare creates. | [o] |
| T148-3-11 | Make the dbspec runtime model list match its test (follows T148-3). Cause: schema/tests/dbspec_runtime.rs:65 (runtime_model_of_bench) asserts the table list of the bench model and the model lists day_cell where the assertion does not. | Acceptance: the list and the model agree for the reason the test states, and the test is red before and green after the fix. | Evidence: RED: cargo test -p polyspec-orm-schema --test dbspec_runtime fails at schema/tests/dbspec_runtime.rs:65, with the bench model (which lists day_cell) against the expectation (which does not). bench.dbs declares day_cell at line 185, and commit 9f27a968 added it to every client but the Rust expectation. GREEN: the same test passes (4 of 4), and make run-databases TARGETS=parity-check UNITS="<the 4 ids of the group>" passes (parity-check PASS, 4 of 4 ids). | [o] |
| T148-3-12 | Encode a MySQL DateTime parameter of the zone tests (follows T148-3). Cause: orm/tests/zone.rs:329 and :374 fail with CODEC_ENCODE "parameter 0 (DateTime(2026-01-02T00:00:00)) is not a datetime value" on mysql, and the other zone cases of the binary fail in the same run. | Acceptance: the zone tests pass under parity for the reason the test states, and the test is red before and green after the fix. | Evidence: RED on cf01f139 (full parity log of main): the zone units fail with CODEC_ENCODE "parameter 0 (DateTime(2026-01-02T00:00:00)) is not a datetime value" at zone.rs:329 and :374, and 6 of 11 ids passed. The cause is the test: since #T89-3 (14f1796a) every client takes a date or datetime slot as its canonical text, and the Rust bind check refuses a native chrono value, so the zone tests bound a value that is refused by design. GREEN: the zone tests bind the canonical text (YYYY-MM-DD HH:MM:SS.ffffff in UTC, docs/spec/protocol.md), and make run-databases TARGETS=parity-check UNITS="<the 11 ids>" passes (parity-check PASS, 11 of 11 ids). | [o] |
| T148-3-13 | Encode a SQLite Date parameter of the value types test (follows T148-3). Cause: orm/tests/value_types.rs:117 fails with CODEC_ENCODE "parameter 3 (Date(2026-01-02)) is not a date value" on sqlite. | Acceptance: the value types test passes under parity for the reason it states, and the test is red before and green after the fix. | Evidence: RED on cf01f139 (full parity log of main): rust-runtime-value-types fails at value_types.rs:117 with the sqlite CODEC_ENCODE "parameter 3 (Date(2026-01-02)) is not a date value", 0 of 1 ids passed. It is the cause of T148-3-12: the test bound a native date, which the bind check refuses since #T89-3. GREEN: the test binds the canonical text YYYY-MM-DD, and make run-databases TARGETS=parity-check UNITS="rust-runtime-value-types" passes (parity-check PASS, 1 of 1 id). | [o] |
| T148-3-14 | Bind the Rust integration, conformance and benchmark tests to a target the binding can run (follows T148-3). Cause: 18 units name a test file that is a module of a target or a src file of a package with no library, so the kit's cargo runner runs cargo test --lib and cargo reports "no library targets found in package polyspec-orm-tests" (or polyspec-orm-bench). | Acceptance: each unit runs the target that holds its test, under its id, and passes. | Evidence: RED on cf01f139 (full parity log of main): the 18 units fail with "no library targets found in package polyspec-orm-tests" (or polyspec-orm-bench), 0 of 18 ids passed. GREEN: make run-databases TARGETS=parity-check UNITS="<the 18 ids>" passes (parity-check PASS, 18 of 18 ids). The plain tests run through feature-rust-unit in their own target (--test integration, --bin integration, --bin conformance, --bin native). The coverage tests are ignored and need a database, so they run through feature-rust-database with ORM_FEATURE_DATABASE=mysql and BENCH_MYSQL_DSN, exact name and --include-ignored, as scripts/features/coverage.mts runs them per database. The parity ids name no database, so those units run on mysql; contracts/features.json runs the same seven test files on postgres and sqlite. | [o] |
| T148-3-14-1 | Bind the postgres and sqlite units of the integration coverage tests (follows T148-3-14). Cause: T148-3-14 bound the seven integration coverage tests to MySQL only, because the parity ids name no database, while the Go units of the same features have one unit per database and features.json runs these files on all three databases. | Acceptance: each coverage unit has a postgres and a sqlite unit that runs the same test against that database, and parity passes them. | Evidence: RED: the 22 new ids (postgres and sqlite of the 11 mysql units) name no unit, so parity-check reports 22 findings ("names no unit of config/parity.json"). GREEN: config/parity.json declares them with FEATURE_DATABASE=postgres and BENCH_POSTGRES_DSN, and sqlite with BENCH_SQLITE_DSN; make run-databases TARGETS=parity-check UNITS="<the 22 ids>" passes (parity-check PASS, 22 of 22 ids, no R3 not-executed line). | [o] |
| T149 | Cover Python in schema_definition. Cause: schema_definition has Python planned with no verification command, so the Python set reader and analysis tests are owned only through make python-check. | Acceptance: Python's schema_definition coverage runs with its verification command (the file ownership part moved to T151). | Evidence: before the change node scripts/features/coverage.mjs --feature schema_definition --part owner/python failed with undeclared owning client, since the checker knew no Python runner; the checker now runs Python owners on ORM_PYTHON (a missing one fails, never a python of PATH), schema_definition declares schema-python (make dbspec-python-check) and a Python owner running the seven fixture cases, which pass twice, and make feature-unit-check and node scripts/features/build.mjs --check pass. | [o] |
| T150 | Correct two stale generator statements. Cause: docs/operations/usage.md says --check exists in the Go, PHP and TypeScript orm-gen while Python's has it too, and docs/operations/packaging.md's configuration row names the schema path a connection option of Go, PHP and TypeScript only. | Acceptance: both statements (+ko) match the five clients, checked against the code. | Evidence: bin_orm_gen.py has --check, which prints missing or differs for its one models.py, and Db.connect_schema(dsn, schema) takes the generated schema; docs/operations/usage.md and docs/operations/packaging.md (+ko) now say so; make documents-check and npm run docs:rules-check pass. | [o] |
| T151 | Classify and own the files that CI's static group refused. Cause: run 38413640279 failed release-coverage (the editor packages classified nowhere in config/release.json) and feature-unit-check/owners (seven files that select no behaviour test: the set files, browser-entry.mjs, dbspec_names_test.php, syntax-vectors.mjs, install-cli.mjs). | Acceptance: the editor packages are notReleased until T128-R with that reason; each file is an input of the check that reads it; the generator gets a test that regenerating gives the committed bytes, and install-cli a check that installs into a temporary prefix. | Evidence: node scripts/kit/release.mjs coverage classifies every package file; make feature-unit-check/owners passes 19 cases; packages/orm-npm/tests/syntax-vectors.mjs fails on an edited syntax.json and passes on the committed one; make install-cli-check installs without a symbolic link, formats and checks a fixture, and uninstalls. | [o] |
| T152 | Release 0.0.10. Cause: the request hook (T125), the renderer names (T129, T129-2), the audited immutable tables (T130), the generator scan (T132), syntax analysis, formatting and declaration positions in every runtime (T128-3, T128-3p), the document set file (T128-2a) and the editor packages so far, which the hyper and consumer sessions wait for, are on main with green CI. | Acceptance: the version-bump commit of AGENTS.md "Releases" sets every manifest to 0.0.10 and moves Unreleased to 0.0.10, the consumer locks are written for v0.0.10 on that commit, its CI succeeds, and the maintainer pushes the tag v0.0.10. | Evidence: make version-check passes; the release workflow of the tag publishes the release. | [o] |
| T153 | Import key_text where the Python Collection uses it. Cause: Collection.get and Collection.fetched in packages/orm-python/src/polyspec/orm/model.py call key_text, which only put and has import, so they raise NameError (found by T147-1). | Acceptance: a unit test calls both, red first, and the module imports key_text once. | Evidence: before the change test_collection.py failed with NameError: name 'key_text' is not defined in Collection.get; f55fa60a moves key_text and scalar_key to polyspec/orm/keys.py, which model.py imports at its top without a cycle, and make python-unit-check passes 167 tests. | [o] |
| T154 | Remove the import fallback of the Python Mermaid test. Cause: packages/orm-python/tests/dbspec_mermaid.py falls back to a hard-coded sibling ordered-json path when its import fails, a fallback that turns a wrong environment into a plausible one (found by T149). | Acceptance: the test imports through the declared PYTHONPATH only and a missing package fails naming it, red first. | Evidence: before the change the test passed with ordered-json missing from PYTHONPATH, through its guessed sibling path; after it the same run fails with ModuleNotFoundError: No module named 'polyspec.ordered_json', and with the declared PYTHONPATH it runs its 13 cases. | [o] |
| T155 | Release 0.0.11. Cause: restore under the request hook (T140), which hyper's S-285 waits for, the component rule check with its rules (T147), the strict vector readers (T133), the Python schema coverage (T149) and fixes since 0.0.10 are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.11 and moves Unreleased to 0.0.11, records the dependency review of the changed locks and the consumer locks of v0.0.11; its CI succeeds; the annotated tag v0.0.11 is pushed and the release workflow publishes. | Evidence: make version-check and make dependency-policy-check pass on the commit. | [o] |
| T157 | Pin the merged flowmark in the markdown conformance. Cause: T140 pinned the flowmark commit 5e065f4 of a branch while flowmark T14 was open; flowmark merged T14 as 8b5d16a on main with orm v0.0.11. | Acceptance: tests/markdown/flowmark.version pins flowmark main and the dialects table mirrors it. | Evidence: the pin is v0.0.0-20261013184618-8b5d16a81141, node scripts/docs/markdown-table.mjs reports both tables mirror it, and make markdown-conformance-check passes the restore case in every client. | [o] |
| T158 | Return orm main CI to green after T134, T135 and T142. Cause: CI run 38462697441 on 85d84f2c failed feature_coverage, static, feature_verify_rust and conformance. | Acceptance: each sub-item's check passes locally. | — | [o] |
| T158-1 | Record the Go codec symbols of T142 (follows T158). Cause: the interface check fails with go: unexpected packages/orm-go/orm/codec.go::yamlCoreScalar and ::yamlPlainUntagged in feature_coverage (interface_contract/owner/go), conformance (interfaces/go) and feature_verify_rust (interface_contract/interface-self-test). | Acceptance: the Go records hold the two symbols and go run ./tests/interfaces/check -language go passes, red first. | — | [o] |
| T142-6 | Make interface-check install the PHP dependencies it runs (follows T142-5). Cause: in a fresh checkout interfaces/php and interfaces/php-extension fail with php declarations: exit status 255 until make install-php runs. | Acceptance: the target has the prerequisite, red first in a fresh worktree. | — | [o] |
| T158-2 | Follow T135's socket folder in the socket length test (follows T158). Cause: static repo-check fails "test-servers.sh refuses a socket path longer than the platform limit" with Cannot find module '/tmp/orm-sock-.../d/.../scripts/free-ports.mjs', since T135 moved the sockets out of the checkout and the test still lengthens the checkout path. | Acceptance: the test exceeds the limit through T135's declared socket place and make repo-check passes, red first. | — | [o] |
| T158-3 | Declare python as a need of codec-check (follows T158). Cause: T142-3 made codec-check run the Python vectors, but contracts/check-inputs.json gives codec-check no python need, so the static job fails with python-venv-check: .../.venv/bin/python is missing (run 38465371334). | Acceptance: the need is declared and the static job passes codec-check. | — | [o] |
| T158-4 | Name the fix in the dbspec-vscode integration argument error (follows T158). Cause: repo-check refuses packages/dbspec-vscode/tests/integration/run.mjs:22, whose missing --vscode message names no fix (run 38465371334). | Acceptance: the message names make dbspec-vscode-integration, and repo-check passes. | — | [o] |
| T159 | Pass the request's parameters to the request hook, read-only, in every client. Cause: the hyper scope hook must check a bound value and fail closed, while only PHP gives the hook the parameters ($r->params). | Acceptance: Node and Python call the hook with (request, addParam, params), Go and Rust read them through their params type, the parameters cannot be changed through them, with a case per client, red first, and the interfaces records and docs updated. | — | [o] |
| T160 | Release 0.0.12. Cause: the key-prefix methods (T156), the request hook parameters (T159), which hyper's S-285 waits for, the saphyr YAML codec in Rust (T139-1) and fixes since 0.0.11 are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.12 and moves Unreleased to 0.0.12, records the dependency review of the changed locks and the consumer locks of v0.0.12; its CI succeeds; the annotated tag v0.0.12 is pushed and the release workflow publishes. | Evidence: make version-check and make dependency-policy-check pass on the commit. | [o] |
| T161 | Declare the query methods of PHP generated models. Cause: PHP resolves column filters, orders and key-prefix chains through Model::__call, so nothing declares them and PHPStan at level 9 reports 372 method.notFound on hyper's modules (hyper S-286-7-1), while Go, Rust and TypeScript declare their methods from the schema (T156). | Acceptance: the PHP generator declares, as real typed methods, every query method the schema defines for a model (the methods of every key prefix, the column filters and the orders that docs/operations/usage.md states); a fixture model's query calls give PHPStan no method.notFound, red first; the interface records, the model checks and docs/operations/usage.md (+ko) are updated; the other clients are checked for the same gap and any gap is its own item. | — | [o] |
| T161-1 | Declare the query methods of Python generated models (follows T161). Cause: Python's Model.__getattr__ (model.py) resolves every chain at run time and nothing declares the methods, so a type checker sees untyped attributes, the gap T161 closed in PHP. | Acceptance: the Python generator declares the query methods the schema defines (the condition method of every column, the tuple method of every key prefix, the column methods), each typed to return the model, read through typing.get_type_hints on a fixture model, and each builds the query of its chain name, red first. A type checker reports no missing method on a Python model even before, because __getattr__ admits every name; the gap was the untyped result. | — | [o] |
| T161-2 | Return the 0.0.13 release commit's CI to green (follows T161). Cause: run 38509680967 failed: scripts/check/run.test.mjs did not list the new client-unit-check/model-methods part, and PHPStan left phpstan in the system temporary folder (feature-verify-other: 1 temporary entries). | Acceptance: the parts table names the part and the case keeps PHPStan's cache in its own folder. | — | [o] |
| T162 | Release 0.0.13. Cause: the PHP query methods (T161), which hyper takes together with T156 and T159 in one raise, are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.13 and moves Unreleased to 0.0.13, records the dependency review of the changed locks and the consumer locks of v0.0.13; its CI succeeds; the annotated tag v0.0.13 is pushed and the release workflow publishes. | Evidence: make version-check and make dependency-policy-check pass on the commit. | [o] |
| T163 | Load the documents of a generated schema value in Python and TypeScript. Cause: PHP (RuntimeModel::loadSet), Go (runtimemodel.LoadSet) and Rust (Manifest::load_set) return the parsed documents of a schema value, while Python and TypeScript export no such step, so consumer's Python scopes (S-285) would have to split the texts itself. | Acceptance: Python RuntimeModel.load_set and TypeScript loadSet return the documents of the manifest text, then the external ones marked external, and fail with SCHEMA_INVALID on a text without a header, red first, with the symbol records and docs/spec/schema.md (+ko) updated. | — | [o] |
| T164 | Release 0.0.14. Cause: the documents of a schema value in Python and TypeScript (T163), which consumer's S-285 Python scopes wait for, are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.14 and moves Unreleased to 0.0.14, records the dependency review of the changed locks and the consumer locks of v0.0.14; its CI succeeds; the annotated tag v0.0.14 is pushed and the release workflow publishes. | Evidence: make version-check and make dependency-policy-check pass on the commit. | [o] |
| T165 | Take kit 0.0.39, then 0.0.40 (follows T137); 0.0.40 carries the fix of the first mise.lock (kit 5aee092). Cause: T128-8-3 imports @polyspec/kit/install-vscode, which kit exports from 0.0.39; 0.0.39 also moves the toolchain to mise (K127), so config/toolchain.json's go, python, pythonBuild, uv, composer, cargoAudit, govulncheck and alint keys fail kit's schema and install-tools takes no tool names. | Acceptance: package.json pins kit 0.0.39, make kit-sync and make kit-check pass, the toolchain is declared as kit 0.0.39 requires, and a fresh checkout installs and passes interface-check, the model checks and codec-check. | — | [o] |
| T165-1 | Take kit 0.0.41 so the ARM runners install the tools (follows T165). Cause: run 38521800267 stopped every job at install-tools: kit 0.0.40's mise.lock pins no linux-arm64, and orm's CI runs on ubuntu-26.04-arm (No lockfile URL found ... on platform linux-arm64). | Acceptance: orm pins kit 0.0.41, mise.lock pins linux-arm64, and main's CI passes. | — | [o] |
| T165-2 | Return orm main CI to green after the mise toolchain (follows T165-1). Cause: run 38526082365 failed: npm 12, now first on PATH, writes npm pack --json as an object (package-declarations and package-check read an array) and refuses the URL dependency of install-cli (EALLOWREMOTE); scripts/repo/toolchains.mjs required actions/setup-go, which the Go of mise.toml replaces; the editor job ran no make install-go. | Acceptance: each reads or installs as npm 12 and mise require, and the checks pass. The Go distribution that mise installs inside the module root breaks go vet and go test (kit K135, reported to kit). | — | [o] |
| T165-3 | Take kit 0.0.42 for the Go module root (follows T165-2). Cause: run 38526082365 failed go-vet-check and go-test-check: mise installs Go into var/tools/mise/installs/go inside the module root, and ./... walked its test sources. | Acceptance: orm pins kit 0.0.42, whose bootstrap writes var/tools/go.mod, and go vet passes. | — | [o] |
| T165-4 | Declare the mise files as check inputs (follows T165-3). Cause: run 38532926973 failed feature-unit-check: mise.lock was an input of no check target. | Acceptance: kit-check takes mise.toml and mise.lock as inputs and the selection tests pass. | — | [o] |
| T166 | Parse a document set in every runtime, with each document's result. Cause: flowmark's catalog (its T12-3-1) and orm's own runtimemodel key the documents of a set by re-reading the dbspec 1 <document> header with string matching before parsing, because no runtime exports a parse of a set that returns each document's document, positions and diagnostics. | Acceptance: a standard parse of a document set in every runtime, with vectors, red first, and runtimemodel uses it; flowmark T12-3-1 takes it after its release. | — | [o] |
| T167 | Declare the generic types of the PHP client for PHPStan. Cause: PHPStan level 9 reports method.nonObject and mixed in hyper's modules, because Db::transaction declares no type for its callback's result and model rows carry no generic type (hyper S-286-7-1, after T161). | Acceptance: the PHP client declares @template generics for the transaction result and the rows of a model, a PHPStan level 9 fixture reports none of those findings, red first, with the interface records updated. | — | [o] |
| T168 | Ask mise for the Python of mise.toml. Cause: python-venv-check read the release out of mise.toml with a sed expression, a second TOML reader beside mise (found in the review of reinvented wheels). | Acceptance: the check takes mise current python, the python-venv tests pass and the venv check passes. | — | [o] |
| T169 | Keep the PHP plan cache from growing with each IN-list and insert length. Cause: hyper's product import measured Engine::$plans (256 entries per manifest) holding 8.6-35 MiB, because each IN-list length and each multi-row insert length is its own plan shape. | Acceptance: a plan that does not depend on those lengths, or a stated memory bound on the cache, measured red first, and the same judgement for every client's plan cache. | Evidence: contracts/fixtures/insert_parts.json passes in all five clients; the conformance vector creates_and_save records the parts (T169-1). | [o] |
| T169-1 | Record the insert parts in the conformance vectors (follows T169). Cause: T169 changed creates into parts 2 + 1, but tests/conformance kept one 3-row INSERT, so main CI failed in all five clients alike. | — | Evidence: check compare -vector creates_and_save reports 1 vector × 5 languages identical on mysql, postgres and sqlite. | [o] |
| T169-2 | Run a selected conformance vector through make and describe five clients (follows T169-1). Cause: tests/conformance/README.md still names four clients and four output files, and no make target passes -vector to check run, so a single vector needs hand-made databases and the make environment. | Acceptance: a make target runs, compares and records the named vectors with the test servers, and the README describes it and the five clients. | Evidence: VECTOR=creates_and_save reports 1 vector × 5 languages identical on the three databases; VECTOR with RECORD=1 exits 2 with its reason. | [o] |
| T169-3 | Keep the plan cache from growing with each IN-list length (follows T169). Cause: T169's closing evidence covers the insert parts only. The Planner binds one placeholder per element of an IN-list (packages/php/src/Planner.php:525, and the same in the other planners), so each IN-list length is its own plan and its own cache entry. Red: a test that runs the same IN-list request with lengths 1 to 300 and counts the plan cache entries; it grows by one per length on the current code. | Acceptance: the plan does not depend on the list length (a fixed number of placeholder groups with a stated fill), or a memory bound on the cache is stated and enforced, judged the same way for every client's plan cache. | Evidence: commits e2bcacbf (Go) and 9767c513 (PHP, Python, TypeScript, Rust) on main: the plan key bounds the IN list to a power-of-two bucket (padded by repeating the last value, parameters renumbered), so each client's plan cache holds one entry per bucket instead of one per list length. Red then green per client: Go TestPlanCacheBoundedByInListShape ('length 11: plan cache holds 11 entries, want at most 10' then PASS); PHP PlanCacheBoundTest (same red, then OK 1 test 600 assertions); Python test_plan_cache_bound (red then OK); TypeScript plan-cache-in (red '11 plan cache keys, want at most 10' then 3 of 3); Rust already bucketed (pad_in), regression test in_list_keys_are_bounded_by_bucket passes. documents-check, docs:rules-check and layout-check pass; interface-check and rust-fmt-check exit 2 on main as well (not this row's). | [o] |
| T169-4 | The plan cache tests of the five clients are declared. Cause: T169-3 landed a test in each client without declaring it, so make -s parity-declarations-check reports 5 R5 undeclared-test findings: go packages/go/orm/plan_cache_test.go, php packages/php/tests/PlanCacheBoundTest.php, typescript packages/ts/tests/plan-cache-in.mjs, python packages/python/tests/test_plan_cache_bound.py, rust packages/rust/orm/src/request.rs (found after T169-3). | Acceptance: each file is a unit of the internal profile of its language (reason and spec anchor docs/spec/parity.md#implementation-internal-tests), one binding each; R5 is 0. Red: the 5 R5 findings. Green: 0 R5. | Evidence: commits 07ebb236 and b4d24f9e on main (declarations, then the python test entry point via testcase; rebased on 866e074a): the five plan cache files are declared as units of their language's internal profile, and their R5 findings are gone. make -s parity-declarations-check no longer lists plan cache files; the R5 findings it still lists are other rows' collection identity files (T77-6) and rust testcase lib.rs (K204 false positive). The python plan cache test runs through testcase.main and passes with test_testcase. | [o] |
| T170 | Generate TypeScript models with only what they use (follows T137-1-3-1). Cause: fsm's lint-ts on kit 0.0.43 finds in the models.ts orm-gen writes unused imports, a class and an interface of the same name (declaration merging) and unused MANIFEST_* exports. | Acceptance: the TS emitter writes only the imports and exports a model uses and declares each type once, a generator test asserts it, and the lint of the generated file passes. | Evidence: the models-scan test that refuses unused imports and a class merged with an interface fails before the fix and passes after; ts-check/types and ts-model-check pass. MANIFEST_TEXT and MANIFEST_HASH stay exported as docs/operations/usage.md states. | [o] |
| T170-1 | Record the TypeScript symbols of the generated models (follows T170). Cause: T170 regenerated the models without recording contracts/symbols/typescript.json, so make interface-check failed on main. | — | Evidence: the record differs only in the generated models and declareMember; interface-check passes all 8. | [o] |
| T170-2 | Read the declared chain methods in the TypeScript generator tests (follows T170). Cause: generate.mjs and generate-scan.mjs read the merged interface that T170 removed, so the conformance and feature_verify_other groups failed on main. | — | Evidence: both tests pass and read the declare fields of each model class. | [o] |
| T171 | Release 0.0.15. Cause: the set parse per document (T166), the YAML core schema in every client (T142-1, T142-2, T142-9 to T142-11), the insert parts (T169) and the generated models without allows or unused imports (T137-1-3-1, T170), which flowmark, fsm and hyper wait for, are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.15 and moves Unreleased to 0.0.15, records the dependency review of the changed locks and the consumer locks of v0.0.15; its CI succeeds; the annotated tag v0.0.15 is pushed and the release workflow publishes. | Evidence: make version-check and make dependency-policy-check pass on the commit. | [o] |
| T171-1 | Fetch the Rust crates after the CI cache restore (follows T171). Cause: the CI of the 0.0.15 bump failed in every Rust group because Swatinem/rust-cache restored an older registry over the crates make install-rust had fetched, and rustls 0.23.46 was missing. | — | Evidence: install-rust is install-rust-toolchain before the restore and install-rust-crates after it; scripts/check and scripts/repo tests pass. | [o] |
| T172 | Declare the scanned chain methods in the generated PHP models. Cause: the PHP orm-gen has no --scan (gen --set --out --namespace [--check]), so a chain method that module code calls (ltSeq, andSlotNumber) is declared nowhere and PHPStan reports method.notFound on calls that run correctly; Go, TypeScript and Rust generate the methods their scan finds and Python reports unmatched ones, and docs/spec/dsl.md 2.2 leaves PHP alone without a scan (reported by hyper on 0.0.15, 8 calls). | Acceptance: the PHP orm-gen takes --scan, reads the sources with nikic/php-parser, declares each chain method found as a @method tag with exact parameter and return types on the model class, and rejects an unknown column, operator or argument count during generation as the other generators do; docs/spec/dsl.md (+ko) states it; generator vectors with accepting and rejecting cases, its parity unit, and a PHPStan run over a fixture with no method.notFound. | Evidence: OrmGenScanTest (23) and OrmGenScanParityTest (14) failed first with "flag provided but not defined: -scan" and pass; the PHPStan fixture at level 9 reports no method.notFound and an unscanned control call does; the symlink case failed (exit 0) before the RecursiveDirectoryIterator walk. | [o] |
| T172-1 | Record the review of nikic/php-parser (follows T172). Cause: T172 moved nikic/php-parser into the require of packages/php/composer.json without a dependency review, so dependency-policy-check of the CI group static failed on 65c758b5 (the locks changed after the review, nikic/php-parser has no entry). | Acceptance: make dependency-review RECORD=1 records 47 registry dependencies and 9 locks, and make dependency-policy-check passes. | Evidence: dependency-policy-check passes on the recorded review of 2026-10-14T18:06:24Z. | [o] |
| T172-2 | Make the PHPStan case of the PHP scan pass in CI (follows T172). Cause: on 65c758b5 the CI group feature_verify_other failed OrmGenScanTest::test_phpstan_finds_every_scanned_chain_method at "the control call is declared", while it passes locally; the case did not read the exit status of PHPStan, so a run that did not analyse gave no findings and passed the first assertion without analysing. | Acceptance: the case fails with the output of PHPStan on any status other than 0 or 1 and requires status 1 for the control; the cause in CI is found from that output and fixed, and feature_verify_other passes. | Evidence: with the exit status read, CI run 38615714975 showed the cause: on the runner PHPStan's raw format wrote control.php:6:Call to an undefined method ...::ltReadCount(). without the [identifier=...] suffix the case matched, so the control found no method.notFound. The case reads --error-format=json and matches messages[].identifier; it passes locally, and the CI of the commit that closes it runs it in static and feature_verify_other. | [o] |
| T172-3 | Declare tests/generator in the root tests layout (follows T172). Cause: T172 added tests/generator/php-scan-vectors.json, the vectors that the PHP and the node generator are compared with, in a folder that layout-root-tests (S7) does not declare, so make layout-check reports it. | Acceptance: config/layout.yml declares tests/generator with the shared vectors and its reason, and layout-check no longer reports it. | Evidence: make layout-check reports no S7 finding for tests/generator. | [o] |
| T173 | Release 0.0.16. Cause: the PHP orm-gen scan (T172, T172-1), which hyper waits for to clear the PHPStan findings of its dynamic chain methods, the PHPUnit runner of the PHP client with its development tools at the root (T148-1, T148-1-1) and the case ids of the shared vectors (T148-2, T148-2-2) are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.16 and moves Unreleased to 0.0.16, records the dependency review of the changed locks and the consumer locks of v0.0.16; its CI succeeds; the annotated tag v0.0.16 is pushed and the release workflow publishes. | Evidence: make version-check and make dependency-policy-check pass on the commit. | [o] |
| T173-1 | Make the release commit of 0.0.16 pass CI (follows T173). Cause: CI run 38615714975 of ae23528a failed: docs/features.md was not regenerated after T172 changed the description of model_generation; CoveragePerformanceGateTest still ran packages/php/vendor/bin/phpunit, which T148-1-1 removed, since it builds the path from the package directory; and the PHPStan case of T172-2. | Acceptance: the generated documents are current, the case runs vendor-php/bin/phpunit, and static, feature_coverage and feature_verify_other pass. | — | [o] |
| T174 | Declare the chain methods called in not, andNot and orNot callbacks in the TypeScript scan. Cause: the TypeScript scan resolved the first parameter of a callback given to and, or, on, fetchKey and fetchValue as a model of the receiver's class, but not that of not, andNot and orNot, which the PHP scan (T172) and the chain grammar (docs/spec/dsl.md) treat alike; so tests/conformance/runner_typescript calls orGtReadCount and ltReadCount that its generated models do not declare, and tsc refuses them. | Acceptance: the scan treats the callbacks of not, andNot and orNot as it treats and and or, with a generator scan case that fails before the change, and the committed models declare the methods the runner calls. | Evidence: generate-scan model_calls failed with "Author lacks gtServiceSeq, lbName, neIsDisplay" before the change and passes; npm run models adds ltReadCount and orGtReadCount to Author; make ts-model-check passes. | [o] |
| T174-1 | Record the TypeScript symbols of the scanned chain methods (follows T174). Cause: T174 regenerated packages/ts/src/models/models.ts with Author.ltReadCount and Author.orGtReadCount but did not record the TypeScript symbols, so interface-check (feature_verify_rust, interface_contract/interface-self-test) reported them as unexpected on 297b6afd. | Acceptance: contracts/symbols/typescript.json holds the two symbols and contracts/interfaces.json its new hash; go run ./tests/interfaces/check -language typescript passes. | Evidence: -record adds exactly the two symbols, and the check passes with the hash it computes. | [o] |
| T175 | Resolve the invocable model in the PHP scan (follows T172). Cause: the PHP orm-gen scan resolves $m = new C; $m->chain() but not (new C)($db)->chain(), the invocable-model idiom that hyper modules use about 130 times each: ScanVisitor has no case for a call on a new C expression nor on the result of __invoke, which returns the model, so --scan declared no chain method in member, page, board, canister, product and ordersheet (reported by hyper on 0.0.16). | Acceptance: a call on (new C), on (new C)(...) and on a chain of either resolves to C; a scan vector of tests/generator/php-scan-vectors.json and a PHPStan fixture use the idiom; the PHP scan and its parity with the node scan pass. | Evidence: orm-gen-scan failed 3 cases before the change (two invoked vectors, PHPStan method.notFound) and passes 27; orm-gen-scan-parity passes 16; interface-check -language php reports no symbol change. | [o] |
| T176 | Refuse SQLite older than 3.46 in the Python client. Cause: the Go (db.go), Rust (db.rs), PHP (Orm.php) and TypeScript (driver.ts) clients refuse a SQLite older than 3.46 with CAPABILITY_UNSUPPORTED when they connect, as docs/spec/dialects.md requires; the Python client has no such check, and Python links the system SQLite. | Acceptance: a RED case shows the Python client connecting to a SQLite reported older than 3.46; the client then refuses it with CAPABILITY_UNSUPPORTED and the message of the other clients, and accepts 3.46 and later. A shared vector is not added: each client reads the version of the library it links, which no case can swap in another client, so each check is its own comparison. | Evidence: test_sqlite_version failed with "OrmError not raised" for 3.45.3 before the change; it refuses 3.45.3, 3.9.0 and 2.8.17 and accepts 3.46.0, 3.50.4 and 4.0.0 after it, and python-unit-check passes. | [o] |
| T177 | Release 0.0.17. Cause: the PHP orm-gen scan of invoked models (T175), which hyper's modules wait for, the SQLite version check of the Python client (T176), ordered-json 0.0.9 in every client (T178), which consumer waits for, and the TypeScript infrastructure scripts (T137-1-4-5, T137-1-4-9) are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.17 and moves Unreleased to 0.0.17, records the dependency review of the changed locks and the consumer locks of v0.0.17; CI passes on that commit; the annotated tag v0.0.17 publishes the release assets. | Evidence: the release commit bcba7ddc passed CI (run 38648667367) after T177-1, T177-2 and T177-3; the annotated tag v0.0.17 ran the release workflow (run 38651315793), which published v0.0.17 with seven assets; the Python wheel requires polyspec-ordered-json 0.0.9 by its sha256. | [o] |
| T177-1 | Pass the package check of the 0.0.17 release commit (follows T177). Cause: CI of 739f8709 failed in package-check: the external crate that checks the Rust client as a path dependency has no lock, so offline cargo resolves the git dependency polyspec-ordered-json by its tag v0.0.8, which the restored git database of the runner did not hold; earlier runs passed only while a cache held the tag. | Acceptance: the external crate takes packages/rust/Cargo.lock, and the check passes with a cargo home that holds only what cargo fetch --locked of the client fetched. | Evidence: with a fresh CARGO_HOME after cargo fetch --locked, cargo check of the external crate failed with "failed to find tag v0.0.8" before the change; with the lock it passes, and make package-check passes with that cargo home. | [o] |
| T177-2 | Read the first lines of a started server on Linux (follows T177). Cause: CI of a7d4dac1 failed in repo-check: the startLogged case hung on Linux with nothing on stderr. startLogged read the size of the log after it spawned the server, and on Linux the server had already written its lines by then, so reading began past the ready line and no later write woke it; macOS passed only because its spawn is slower. | Acceptance: the read starts at the size of the log before the spawn. | Evidence: the CI log shows the case ending at its 20 s limit with an empty stderr, which means no line was read; with the size taken before the spawn the case passes 8 runs of 8 locally. | [o] |
| T177-3 | Judge a stopped process by its state in the server tests (follows T177). Cause: CI of e8dac3c8 failed in test-servers-check: on Linux, stopPid returned on the exit event while the ended fake server was a zombie that its adopting process had not reaped yet, and the case took kill(pid, 0), which succeeds on a zombie, for a running process; macOS passed only because the old shell step gave the reaper time. | Acceptance: the stopPid case and the binary-log fixture judge an ended process by ps: gone, or state Z. | Evidence: the CI record names "quick server still runs after stopPid" 17 ms after the exit event; with the state check test-servers.test passes 9 locally, the stopPid case 3 runs of 3. | [o] |
| T178 | Take ordered-json 0.0.9 in every client. Cause: ordered-json 0.0.9 carries the Python parser (T1.41) that consumer needs for its import speed, and orm pins ordered-json 0.0.8 exactly in PHP (the zip URL), TypeScript and the Python wheel's Requires-Dist, which a consumer cannot override (reported by kit and consumer). | Acceptance: the PHP, TypeScript, Python, Rust and Go clients require 0.0.9 (Go through its new module path github.com/polyspec/ordered-json/packages/go, Python through the wheel URL with its sha256); the locks and the dependency review are recorded; the codec and JSON checks of each client pass. | Evidence: the wheel's sha256 is bc4d26a9…f01d as downloaded; go test of the JSON, codec and AES cases passes against the test servers, the PHP hostcodec unit passes 61, the Rust codec tests pass 8, python-unit-check passes and the TypeScript codec vectors pass; dependency-policy-check passes after the review. | [o] |
| T179 | Emit checked reads from the TypeScript orm-gen backend (reported by kit for fsm). Cause: the generated TypeScript read entities with entities.get(..)! and columns with column(..) as T, which the lint of kit 0.0.47 rejects in every consumer's generated models. | Acceptance: the generated code looks an entity up through a guard that throws naming it, checks each column value by typeof/instanceof and throws a TypeError naming the column, and reads an ordered_json column through Core.jsonColumn, a checked accessor of the TypeScript model runtime (Core is internal in contracts/interfaces.json); the generated signatures do not change, and a test rejects any unchecked read in generated output. | Evidence: the new uncheckedReads check of packages/ts/tests/generate.mjs failed with 90 unchecked reads on the old generator and passes; styled-value/json-column accepts an ordered-json value and an SQL NULL and rejects a plain object with a TypeError naming author.json_setting; the regenerated models pass oxlint and models-scan, and no exported model-class key of contracts/symbols/typescript.json changed. | [o] |
| T180 | Release 0.0.18. Cause: the checked reads of orm-gen's TypeScript output (T179), which fsm waits for to pass the lint of kit 0.0.47, the Go module at packages/go (T137-1-5, T137-1-6), the layout of kit (T137-1-7, T137-1-8, T137-1-9), the TypeScript infrastructure readers and checkers (T137-1-4-2, T137-1-4-3, T137-1-4-6) and kit 0.0.47 with its lint (T137-1-4-7, T137-1-4-7-1) are on main. | Acceptance: the version-bump commit sets every manifest to 0.0.18 and moves Unreleased to 0.0.18, records the dependency review and the consumer locks of v0.0.18; CI passes on that commit; the annotated tags v0.0.18 and packages/go/v0.0.18 publish the release. | Evidence: CI of the release commit 4e304aac passed (run 38665460029) after T180-1 and T180-2; the annotated tags v0.0.18 and packages/go/v0.0.18 ran the release workflow (runs 38670294931, 38670294798), which published v0.0.18 with seven assets and the Go module github.com/polyspec/orm/packages/go v0.0.18. | [o] |
| T180-1 | Keep test tools and build records out of the release (follows T180). Cause: make release-assets refused the Python wheel because it declared the test validator jsonschema-rs as an extra, a marker the release does not accept and no consumer needs; the composite build of the TypeScript client had committed its incremental record packages/ts/tsconfig.build.tsbuildinfo; go.work.sum lacked the sums of the tests module. | Acceptance: the test validator is a PEP 735 dependency group that make install-python installs, each TypeScript build keeps its record beside its own output and removes it, go.work.sum is current, and release-assets passes. | Evidence: release-assets failed with "Requires-Dist jsonschema-rs==0.58.6; extra == "test" is a marker" before the change and passes; make install-python installs jsonschema-rs from the group; a typescript-build leaves no tsbuildinfo in the tree; dependency-policy-check passes; kit takes the review of dependency groups as K177. | [o] |
| T180-2 | Generate the checked read of the Unicode table (follows T180). Cause: CI of the release commit failed in go-test-check and feature-verify-other: the lint pass of T137-1-4-7-1 had edited the generated packages/ts/src/dbspec/unicode_word.ts by hand, so it no longer equalled the output of go run ./tests/dbspec/unicode -write. | Acceptance: the generator emits the checked read through present, the file is generated again, and the generated TypeScript of models.ts equals its generator (ts-model-check). | Evidence: the CI record names TestCommittedTablesAreGenerated (unicode_word.ts differs from the table of Unicode 17.0.0); after the change go test ./tests/dbspec/unicode passes, typescript-build and ts-model-check pass, and CI of 4e304aac passes. | [o] |
| T181 | Take kit 0.0.48 to 0.0.50. Cause: kit 0.0.48 (K169, K171: node through mise.toml and every recipe tool through kit's variables or an absolute path), 0.0.49 (K172 to K177) and 0.0.50 (K178 to K185) change the copies, the Makefile rules and the dependency review; kit advises taking 0.0.50 with them at once. | Acceptance: make kit-sync takes the latest of them, every recipe starts its tools as K171 requires, and kit-check, lint-ts, layout-check and dependency-policy-check pass. Cause of the bypass: 0.0.50 is not released. Retry: when kit 0.0.50 is released. | Evidence: make kit-sync takes kit 0.0.50 (kit-check passes): node 26.12.0 is a tool of mise.toml and .node-version is gone, every recipe starts its tools through $(NODE), $(NPM), $(GO), $(COMPOSER) and $(PYTHON3) (kit-check found 84 bare names, now 0), knip declares its binaries, ciExempt names feature-owner-check with its reason, the php-ext composer.json is covered by the dependency policy with its own lock, and ruff 0.17.0 and vulture 2.16 are tools of mise.toml, so make lint-python runs and passes after its 125 ruff, 135 format and 658 vulture findings were fixed without a disable; lint-ts, layout-check, dependency-policy-check, release-coverage, python-unit-check, scripts/repo and scripts/check pass. | [o] |
| T181-1 | Commit the lock sidecar of vulture (follows T181). Cause: mise.lock names the uv lock of pypi:vulture by its path .mise/locks/pypi-vulture/2.16 and its sha256, and the T181 work deleted that folder, so mise install --locked on a fresh checkout failed: "dependency sidecar … .mise/locks/pypi-vulture/2.16: No such file or directory" (reported by kit). | Acceptance: the sidecar is tracked with the digest mise.lock names, and install-tools succeeds on a fresh worktree. | Evidence: on a fresh worktree of origin/main make install-tools failed with that message; make toolchain-lock wrote the sidecar and its digest into mise.lock, install-tools then succeeds, and kit-check, layout-check, dependency-policy-check and release-coverage pass. | [o] |
| T181-2 | Name the Python connection type of dbspec in public (follows T181). Cause: T181 replaced the undefined name Connection of the Python introspect_dbspec and apply signatures by a private Protocol _Connection, so a public signature named a private type and make interface-check failed on main (python/Dbspec.introspect, apply, recover: want Connection, got _Connection). | Acceptance: the Protocol is the public Connection of polyspec.orm.dbspec.introspect, the signatures name it, the Python symbols record it, and interface-check passes. | Evidence: interface-check failed with the want/got lines above; with Connection exported and its two symbols recorded (and the python snapshot hash the checker computes), interface-check, lint-python, python-unit-check and the repository check pass. | [o] |
| T181-3 | Declare the owner of the mise lock sidecars (follows T181-1). Cause: T181-1 tracked .mise/locks/pypi-vulture/2.16/uv.lock without an owner, so feature-unit-check/owners failed on main ("every tracked file selects a behaviour test or declares its scope"). | Acceptance: kit-check, which verifies mise.lock, owns .mise/locks/**, and the owners check passes. | Evidence: the owners check listed that path as its only finding; with .mise/locks/** among the kit-check inputs of contracts/check-inputs.json it passes, as do kit-check, layout-check and the repository check. | [o] |
| T181-4 | Pin push-gate to the setup-node release that reads mise.toml (follows T181). Cause: T181 moved node into mise.toml and set node-version-file: mise.toml, but push-gate.yml pins actions/setup-node to 82076278, an earlier v7 commit that reads no mise.toml, so every push-gate run on main since T181 failed ("Unable to find Node version '[tools]'"). | Acceptance: push-gate pins 949feb24 (v7.1.0), the commit kit's release.yml pins, and push-gate passes on main. | Evidence: the push-gate runs of main from 2a934dc6 to a2232a24 failed at setup-node with that message, while docs-pages, on the v7 tag that resolves to 949feb24, read mise.toml and passed; push-gate.yml now pins 949feb24. | [o] |
| T182 | Implement the complete PHP client protocol in the native extension: validation, encoding, planning, execution and the shared runtime. | Acceptance: every shared planner, bind and MySQL/PostgreSQL/SQLite conformance vector passes with the other five clients, and parity includes php-extension in every query unit. The C extension implements the PHP client protocol using the same class names; Composer loads PHP implementations only when native classes are absent. Native installation is optional and preferred where available. Use libfyaml for YAML 1.2, yyjson raw numbers for ordered JSON, and the PHP-linked OpenSSL and zlib libraries. | make parity-declarations-check. Native planner/bind evidence: var/evidence/final-integrated-20261026/continuation.log records CoveragePlannerExtensionTest 84 cases/745 assertions and InsertPartsTest 6 cases/12 assertions with PHP product fallback excluded; retry-feature_coverage.log records planner/owner/php-extension runs 1 and 2 passing. The select/update/insert bind-type cases explicitly pass. T182-2 through T182-6 cover IR validation, codecs, planner, executor and runtime; T182-6 receipts cover all 59 conformance vectors across six clients and three databases, with the failed audit_lazy vector repaired separately and interfaces/results tail passing. Current parity declarations hold. Evidence index: var/evidence/final-integrated-20261026/t1826-acceptance-evidence-index.json. | [o] |
| T182-0 | Register the PHP client's class names in the extension (follows T182). Cause: the extension registers Polyspec\Orm\Dbspec\Native and Polyspec\Orm\Native, which PHP code must name to choose it, while the decision of T182 makes user code the same with or without the extension. | Acceptance: the extension registers the PHP client's names (Polyspec\Orm\Dbspec\Dbspec, Polyspec\Orm\Code and the rest); a process with the extension loads none of the client's PHP files for those classes, and a process without it loads them; the declaration tests compare the extension with the client under the same names, each run in its own process; the Native namespaces are removed with no alias; docs/spec/dbspec.md (+ko) states the rule. | make dbspec-php-extension-check/declarations | [o] |
| T182-0-1 | Every check of the extension is a case of the PHP client's tests (follows T182-0). Cause: the extension's tests (packages/php-ext/tests) compare the extension with the PHP client in one process (DbspecTest: hashes, render, manifest diagnostics, bytes, arguments, objects, documents, introspect and apply; DbspecSetTest, DbspecShowCreateTest, DbspecAnalyzeTest, DbspecNamesTest, DbspecSetsTest, DbspecSetParseTest, CoverageSchemaDefinitionTest), which the same class names make impossible. | Acceptance: each such comparison is a case of the matching packages/php/tests file with its expectation (a shared vector, or the value or exception the PHP client gives today, written into the test), so the same file checks either implementation in its own process; the PHP client passes every new case. | Evidence: the comparisons that the shared vectors already fix are the client tests the extension will run (DbspecTest, DbspecManifestTest, DbspecRenderTest, DbspecIntrospectTest, DbspecPlanApplyTest, DbspecApplyTest, DbspecAnalyzeTest, DbspecSetTest, DbspecSetParseTest, the sets helper, CoverageSchemaDefinitionTest). The rest are 176 cases of packages/php/tests/dbspec_behaviour.php (bytes, arguments, manifest, objects, documents, introspect failures, apply arguments, SetResult, Names and 112 SQLite apply scripts), whose outcomes packages/php/testing/dbspec/behaviour.php records from the PHP library into packages/php/tests/fixtures/dbspec-behaviour.json, deterministic across two records and with no machine path; DbspecBehaviourTest compares them (177 tests, 353 assertions). The extension's show-create introspection test, which checks the public introspect against tests/dbspec/show-create.json only, moved to packages/php/tests/DbspecShowCreateIntrospectTest.php (9 tests). Recording found the PHP library's unchecked element types, fixed first by T182-0-1-1. make -s dbspec-php-check and make -s feature-unit-check pass. | [o] |
| T182-0-1-1 | The PHP library checks the element types of a document set and its object graph (follows T182-0-1). Cause: the extension refuses a document set or a document graph whose arrays hold another type (%s holds %s, not %s in src/convert.c, at the document set and 24 properties such as Table::$columns, Document::$uses and Plan::$renameTables), while the PHP library relies on PHP's property types, which do not type array elements: Dbspec::manifest(['text']) warns Attempt to read property "name" on string and goes on (found recording T182-0-1). | Acceptance: the PHP library raises the same TypeError, in the same traversal order and with the same message, at every entry point that takes documents or plans; each checked property has a rejecting case and the valid documents of the vectors still pass. | Evidence: Polyspec\Orm\Dbspec\Graph (@internal) checks in the extension's order, called by emit, render, manifest, externalDifferences, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps, exportMermaid and the apply commands (after the dialect and error-mode checks, as the extension). RED: packages/php/tests/DbspecGraphTest.php on main's library: 50 tests, 49 failures, 12 warnings, 2 deprecations. GREEN: 50 tests, 841 assertions (37 document properties through 10 entry points, 11 list and plan cases, the first element, a valid document); make -s dbspec-php-check passes with the new graph target; make -s feature-unit-check exits 0. | [o] |
| T182-0-1-2 | Bind the PHP tests that T182-0-1 and T182-0-1-1 added (follows T182-0-1). Cause: 88cdbe1a and 69115256 added DbspecGraphTest, DbspecBehaviourTest, DbspecShowCreateIntrospectTest and their case file without parity bindings, and their commits did not run the parity declarations check, so it failed on main. | Acceptance: each test method is a php-internal unit, the case file is a php support file, and the check holds. | Evidence: RED: make -s parity-declarations-check on main exited 1 with four R5 undeclared-test findings (packages/php/tests/dbspec_behaviour.php, DbspecBehaviourTest.php, DbspecGraphTest.php, DbspecShowCreateIntrospectTest.php). GREEN: seven php-internal units and the support entry; the check prints "every declaration holds". | [o] |
| T182-0-1-3 | Record the PHP symbols of Graph (follows T182-0-1-1). Cause: 88cdbe1a added packages/php/src/Dbspec/Graph.php without recording contracts/symbols/php.json, and its commit ran the dbspec tests but not the interface check, so make interface-check failed on main with php: unexpected ...Graph for the class and its ten methods. | Acceptance: the snapshot records Graph, its six exported symbols are nativeOnly entries with their reason (R11), and the checks hold. | Evidence: RED: make -s interface-check failed on main, not ok - interfaces/php with eleven unexpected Graph symbols. GREEN: -record -language php adds them (44 lines) and the symbol hash; make -s interface-check exits 0; the six nativeOnly entries are declared and make -s parity-declarations-check holds. | [o] |
| T182-0-1-4 | Read plans.json through its one reader (follows T182-0-1). Cause: DbspecGraphTest.php (T182-0-1-1) and dbspec_behaviour.php (T182-0-1) opened tests/dbspec/plans.json themselves, while DbspecPlanReaderTest requires that only dbspec_plan_cases.php opens it. | Acceptance: both read it through plan_vectors() and the reader test passes. | Evidence: RED: the unit php-internal-dbspec-plan-reader failed in the parity run, plans.json must be opened by one file only: DbspecGraphTest.php, dbspec_behaviour.php, dbspec_plan_cases.php. GREEN: both take plan_vectors(); DbspecPlanReaderTest passes, and DbspecGraphTest (50 tests) and DbspecBehaviourTest (177 tests) still pass. | [o] |
| T182-0-2 | The extension takes the client's names and runs the client's tests (follows T182-0-1). | Acceptance: the stubs and src/*.c register Polyspec\Orm\Dbspec* and Polyspec\Orm\Code; the extension's parity bindings and make targets run the packages/php/tests files with the extension loaded; packages/php-ext/tests keeps only the extension's own checks (declarations, package, required modules); the Native namespaces, the mirror check and the name rewriting of declarations.php are removed; the interface records, contracts/interfaces.json and docs/spec/dbspec.md (+ko) name the client classes. | make dbspec-php-extension-check/declarations, make dbspec-php-extension-check/behaviour | [o] |
| T182-0-2-1 | Reject the column limit document within a test's memory (follows T182-0-2). Cause: DbspecRulesTest limit/columns (121 tables of 1000 columns) peaks at 98 MB over the process baseline in the PHP library and 109 MB in the extension, about 0.9 KB per parsed column, before the 120001st column trips the limit; run with the extension under PHPUnit's 128 MB limit it ends the process with Allowed memory size of 134217728 bytes exhausted (found running the client's tests with the extension, T182-0-2). | Acceptance: the memory of a parsed column is measured in both implementations and its largest share removed, so the limit cases peak well under the limit in both; a unit test asserts the peak of the limit/columns case below a stated bound. Measured (2026-10-18): the largest legal document (120000 columns) peaks at 100.6 MB in the PHP library and 196.6 MB in the extension; 120000 columns is a document the limits allow, so the limit check has no defect. The PHP library's cost is about 650 bytes per column with its position; the extension holds its C model and the PHP object graph at once, twice that. Branch feat/ext-names-T182-0-2 holds the memory requirement (256M in phpunit.xml.dist) and DbspecRulesTest::test_largest_document, which bounds the peak under 160 MB: the PHP library passes it and the extension fails it. | Commands: make dbspec-php-extension-check/declarations | [o] |
| T182-0-2-1-1 | Cut the PHP library per-column memory so the limit/columns case peaks under its derived bound (follows T182-0-2-1). | packages/php/src: no fresh token buffer per line; generated-name source text formatted only for an error. packages/php/tests/DbspecRulesTest.php: testLimitColumnsPeak (derived bound, library mode). | make dbspec-php-check/rules | [o] |
| T182-0-2-2 | Names::column states its own argument error (follows T182-0-2). Cause: a column list with a non-string element is a TypeError from the internal call Names::render(): Argument #2 ($suffix) must be of type string, int given in the PHP library, which names a parameter the caller never passed, while the extension says Names::column(): Argument #3 ($columns) must contain only strings, int given. | Acceptance: the PHP library raises the extension's message, the behaviour case names/column/non-string-element is recorded again, and both implementations pass it. | Evidence: RED: the behaviour test with the renamed extension failed only on names/column/non-string-element, the PHP library recording Names::render(): Argument #2 ($suffix) must be of type string, int given. GREEN: Names::column raises Polyspec\Orm\Dbspec\Names::column(): Argument #3 ($columns) must contain only strings, int given; the case is recorded again (one line changed); DbspecBehaviourTest passes 177 tests in the PHP library and with the renamed extension build; make -s dbspec-php-check passes. | [o] |
| T182-0-3 | The conformance check runs php-extension as the PHP runner with the extension loaded, so every conformance vector runs in the extension (follows T182-0-2). Cause: config/parity.json declares php-extension an implementation, and tests/conformance/check/main.mts runOne has no case for it, so every conformance run, RECORD=1 included, stops with unsupported language php-extension | tests/conformance/check, the Makefile conformance targets | node --test tests/conformance/check/main.test.mts | [o] |
| T182-1 | The error catalog in the extension (follows T182). | Acceptance: the extension registers the codes of errors.yaml as Polyspec\Orm\Native\Code from a stub generated by orm-gen, as the other clients do, with the constants of the PHP client's Code. The error-code api units of the client profile count it when T182-6 adds the extension to that profile, since a profile requires every unit of its implementations. | Evidence: orm-gen errors --lang php-extension writes packages/php-ext/stubs/code.stub.php; packages/php-ext/scripts/arginfo.php makes src/code_arginfo.h from it with gen_stub.php; MINIT registers the class; declarations.php compares it with Polyspec\Orm\Code under the native naming rule (Native as the last namespace segment). RED: make -s dbspec-php-extension-check/declarations failed in test_client with Polyspec\Orm\Native\Code and its 40 constants (not declared). GREEN: the arginfo check and the declarations target pass, 5 tests twice; the catalog drift loop gives the same files for go, php, php-extension and rust; the interface check records the class (contracts/symbols/php-extension.json); make -s feature-unit-check exits 0. | [o] |
| T182-1-1 | Copy every extension stub in the interface specification test (follows T182-1). Cause: T182-1 added packages/php-ext/stubs/code.stub.php and recorded its class in contracts/symbols/php-extension.json, but the case the extension declarations are checked against the contract of tests/interfaces/check/specification.test.mts copies only orm_dbspec.stub.php into its temporary root, so the recorded snapshot names a class the copy lacks; T182-1 ran the interface check of php-extension, not this test. | Acceptance: the case copies every stub of the extension's root and passes. | Evidence: RED: on main the case fails with the recorded stub and snapshot are refused. GREEN: with every other stub copied as it is, specification.test.mts passes. | [o] |
| T182-2 | The codec in the extension (follows T182-1). | Acceptance: encode and decode of every logical type, AES and the decode vectors; the codec, codec-aes and codec-decode suites run in it under their ids. | Commands: make dbspec-php-extension-check/declarations | [o] |
| T182-2-1 | The extension implements the codec of every logical type, and the codec suite (tests/codec/vectors.json) runs in it under its ids (follows T182-2). Cause: packages/php-ext has no codec, so the PHP library tests run its Codec in PHP even with the extension loaded | packages/php-ext/src, the stub, config/parity.json | make dbspec-php-extension-check/codec, make dbspec-php-extension-check/declarations | [o] |
| T182-2-2 | The extension decodes the host styles, and the codec-decode suite (tests/codec/decode-vectors.json) runs in it under its ids (follows T182-2-1) | packages/php-ext/src, the stub, config/parity.json | make dbspec-php-extension-check/decode | [o] |
| T182-2-3 | The extension implements AES and the blind index, and the codec-aes suite (tests/codec/aes-vectors.json) runs in it under its ids (follows T182-2-2) | packages/php-ext/src, the stub, config/parity.json | make dbspec-php-extension-check/aes, make dbspec-php-extension-check/hostcodec | [o] |
| T182-2-4 | Make the extension fail loudly on a silent path (follows T182-2). Cause: validator.c fail returns false with no exception when OrmException cannot be found, so Validator::restoreKey and conflictKey return null silently; db.c scall returns false without an exception when a call is not callable, and record_inserted ignores that result, a silent discard | packages/php-ext/src/validator.c, packages/php-ext/src/db.c, packages/php-ext/src/config.m4, packages/php-ext/tests/silent-paths | make dbspec-php-extension-check/silent-paths | [o] |
| T182-3 | The IR validator in the extension (follows T182-1). | Acceptance: tests/ir/validate.json runs in it under its case names with the codes the other clients give. | make dbspec-php-extension-check/ir, make dbspec-php-extension-check/declarations | [o] |
| T182-4 | The planner in the extension (follows T182-3). | Acceptance: planner, insert-parts and ir-answers run in it under their ids. | make dbspec-php-extension-check/planner, make dbspec-php-extension-check/declarations | [o] |
| T182-5 | The executor in the extension over PHP's own database layer (follows T182-2, T182-4). | Acceptance: statements run through the database API PHP already provides, called from C, not a new driver; statement-events, statement-fault, sqlite-paths and mysql-session run in it. | Commands: make dbspec-php-extension-check/declarations | [o] |
| T182-5-1 | The extension implements the connection and statement path of Polyspec\Orm\Db in C over PDO: connecting through Orm::connect, select, scalarOf, writeOf, paginate, args, exec, fetch and the statement events, so sqlite-paths and mysql-session run in it (follows T182-5) | packages/php-ext/src, the stub, config/parity.json | make dbspec-php-extension-check/executor | [o] |
| T182-5-2 | The extension implements the transactions and savepoints of Polyspec\Orm\Db in C: begin, finish, savepoint, cleanup, audit and the fault injection, so statement-events, whose cases run in transactions, and statement-fault run in it (follows T182-5-1) | packages/php-ext/src, the stub, config/parity.json | make dbspec-php-extension-check/executor | [o] |
| T182-5-3 | The extension implements the request hook, Utils and Stats of Polyspec\Orm\Db in C, so every public member of Db is the extension's (follows T182-5-2) | packages/php-ext/src, the stub, config/parity.json | make dbspec-php-extension-check/executor | [o] |
| T182-5-3-1 | The comments of the extension Db state what it implements (follows T182-5-3). Cause: packages/php-ext/src/db.c:8-10 and polyspec_orm.stub.php:896-898 still say the transaction, savepoint, audit, execute, Utils and Stats methods throw INTERNAL until T182-5-2 and T182-5-3, which implemented them | packages/php-ext/src/db.c, packages/php-ext/src/polyspec_orm.stub.php | make text-absent FILE=packages/php-ext/src/db.c TEXT="and Utils and Stats to T182-5-3", make text-absent FILE=packages/php-ext/src/polyspec_orm.stub.php TEXT="and Utils and Stats to T182-5-3" | [o] |
| T182-5-4 | Unsubscribing in the extension frees the function of its unsubscribe closure (follows T182-5). Cause: each subscribe() of the extension allocates one persistent function struct for the unsubscribe closure and never frees it, so a long-lived process that subscribes and unsubscribes grows without bound | packages/php-ext/src/db.c, a leak test | make dbspec-php-extension-check/executor | [o] |
| T182-5-5 | With the extension, transactions and statement timeouts behave as in the PHP library (follows T182-5). Cause: with the extension loaded, ModelTest test_transactions fails with CONFIG "row locks are allowed only inside a transaction" (packages/php-ext/src/db.c:1265), test_statement_timeout fails on MySQL and PostgreSQL, and test_statement_timeout_through_a_pooler never returns, so a lock wait or statement timeout is not honoured on that path; the executor check does not run ModelTest | packages/php-ext/src/db.c, the dbspec-php-extension-check/executor target with the ModelTest cases | make dbspec-php-extension-check/executor | [o] |
| T182-6 | The query, model and collection client in the extension (follows T182-5; T199 is this item's surface). | Acceptance: the generated models run on the extension; conformance on MySQL, PostgreSQL and SQLite, identity-load, aggregate-numeric, decimal-model, styled-column-states, authenticated-encryption and install-changed-database run in it; the client profile lists php-extension, and parity counts it in every unit. | make model-native-php-extension-check, make dbspec-php-extension-check/declarations, make conformance-check (php-extension output must match on MySQL, PostgreSQL and SQLite), and the extension-mode owner tests of identity-load, aggregate-numeric, decimal-model, styled-column-states, authenticated-encryption and install-changed-database. Completed on main: model-native original 106 passing cases plus the four repaired child cases (35 assertions); declaration cases 8/24 and 3/8; seven remaining library cases/47 assertions; all 39 named native fixture cases; aggregate/authentication six-client receipts; three-database 59-vector comparison with only audit_lazy retried; interfaces/results tail exit 0; current parity declarations hold. Evidence: var/evidence/final-integrated-20261026/t1826-acceptance-evidence-index.json, conformance-after-fixes-20261027.log, audit-lazy-comparison/, interfaces-results-tail-20261027-final.log and parity-declarations-after-native-api-audit.log. Source fixes: f76c5a87d, 8a91e2697 and 379a177a4. The initial aggregate run exited 2; its failed vector and previously unreached result-shape phase were verified separately, without replaying passing vectors. | [o] |
| T182-6-12 | The PHP extension owns Collection in C and returns a native Iterator through IteratorAggregate, preserving typed key identity, ordered replacement, deferred snapshot traversal, clone behavior and value callbacks (follows T182-6). | packages/php/src/Collection.php, packages/php-ext/src/collection.c, packages/php-ext/src/collection.h, packages/php-ext/tests/CollectionTest.php | make collection-php-extension-check builds with pinned PHP 8.4, runs the native Collection PHPUnit cases with the extension loaded and the PHP source identity/coercion cases in library mode. It does not complete T182-6 or cross-database parity. | [o] |
| T182-6-13 | The PHP extension owns native C implementations of Request, StyledValue, Page, Func and PendingTime that preserve the PHP source API and behavior (follows T182-6). Acceptance: each class is registered once from the generated stub; focused native and source unit tests cover the complete public surface, request canonicalization, PHP weak scalar rules and null/error behavior. | packages/php-ext/src/model_types.c, packages/php-ext/src/model_types.h, packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/src/polyspec_orm_arginfo.h, packages/php-ext/src/polyspec_orm.c, packages/php-ext/tests/ModelTypesSurfaceTest.php, config.m4, Makefile, contracts/check-inputs.json, contracts/symbols/php-extension.json, contracts/interfaces.json | make model-types-php-extension-check | [o] |
| T182-6-14 | The PHP extension owns the complete runtime Model surface and required native helper classes in C (follows T182-6-13). Acceptance: all Model methods, properties, flags and defaults match the PHP source; each Model and helper class registers once through generated arginfo; generated subclasses retain late static meta() dispatch; focused native tests cover query, read, write, rollback, Fiber isolation, GC, serialization, clone, typed keys, column states and weak null behavior. Verified on committed implementation 24a1e7e: make item-test ID=T182-6-14 passes on PHP 8.4.26 with 47 native tests (654 assertions), 3 declaration tests (8 assertions), and 10 PHP source comparison tests (68 assertions). | packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/src/polyspec_orm_arginfo.h, packages/php-ext/src/polyspec_orm.c, packages/php-ext/src/model_state.c, packages/php-ext/src/model_state.h, packages/php-ext/src/model_chain.c, packages/php-ext/src/model_chain.h, packages/php-ext/src/model_native_methods.c, packages/php-ext/src/model_native_methods.h, packages/php-ext/src/model_output.c, packages/php-ext/src/model_output.h, packages/php-ext/src/model_query.c, packages/php-ext/src/model_query.h, packages/php-ext/src/model_dsl.c, packages/php-ext/src/model_dsl.h, packages/php-ext/src/model_read.c, packages/php-ext/src/model_read.h, packages/php-ext/src/model_write.c, packages/php-ext/src/model_write.h, packages/php-ext/src/model_assembly.c, packages/php-ext/src/model_assembly.h, packages/php-ext/src/runtime_model.c, packages/php-ext/src/runtime_model.h, packages/php-ext/src/registry_native.c, packages/php-ext/src/registry_native.h, packages/php-ext/tests/ModelNativeTest.php, packages/php-ext/tests/ModelDslNativeTest.php, packages/php-ext/tests/ModelReadNativeTest.php, packages/php-ext/tests/ModelWriteSurfaceTest.php, packages/php-ext/tests/ModelAssemblyTest.php, packages/php-ext/tests/RuntimeModelTest.php, packages/php-ext/tests/ChainNativeSurfaceTest.php, packages/php-ext/tests/RegistrySchemaNativeTest.php, config.m4, Makefile, contracts/check-inputs.json, contracts/symbols/php-extension.json, contracts/interfaces.json | make model-native-php-extension-check builds the extension and runs the focused Model and helper PHPUnit tests without a database. | [o] |
| T182-6-15 | Generate PHP extension arginfo offline in a temporary build copy and map the implemented native Stats class (follows T182-6-14) | packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/scripts/build.sh, packages/php-ext/tests/ArginfoBuildTest.php, packages/php-ext/src/polyspec_orm.c, packages/php-ext/tests/DeclarationsTest.php, contracts/interfaces.json, .gitignore, config/generated.json, contracts/check-inputs.json, Makefile | make native-arginfo-php-extension-check; make dbspec-php-extension-check/declarations | [o] |
| T182-6-16 | The PHP extension binds the LIMIT and OFFSET values of a native read as integers on MySQL, as the PHP client does (follows T182-6). Cause: CoverageModelQueriesExtensionTest on MySQL fails with SQLSTATE[42000] 1064 near ''0', '3'' for limit(0, 3), and the MySQL conformance child of CoverageConformanceVerificationExtensionTest fails with the same error, so the native read sends the limit as quoted strings | packages/php/src/Db.php (exec), packages/php-ext/src/db.c (exec_stmt) | node scripts/features/coverage.mts --feature model_queries --part owner/php --database mysql; node scripts/features/coverage.mts --feature model_queries --part owner/php-extension --database mysql, both in the run environment of make run-databases (BENCH_* DSNs and POLYSPEC_ORM_EXTENSION) | [o] |
| T182-6-17 | The relations and explicit root-page conformance vectors match across Go, PHP, PHP extension, Rust, TypeScript and Python on MySQL, PostgreSQL and SQLite (follows T182-6). Cause: the recorded explicit root-page SQL and bind expectations predated T236 binding offset_p and count_p. | tests/conformance/vectors.json, tests/conformance/vectors.postgres.json, tests/conformance/vectors.sqlite.json | Selected record and recheck: tests/conformance/check/main.mts run with conditions_connectors, conditions_group, conditions_leading_group, conditions_leading_prefix, terminal_by, terminal_reuse, expression_forms, joins, relations, aggregates, empty_reads, get_query and relations_flatten; MySQL, PostgreSQL and SQLite, two matching runs each. All 13 vectors matched across six clients on each database. Evidence: var/parallel/resume-landing/relations-root-page-conformance-record.log (SHA-256 3faeba3aaa6d32cab85a743eba767240ef3fcb0cd1dc46dce4a12c87aee351ff). The enclosing command later exited 2 in the separate PHP-extension interface-symbol snapshot check; that result is not part of this selected conformance result. | [o] |
| T182-6-18 | OrmException::fromDriver of the PHP extension leaves the Zend heap intact (follows T182-6). Cause: it released the rv zval of zend_read_property for errorInfo, which the read never writes for a declared property, so an -O2 build released an uninitialized stack zval; the php-extension owners of model_writes, statement_events, audit_triggers and request_hook ended with zend_mm_heap corrupted on MySQL, PostgreSQL and SQLite | packages/php-ext/src/orm_exception.c, packages/php-ext/tests/OrmExceptionNativeTest.php | make native-api-equivalence-check NATIVE_API_EQUIVALENCE_FILTER=testDriverFactoryLeavesTheHeapIntactAcrossRepeatedDriverErrors | [o] |
| T182-6-18-1 | Registry::model of the PHP extension leaves the Zend heap intact for a manifest hash with no registered model class (follows T182-6-18). Cause: when no class is registered for the hash, registry_native.c builds the model through loadSet without writing args[2], then releases args[0..3], so it releases an uninitialized stack zval; Registry::generated followed by Registry::model on the fixtures of contracts/fixtures ends the process with SIGBUS | packages/php-ext/src/registry_native.c, packages/php-ext/tests/RegistrySchemaNativeTest.php | make model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testModelOfGeneratedTextKeepsTheHeapIntact | [o] |
| T182-6-19 | An extension run of the PHP extension tests loads no class of the PHP library from packages/php/src (follows T182-6). Cause: the php-ext targets run PHPUnit without the --without-library packages/php of kit run-tests, so the root Composer autoloader maps Polyspec\Orm\ to packages/php/src, and a native model run loads Assemble, Config, DbState, Dbspec\Meet, Dialect, Engine, Generator, OrmGen, ParamCheck and TxFrame as userland classes; S10: "A PHP extension implements every PHP library of its repository and registers their classes and functions under the same names and signatures". This item turns --without-library on and lands after its sub-items -1 to -5 | Makefile (PHP_EXTENSION_TESTS and the php-ext PHPUnit targets), packages/php-ext/tests | Evidence: main 8c28c87b3; make model-native-php-extension-check passed on PHP 8.4.26: 97 native tests / 1,348 assertions, 3 declaration tests / 8 assertions, and 5 PHP library tests / 48 assertions; the --without-library packages/php guard passed. The fake Flowmark protocol path is covered; no real Flowmark server or MySQL/PostgreSQL/SQLite feature matrix was run. | [o] |
| T182-6-19-1 | The PHP extension implements Engine (the plan cache) and Assemble in C under their names and signatures (follows T182-6-19). Cause: db.c calls Polyspec\Orm\Engine::for and Engine::plan and Assemble::index by name, which resolve to packages/php/src/Engine.php and Assemble.php | packages/php-ext/src (engine and assemble), packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/tests | make model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testEngineAndAssembleAreNative | [o] |
| T182-6-19-2 | The PHP extension implements Config, Dialect, ParamCheck, TxFrame and DbState in C under their names and signatures (follows T182-6-19). Cause: a native model run loads them from packages/php/src | packages/php-ext/src, packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/tests | make model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testConfigDialectAndFramesAreNative | [o] |
| T182-6-19-3 | The PHP extension implements Dbspec\Meet in C under its name and signature (follows T182-6-19). Cause: a native model run loads it from packages/php/src/Dbspec/Meet.php | packages/php-ext/src, packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/tests | make model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testDbspecMeetIsNative | [o] |
| T182-6-19-4 | orm-gen (bin/orm-gen with OrmGen, Generator, Scan, ScanVisitor, ChainDeclare and UsageError, which serve only it) is its own Composer package that requires the library, and the extension test harness uses models that this CLI generated ahead instead of calling Generator (follows T182-6-19). Ruling (coordinator, 2026-10-25) from S10: "The PHP library is library code only: an application on a userland framework, such as a PHP console application, is its own Composer package that requires the library"; no library API outside the CLI calls Generator, so the extension does not provide these classes | packages/php (bin/orm-gen and its classes moved to their own package), composer.json, config/release.json, packages/php-ext/tests, packages/php/tests | make model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testGenerationToolingIsNotLoaded | [o] |
| T182-6-19-5 | The PHP extension implements the other public library classes that its stub lacks: Bytes, Transform, Decimal, Go, GoMap, Subscription and Notification (follows T182-6-19). Cause: they are not @internal in packages/php/src and polyspec_orm.stub.php declares none of them; db.c uses Bytes by name | packages/php-ext/src, packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/tests | make model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testPublicValueClassesAreNative | [o] |
| T182-6-20 | make model-native-php-extension-check passes on main (follows T182-6). Cause: on 4b8db7551 it fails with 88 passed, 1 failed, 1 timed out: ModelWriteSurfaceTest::testNativeMarkdownWriteReturnsFirstKeyOrNullWhenNoKeysWereAnswered throws "CONFIG: a markdown DSN selects the markdown IR executor and has no PDO DSN; read it with parseMarkdownDsn" from MarkdownModel\connect (packages/php/markdownmodel/schema_declarations.php:107), and the PHPUnit process does not end within 30 s after its last case, RegistrySchemaNativeTest::testPerManifestRuntimeCacheAndInvalidationAfterRegister | the markdown connect path of the extension or of the generated markdown model, the process exit of the native model run, packages/php-ext/tests/ModelWriteSurfaceTest.php | make model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testNativeMarkdownWriteReturnsFirstKeyOrNullWhenNoKeysWereAnswered | [o] |
| T183 | Remove the temporary root of the MySQL migration fixture when its setup fails. Cause: fixture() in scripts/test-servers-mysql.test.mts made /tmp/orm-mig-XXXX, copied the script, checked its text and ran mysql-start before it returned close(), so a failure in those steps left the root and any mysqld it started; nine such folders (about 1.7 GB) were found under /private/tmp. | Acceptance: when the setup fails, the fixture runs its own close() (stops the servers, removes the socket directory and the root) and rethrows the original error. | Evidence: the case "the MySQL fixture removes its root when its setup fails" makes the copied mysql-start exit 3; RED: before the fix it failed with true !== false on existsSync(root); after the fix make test-servers-check passes 15 of 15 and no /tmp/orm-mig-* folder remains. | [o] |
| T184 | Take kit 0.0.53. Cause: every other polyspec repository moved to kit 0.0.53 (kit K131); it brings K188 (no knip hint), K189 (allow-remote for the review), K190 and K195 (sibling wheels), K191 (owner-check selects tests by imports), K192 (a tracked mise sidecar) and K193 (.php-version with PHP, which orm has). | Acceptance: package.json and package-lock.json resolve 0.0.53 with the published integrity, make kit-sync writes its copies, and the checks pass. | Evidence: the lock resolves v0.0.53 with sha512-b5N/8uNp…EQ==; kit-sync wrote scripts/kit/kit-bootstrap.mk, .github/workflows/release.yml and config/layout.kit.yml; dependency-review RECORD=1 recorded 65 registry dependencies and 10 locks; kit-check, dependency-policy-check, release-coverage, toolchain-check, layout-check, interface-check, go-model-check, go-vet-check, the Go engine tests, lint-python, lint-ts, client-unit-check and rust-fmt-check pass. | [o] |
| T185 | Bound a coverage process by the longest declared test class (follows T184). Cause: scripts/features/coverage.mts gave every native test process of a coverage run the DATABASE class (120000 ms), while a test bounds its own work by the class it declares; the Go model_generation case declares testcase.Process (300000 ms) and runs orm-gen from a cold build, so ci run 38715909724 stopped it after 120000 ms inside its own deadline. | Acceptance: the outer deadline NATIVE_DEADLINE is not shorter than PROCESS, with a case that fails while it is. | Evidence: the new case of scripts/features/coverage.test.mts failed with "NATIVE_DEADLINE 120000 ms is shorter than PROCESS 300000 ms" while NATIVE_DEADLINE was DATABASE, and passes with PROCESS; make feature-unit-check/coverage passes. | [o] |
| T186 | Point to the local links of 0.0.x development from the development rules. Cause: kit K198 records in kit docs/plans/local-links.md that a development checkout links its installed polyspec packages to their source while the polyspec versions are 0.0.x, and AGENTS.md did not name it. | Acceptance: AGENTS.md and AGENTS.ko.md name that document in one sentence at the shared tools rule, and make documents-check passes. | Evidence: AGENTS.md and AGENTS.ko.md point to kit docs/plans/local-links.md for the 0.0.x local links; the documents check, the docs rules, the repository and features checks, owners, layout, go-model, interface, go-vet and the Go engine tests pass on a worktree installed from the locks, unlinked. | [o] |
| T187 | make typescript-build builds every TypeScript workspace package it needs, so make lint-ts passes on a fresh checkout. Cause: protocol-defect fix does not apply; the build defect: typescript:build (package.json) builds only packages/ts, so packages/dbspec-lsp, which imports @polyspec/dbspec-language, cannot resolve it until a later workspace build; a fresh install then fails make lint-ts with exit 2 (reproduced in the T53 worktree after make install-packages and make typescript-build). | Acceptance: after install-packages and typescript-build alone, make lint-ts exits 0; the typescript:build script builds dbspec-language before dbspec-lsp, and a check fails on a missing dependency build first (red first). | Evidence: commit 3297593b (main, from 2310bc71): RED: after typescript-build alone, make lint-ts exits 2 (oxlint 300 problems on the error-typed @polyspec/dbspec-language import; after project references, 157 problems in tests/interfaces/typescript.mts); repo check exits 1 for the unwrapped typescript:build line. GREEN: make typescript-build exits 0 and make lint-ts exits 0 with the install prerequisite; tsBuildInfoFile sits in the ignored dist, so git status shows no .tsbuildinfo. Checks on main: documents-check, docs:rules-check and repo check exit 0. | [o] |
| T187-1 | lint-ts builds the workspace packages it reads first (follows T187). Cause: the lint-ts target does not depend on typescript-build, so on a fresh install it reads the unbuilt @polyspec/dbspec-language and reports thousands of error-typed problems (found on a fresh worktree). | — | Evidence: RED on a fresh install without the build: make -s lint-ts exits 2 with 2376 error-type findings (packages/dbspec-lsp/src/results.ts); GREEN after lint-ts: typescript-build in the Makefile: make -s lint-ts exits 0 on the same install. | [o] |
| T188 | make check and CI run the PHP extension's dbspec suites. Cause: wiring defect: the umbrella target dbspec-php-extension-check (which runs the sets and schema-definition suites of packages/php-ext) is in neither CHECK_TARGETS nor any CI check group, so make check and CI never reach those suites (found in T111). Red: make -n check and the CI workflow's check groups do not include the target (or kit's check that every bound suite is reached, if it reports this). | Acceptance: the target is in CHECK_TARGETS and in the CI group where the other dbspec suites run, and make -n check shows it; the red is recorded before the wiring. | Evidence: commit 511fd905 (main; fix 68b05434 before the rebase): dbspec-php-extension-check is in CHECK_TARGETS and in the dbspec CI group. RED (unlinked, recorded): make -n check-run has no match and the group lacks the target; GREEN: make -n check-run shows it once. Linked: make -s dbspec-php-extension-check exits 0 (the sets and schema-definition suites pass). The dbspec group gains about a minute of build and test time; a group budget check is an owner question. | [o] |
| T189 | The generated model setters take the text form for date, time and datetime in every language. Cause: T89-3 refuses native time values at the bind layer, but the generated setters still type their parameters as native values (PHP DateTimeInterface, Python str | datetime.date, the Go generated code), so a caller meets the refusal only at bind time. | Acceptance: each generator emits the text type for date, time and datetime setters in all five languages, with no backward compatibility; red first: a generated setter's type accepts a native value (a check that compiles or type-checks a native argument must fail before the fix); the bind vectors stay identical. | Evidence: commit a5b2edc9 on main (from 9fc03812 before the rebase onto 0a1c643c): the generated setters take text for date, time and datetime in Go, PHP, Python, TypeScript and Rust. RED (linked): Go TestGeneratedFieldTypes (SetDay/SetStamp took time.Time), Python test_model_fields (str | datetime.date), PHP ModelMethodsTest::test_a_time_setter_takes_the_text_form (DateTimeInterface), TypeScript typecheck (TS2578 unused @ts-expect-error), Rust build/tests/generate.rs (set_start_dt not text). GREEN: Go generator test, Python 1325 OK, PHP model method tests OK, TS typecheck exit 0, Rust build tests 24+3 OK. Go: the error is CODEC_ENCODE naming the column through the existing failErr mechanism (reused); time_text_test.go proves Create, Update and GetQuery return it on sqlite, MySQL and PostgreSQL. On main after the rebase: documents-check, docs:rules-check, interface-check, go-model-check, go-vet-check, go-fmt-check, go engine test, feature-unit-check/owners, layout-check and bind-check exit 0. | [o] |
| T190 | The Rust ir-answers check compiles on main: the ir-answers-check/rust target fails with E0308 at packages/rust/tests/tests/markdown_conformance.rs:374, where creates takes Vec<Tasks> but the call passes Vec<Result>. Red: make -s ir-answers-check/rust exits 2 with E0308. | Acceptance: the call matches the creates signature, the test compiles, and ir-answers passes in Rust. | Evidence: commit (main, see git log): the markdown conformance test packages/rust/tests/tests/markdown_conformance.rs built the Tasks rows with setters that return Result and passed Vec<Result<Tasks>> to creates. RED: make -s ir-answers-check/rust exits 2 with E0308 (mismatched types) at line 374. GREEN: the rows take ? on their last setter, so creates gets Vec<Tasks>, and make -s ir-answers-check/rust exits 0; rust-fmt-check and the other pre-commit checks pass. | [o] |
| T191 | The schema_definition manifest hash matches its fixture on main. Cause: the Go constant ManifestHash in packages/go/model/orm.go gives sha256:a26e0390c07bd97f6907d6d4588636778e3073086eceb3cd7bcf7e693f875e30, while contracts/fixtures/schema_definition.json wants sha256:6b243f5910037ab61aca570f15bcb9c94461a80690ec6ed9546fecaf51e5af29; the feature check's schema_definition runs (go, php, python, typescript, rust) fail on the same mismatch (found in T118-5-1's baseline, reproduced on clean main 3417edde). | Acceptance: one manifest hash agrees across the constant and the fixture in every implementation, and the owner run passes. Red: node tests/go-test.mts -v -timeout 0 -count=1 -tags featurecoverage ./packages/go/orm -run '^TestCoverageDbspecManifestHash$' exits 1 with manifest hash = sha256:a26e0390…, want sha256:6b243f59… at packages/go/orm/coverage_schema_definition_test.go:87. Green: the same command exits 0. | Evidence: commit 2ed87e31 on main (rebased from 8a432915): the fixture was the stale side. bench.dbs hashes to a26e0390c07bd97f6907d6d4588636778e3073086eceb3cd7bcf7e693f875e30 (shasum -a 256 contracts/fixtures/bench/bench.dbs), the Go constant ManifestText hashes to the same value, and d788f2cd (T117) moved the constant, the generated models and the other implementations to it without updating the fixture. Red: node tests/go-test.mts -count=1 -tags featurecoverage ./packages/go/orm -run '^TestCoverageDbspecManifestHash$' exits 1 with manifest hash = sha256:a26e…, want sha256:6b24…. Green: the same command exits 0 (ok github.com/polyspec/orm/packages/go/orm). The schema_definition owner runs for go, php, python, typescript and rust pass after the fix (per the worktree feature-coverage run). The T191 worktree removal and the server cleanup are noted in the worktree. | [o] |
| T192 | The coverage checker's own tests pass on main. Cause: three tests of scripts/features/coverage.test.mts fail on clean main 8269f6f4, with no change of T118-5-1 involved: selecting coverage parts requires only those parts (the count in the test is 15 against 12 from the manifest, a stale expected count), coverage runs without cargo proceed while the Rust test build runs (30 s timeout in the Rust build), and Go, PHP, and Rust native cases execute through their owning files (the Rust case), each reported as … failed by tests/testcase.mts:231 (found in T118-5-1). | Acceptance: node --test scripts/features/coverage.test.mts passes all of its tests on main. Red: var/tools/bin/node --test scripts/features/coverage.test.mts exits 1 with those three failing tests. Green: exit 0. | Evidence: commit 8b8e6b84 on main (fix d32e69d9, rebased): the three failing tests were the test side. selecting coverage parts requires only those parts expected 4 owners, but statement_events has five owners times three databases (15), so the count is corrected to 5 times the databases; the two Rust/PHP cases ran without the LEASE and CARGO_LEASES that make provides, so the test supplies them with a forwarding stub. RED: var/tools/bin/node --test scripts/features/coverage.test.mts 18 pass, 3 fail, exit 1. GREEN: 21 pass, 0 fail, exit 0 (in the worktree after make install-tools and make install-packages). coverage.mts is unchanged. | [o] |
| T193 | Every tracked Rust file selects a behaviour test or declares its scope. Cause: make -s feature-unit-check/owners fails on main 93b12379 and on the T118-3 branch: packages/rust/tests/tests/integration/main.rs is tracked but is neither selected by a feature unit nor declared in a scope (found in T118-2 landing, reproduced on main). | Acceptance: the owners check passes on main. Red: make -s feature-unit-check/owners exits 2 with that path in the 'actual' list. Green: exit 0. | Evidence: commit of contracts/check-inputs.json declares packages/rust/tests/tests/integration/main.rs as a suite path (the crate root that generates the models and declares the integration modules). Red: make -s feature-unit-check/owners exited 2 with that path in the actual list. Green: exit 0. | [o] |
| T194 | make -s rust-fmt-check passes on main. Cause: packages/rust/tests/tests/integration/main.rs lists its mod lines out of rustfmt order (found in the T118-3 landing, reproduced on main 02d36df9); T118-2 added the file. | Acceptance: cargo fmt --check of the Rust workspaces passes on main. Red: make -s rust-fmt-check exits 2 with this output: "Diff in packages/rust/tests/tests/integration/main.rs:4" (the mod result_chains; line moves after mod coverage_transactions;). Exact output recorded at the time of filing: the diff reproduces the mod reorder in that file only. Green: exit 0. | Evidence: commit 54f850ae on main (fix 9cd4f1e4, rebased): the integration mod result_chains; line moves after mod coverage_transactions;, the rustfmt order. RED: make -s rust-fmt-check exit 2 with Diff in packages/rust/tests/tests/integration/main.rs:4 and :13. GREEN: exit 0 (rust, rust/bench and rust/interfaces pass). Compilation is unaffected (reordering only); documents-check and docs:rules-check pass. | [o] |
| T195 | The extension's DbspecApplyTest matches the PHP client's copy. Cause: packages/php-ext/tests/DbspecApplyTest.php differs from packages/php/tests/DbspecApplyTest.php after its header (apply_dsns reads getenv there, while the PHP client uses requiredDsn), so DeclarationsTest::test_mirrors fails (found in T118-6). | Acceptance: the two files have one test body, and DeclarationsTest::test_mirrors passes. Red: the PHPUnit run of packages/php-ext/tests/DeclarationsTest.php fails test_mirrors. Green: it passes. | Evidence: commit fd094074 on main (the extension apply test copy matches the PHP client's body, requiredDsn in its bootstrap): DeclarationsTest::test_mirrors red 'Failures: 1', then green 'OK (1 test, 15 assertions)'; the whole DeclarationsTest.php is 'OK (5 tests, 27 assertions)'. DbspecApplyTest needs servers and is verified at the final batch. documents-check, docs:rules-check and layout-check pass. | [o] |
| T196 | A set's external path may pass through a symbolic link to a directory, as an installed location is linked to its checkout. Cause: docs/spec/dbspec.md step 3 forbids any symbolic link among the examined prefixes of a path (and set.symlink in the table), so a linked checkout cannot be the location of an external document, although the link premise (development links each installed location to its checkout) requires it. | Acceptance: the set file, its root and every document file stay non-symlinks; a directory symlink is allowed only as an intermediate component of an external path below the root; a final component that is a link stays refused; the identity and duplicate rule (step 4) judges the opened file. The spec steps 3 and 6 and the set.symlink text change in EN and KO; the shared vectors tests/dbspec/set-files.json gain an accepting case (an external through a symlinked directory) and keep every rejecting case; the five set readers (Go, PHP, TypeScript, Python, Rust) and the PHP extension if it reads sets are changed to match. Red: the new vector fails in every reader. Green: the set-files vectors pass in each reader (its own unit tests only). | Evidence: commit bb93eac4 on main (fix d1d9c3d0, rebased and amended with the KO source stamp): a directory symlink is followed only as an intermediate component of an external path below the root; the set file, root, document files, final component and documents prefixes stay refused. Vector tests/dbspec/set-files.json gains the accepting case external-through-linked-directory. Red then green, each reader's own set-files unit test: Go TestReadSetVectors ok; PHP DbspecSetTest 72 OK; PHP extension 75 OK; TypeScript dbspec-set 94/94; dbspec-language resolve-set 55/55; Python 62 OK; Rust dbspec_set 62 passed. The declarations check shows R10, R6 and R4 at 0. documents-check, docs:rules-check and layout-check pass. Windows branches of set.c and set_read/set_file are not compiled here. | [o] |
| T198 | The three feature checks that own the language-set change pass on main. Cause: on main 37a095d2 (unchanged on the T77-10-1 branch), make -s feature-unit-check/coverage exits 2 with six not ok - sample/owner/php/none run 1 lines, make -s feature-unit-check/docs exits 2 with features: generated document is stale: docs/features.md, and node scripts/features/check.mts exits 1 with twelve … belongs to no feature errors (packages/go/orm/collection_identity_test.go, packages/go/orm/plan_cache_test.go, packages/php/tests/CollectionIdentityTest.php, packages/php/tests/PlanCacheBoundTest.php, packages/rust/orm/tests/collection_identity.rs, packages/rust/tests/tests/integration/main.rs, packages/ts/tests/code-catalog.mjs, packages/ts/tests/collection_identity.mjs, packages/ts/tests/plan-cache-in.mjs, packages/python/tests/test_code_catalog.py, packages/python/tests/test_collection_identity.py, packages/python/tests/test_plan_cache_bound.py). | Acceptance: each of the three exits 0 on main. Red: the three outputs above. Green: exit 0 for each. | Evidence: node scripts/features/check.mts exits 0 (19 contracts, no belongs-to-no-feature), make feature-unit-check/docs exits 0, and make feature-unit-check/coverage exits 0; the three test files are listed under schema_definition in contracts/features.json. | [o] |
| T198-1 | The ten Go dialect physical test files moved by T77-9-1 (packages/go/internal/dialects/*_physical_test.go and probe_test.go) are bound by a feature (follows T198). Cause: node scripts/features/check.mts on main reports features: packages/go/internal/dialects/<file>: go test file belongs to no feature for each of the ten files. | Acceptance: each file is selected by a feature's owner test, and the static check exits 0. Red: the ten messages above. Green: exit 0. | Evidence: the ten files are listed in the tests of the feature schema_definition in contracts/features.json. RED: node scripts/features/check.mts exit 1 with the ten belongs to no feature lines. GREEN: exit 0 with features: 19 feature contracts passed. The owner unit test of the owners selection passes after T198-2. make documents-check exit 0. | [o] |
| T198-2 | Every tracked file selects a behaviour test or declares its scope (follows T198-1). Cause: node --test scripts/features/owners.test.mts fails every tracked file selects a behaviour test or declares its scope on main and lists ten files that no verification command, coverage part, helper or make target of contracts selects: packages/go/bench/coverage_performance_gate_test.go, packages/go/internal/dialects/paths.go, packages/go/orm/collection_identity_test.go, packages/go/orm/plan_cache_test.go, packages/php/tests/CollectionIdentityTest.php, packages/php/tests/PlanCacheBoundTest.php, packages/rust/orm/tests/collection_identity.rs, packages/ts/tests/collection_identity.mjs, packages/ts/tests/plan-cache-in.mjs and tests/collection/keys.json. | Acceptance: that test passes and node scripts/features/check.mts exits 0. Red: the failure that lists these ten files, exit 1. Green: exit 0. | Evidence: each file is an input of a verification command that runs it: collection-identity-go, -php, -rust and -typescript in model_queries (tests/collection/keys.json is an input of each), plan-cache-go, -php and -typescript in planner, and dialects-probe-go in schema_definition (paths.go is used by probe_test.go). The Go coverage check is the go owner of performance_gate with the parity unit go-internal-coverage-performance-check-hot-path-check. RED: node --test scripts/features/owners.test.mts scripts/features/manifest.test.mts exit 1, 22 pass and 1 fail, listing the ten files. GREEN: exit 0, 23 pass and 0 fail. node scripts/features/check.mts exit 0 (19 feature contracts). make -s feature-unit-check/docs exit 0 after make feature-docs. The new commands are not run here. | [o] |
| T198-3 | make -s feature-unit-check/docs passes on main. Cause: docs/features.md and docs/features.ko.md were not regenerated from contracts/features.json. | Acceptance: make -s feature-unit-check/docs exits 0. Red: exit 2 with features: generated document is stale: docs/features.md. Green: exit 0. | Evidence: make feature-docs regenerated docs/features.md (one line) and docs/features.ko.md (two lines). RED: make -s feature-unit-check/docs exit 2 with features: generated document is stale: docs/features.md and make: *** [feature-unit-check/docs] Error 1. GREEN: exit 0. documents-check exit 0 (44 document pairs, 1 checklist, 1 changelog). | [o] |
| T156 | Declare a typed method for every prefix of every key in every generator. Cause: only the methods a scanned source calls were generated, so a key prefix that no scanned source called had no method (hyper U-97). | Acceptance: Go, Rust and TypeScript models declare a method for every prefix of the primary key, unique keys and indexes, Python resolves the same names at run time, with the symbol records and docs/operations/usage.md updated. | — | [o] |
| T156-1 | Return orm main CI to green after T156 (follows T156). Cause: run 38502539388 failed: the Python key-prefix test changed PYTHONPATH and ran the removed packages/orm-python/bin/orm-gen, the Go and Rust key-prefix tests started no testcase, generator/gen_go_key_prefix_test.go belonged to no feature, and src/models/markdown_fixture lacked the key-prefix methods. | Acceptance: each is fixed and its owning check passes. | — | [o] |
| T156-2 | Regenerate the Go markdown model (follows T156). Cause: run 38504701356 failed in go-markdown-model-generated: packages/orm-go/markdownmodel lacked the key-prefix methods. | Acceptance: go generate leaves the folder unchanged. | — | [o] |
| T69 | Release 0.0.9. Cause: T61, T62 and the markdown IR executors T62-1 and T63 to T67 add the protocol section, the markdown storage settings and the executors that polyspec/flowmark requires; flowmark requires a released tag instead of a pseudo-version. | Acceptance: the release procedure of the previous release items (T52, T56) for 0.0.9, ending with the tag. Requires T62-1, T63, T64, T65, T66, T67. | Evidence: the annotated tag v0.0.9 is on ddcdca4f and the GitHub release v0.0.9 carries its 4 assets; flowmark took released tags from then on (it now pins v0.0.18). The item was released without being closed. | [o] |
| T77-58 | The parity-check step leaves its own run directory: PARITY_RUN_DIR (.runtime/run/parity-check-$PPID) is created by parity-prepare and removed by no step, and kit's parity.mjs is the only owner that could remove it on exit. Cause: step.mts:166-168 reports leftover TMPDIR entries and does not remove them, so the directory cannot sit under TMPDIR. | Acceptance: a clean parity-check run reports no left directory. Red: the step reports left .runtime/run/parity-check-NNN. Green: it does not. Kit row required. | Evidence: clean run scratchpad/g4-parity.log (84 bindings ran twice, 85 cases passed, every unit holds in every implementation); its parity-check step PASS with no left directory; before the fix the same step failed on left .runtime/run/parity-check-86307. | [o] |
| T200 | restoreOrCreate: restore a soft-deleted row by its key, or create it (consumer S-286-19-1). Cause: soft delete keeps a row's key values, so a new create of the same key fails with DUPLICATE_KEY, and the app must look the key up, restore or create; the existing upsert (duplication()->create()) takes exactly one conflict key, overwrites a live row and leaves a soft-deleted one deleted. | Acceptance: docs/operations/usage.md (+ko) specifies the call in the five clients (php, ts, python, go, rust): on a table with soft_delete, in one transaction, a soft-deleted row that holds the key picked as restore() picks it is restored with the set values, audit included, and returned; a key that no row holds is created with the set values and returned; a live row that holds it fails with the new catalog code ROW_NOT_DELETED and nothing is written. A conformance vector restore_or_create on MySQL, PostgreSQL and SQLite over a bench table with a non-identity unique key fixes the statements and results of the three cases; red before the implementations, green after; the interface rule, symbols and parity unit of the call. Decision (user, 2026-10-18): built now. | Evidence: red with the implementations stashed (var/t200-red3.log): Go RestoreOrCreate undefined, Rust SoftLink has no method restore_or_create, Python CONFIG: Restore is not a column of soft_link; TS and PHP were not reached, since the run stops at the Rust build. Green (var/t200-green.log, make run-databases TARGETS=conformance-check VECTOR=restore_or_create): go, php, typescript, python and rust ok on MySQL, PostgreSQL and SQLite, and the Python case that runs every vector passes it on all three. On SQLite the vector alone also sends the connection's first orm__row_lock table statement, which an earlier vector sends in a full run; its expectation leaves that statement out (tests/conformance/README.md). Owner checks: interface-check, parity-declarations-check, feature-unit-check, python-model-check, the Go generator test and the Rust build tests (the one database case needs its DSN). | [o] |
| T200-1 | The tests that pin the bench set follow T200's new table. Cause: T200 added soft_link to bench.dbs, which changed bench's entity list, count and manifest hash, and T200 ran none of the tests that pin them. | Acceptance: every test and fixture that pins them reads the new values. | Evidence: the PHP RuntimeModelTest and TS engine.mjs entity lists add soft_link; Python test_orm_gen counts 20 entities; the hash sha256:18b7c53c… replaces sha256:a26e0390… in contracts/fixtures/schema_definition.json, docs/spec/protocol.md (+ko), test_orm_gen.py and the Rust soft_delete.rs and sqlite_concurrency.rs. The bench-reading tests pass: Python, the TS files, the PHP files that need no seeded bench database, and Go model. | [o] |
| T201 | The audit triggers record the audited tables of a transaction (consumer S-286-19). Cause: the audit record (seq, uuid, service_seq, account_seq, service_member_seq, action, reason, created_at, request_id) stores no table; only each history table's audit column points back to it, so a page cannot filter or page audits by table. | Acceptance: docs (+ko) specify, in the five clients with the same semantics, that the audit into triggers also write one row per audited table that the transaction changed (the audit seq and the audited table name), in a table of the audit set; it is readable through the ordinary generated models of the audit set as a relation from audit, so a query filters and pages by table with ltSeq, limit and getCount; no matching row behaves as any read does (an empty result or NO_ROWS). Vectors red then green on MySQL, PostgreSQL and SQLite; the triggers' statements, the schema and the generated models of every runtime. Decision (user, 2026-10-18): built now, in parallel with T200. | Evidence: the clause tables <table> (<record column>, <name column>) of the audit setting, in the six dbspec implementations (Go, TypeScript, PHP, the PHP extension, Python, Rust): the insert and update triggers write one row per (audit, table), and introspection restores the clause. Vectors: the canonical and 11 invalid audit-tables cases of tests/dbspec/cases.json, the audit-tables case of ddl.json with its behaviour steps, and the three introspect.json cases, red before each implementation and green after; the dbspec unit and introspect checks of each runtime pass. docs/spec/dbspec.md, docs/spec/dialects.md and docs/operations/usage.md (+ko). | [o] |
| T201-1 | The external text keeps the foreign keys between used tables (consumer requests.dbs). Cause: the external text held each used table with its columns, primary key and unique keys only, so a runtime that re-parses the manifest and external text refused the audit tables clause of T201, whose tables table needs its foreign key to the audit table: Python at import and PHP at Database::open raised SCHEMA_INVALID. | Acceptance: the external text also keeps each foreign key whose referenced table is a used table of the same external document, with the index that leads with its columns, so the reduced text passes the same validation; docs/spec/dbspec.md (+ko); a sets case red before and green after in every implementation. | Evidence: the sets case external-document-used-table-keys of tests/dbspec/cases.json, which also re-parses the manifest and external text. Red (targeted, one case per implementation) with the message tables table audit_table needs the foreign key (audit_seq) references audit (seq) on delete restrict on update restrict: Go TestDocumentSets/external-document-used-table-keys FAIL, TypeScript dbspec-sets fail 1, PHP and the PHP extension Failures: 1, Python SCHEMA_INVALID at RuntimeModel.load_set, Rust set_vectors::external-document-used-table-keys FAILED. Green: Go ok, TypeScript pass 1, PHP and the extension OK (1 test, 22 assertions), Python ok, Rust 1 passed. The tests that read contracts/fixtures/external pass: PHP OrmGenTest, SchemaSetTest and AuditExternalSetsTest, TypeScript generate.mjs and schema-set.mjs, Go orm External, Rust build generate and orm external_documents and register. The bench manifest hash is unchanged. | [o] |
| T204 | A select read that matches no row returns its empty value; no select raises NO_ROWS (user ruling). Cause: get() raises NO_ROWS when no row matches, and getSum() and getAvg() return CODEC_DECODE for the null that SQL gives over no rows, so a caller catches an error for an ordinary empty result; consumer python (262 sites) and its php and node NO_ROWS catches wait on it. Acceptance, each value fixed by a conformance vector run red and then green in Go, PHP, Python, Rust and TypeScript: - get() and getBy…() return null (Python None, Go nil, Rust None); the NO_ROWS path of get() is removed. - gets(), getsBy…() and getsCount() return an empty collection; getsCount() is grouped rows, so its empty value is a collection. getsPage() returns empty items with totalCount and totalPages 0. - getCount…() returns 0. - getSum() returns 0: the sum over an empty set is 0 by definition, COALESCE(SUM(x), 0) is the standard idiom for SQL's null, and the result type stays a number in every language. - getAvg() returns null: the average over an empty set is undefined, and 0 would be an invented average that cannot be told apart from a real one. - SQL SUM and AVG skip null values and give null when no value is left, whether no row matches or every value is null; both are the empty value above. A nonnumeric, nonfinite or overflowing scalar stays CODEC_DECODE: that is a wrong value, not an empty result. - restore() keeps NO_ROWS for a key that no row holds: it is a write, a restore that writes nothing must not look like a success, and restoreOrCreate() (T200) is the call that makes the row exist. - local(key) keeps NO_ROWS for a missing key: it is a transaction-local value, not a select; a missing key is a caller error, and null would not tell it apart from a stored empty value. - docs/spec/dsl.md, docs/spec/interfaces.md, docs/operations/usage.md and README with their ko twins, contracts/interfaces.json, contracts/fixtures/ir_answers.json, the generated Get comments and every test that expected NO_ROWS from a select. | — | Evidence: red with the implementation set aside: Go and Rust fail to build on the nullable get/getAvg, Python, PHP and TypeScript fail with NO_ROWS: query matched no rows. Green: the vector empty_reads and the vectors whose expectations held the old get() NO_ROWS (terminal_by, write_cycle, delete_recursive) are recorded from the agreed output of the five clients on MySQL, PostgreSQL and SQLite; restore keeps its NO_ROWS. The record tool names the fields that differ between clients. Owner checks: interface-check, the Rust workspace clippy, ir-answers in the five clients, and the changed unit tests of each client. | [o] |
| T204-1 | The contracts follow the empty reads of T204, the batch of T205 and the revision of T212. Cause: T204, T205 and T212 changed the API without its contracts: the state contracts of restore (hidden) and delete_recursive (service_left) still expected NO_ROWS where a read now gives null, tests/interfaces/contracts/validate.go had no nullable output for get and getAvg and no input lists Rows,Batch and ColumnRevision, and Python test_python_natives pinned 22 entity rules, now 24. The state contracts were also unchecked in any run of selected vectors. | Acceptance: each specification matches the API, and a run of selected vectors checks their state contracts; red before and green after. | Evidence: Model.get outputs OptionalModel and Model.getAvg OptionalAggregate (validate.go, interfaces.json); validate.go declares Rows,Batch and ColumnRevision; the two state contracts expect null. tests/interfaces/check takes --vector with --results, and conformance-check passes VECTOR to it, so a run of selected vectors checks their contracts only (Makefile, tests/conformance/README.md +ko). Red: the Go TestLogicalContractRejectsNativeDrift Model.get/php return ?static does not implement Model; Python 24 != 22, then KeyError: 'ColumnRevision'; make run-databases TARGETS=conformance-check VECTOR="audit_lazy restore delete_recursive" with the old contracts fails on state contract restore and state contract delete_recursive in the five clients on the three dialects (audit_lazy is the vector of the connection's first row lock). Green: the Go specification test ok, test_python_natives OK, the sequence and flag tests pass, and the same run passes with ok - interfaces/results. | [o] |
| T205 | creates(rows, batch): the caller sets the number of rows of each multi-row INSERT (consumer S-286-19 product import, user request). Cause: creates(rows) splits the rows into statements by its own rule, so the caller cannot choose the statement size. Acceptance, by a conformance vector run red and then green in Go, PHP, Python, Rust and TypeScript: - creates(rows, batch) is the one form: batch is required and positive, and creates(rows) without it is removed, with no default and no fallback split. A positive integer is the only accepted value; another value fails with CONFIG before any statement. - Each batch of batch rows (the last one may be smaller) is one multi-row INSERT, and every batch runs in one transaction (a savepoint inside an active transaction); a failure rolls back every batch. - A batch whose binds exceed the dialect's bind limit fails before any statement runs with the new error code BATCH_TOO_LARGE (docs/spec/errors.yaml, every catalog, a parity unit), whose message names the batch, its bind count and the dialect's limit; it is never split. - The result is the inserted row count. - docs/spec/dsl.md, docs/spec/interfaces.md and docs/operations/usage.md with their ko twins, contracts/interfaces.json, the generated wrappers and parity units. | — | Evidence: red with the model sources of the merge base (the vectors, runners and models kept): the Go conformance runner and the Rust runner fail to build on creates(rows, batch). Green: the vectors creates_batch and creates_batch_too_large, and contains_ascii_fold and contains_binary_code_point, whose rows are inserted by creates, are recorded from the agreed output of the five clients on MySQL, PostgreSQL and SQLite, and the Python case that runs every vector passes them on the three. The green run found that Python ran its batches outside the transaction it opened (transaction null); its executor is now taken inside it. On SQLite creates_batch alone also sends the connection's first orm__row_lock statement, which its expectation leaves out (tests/conformance/README.md). The statement difference now names a statement's tables and transaction. interface-check and parity-declarations-check pass. | [o] |
| T202 | The conflict key of an upsert is validated after the request hook (consumer hyper ScopeHook). Cause: the request hook assigns the scope column of an insert, but the validation of the conflict key of duplication()->create() reads the request before the hook, so a key that the hook completes is refused. | Acceptance: the validator sites (Go ir.go, PHP Validator.php, Python validate.py, Rust validate.rs, TS validate.ts) read the request that the hook returned; a unit case per language, red before and green after. | Evidence (2026-10-19): the case upsert_scope in each client (contracts/fixtures/request_hook.dbs table hook_upsert with unique (scope_col, name); the hook sets scope_col on insert; duplication(hits=7)->create() with only name runs twice and leaves one row a/k/7). RED, with the hook's assignment removed: Go, PHP (Validator.php:180), Python, Rust and TypeScript fail with IR_INVALID: on_duplicate needs exactly one conflict key; unique (scope_col, name) can meet the row but the insert does not assign scope_col (var/t202-red-{go,php,python,rust,ts}.log). GREEN with the real hook: all five pass (var/t202-green-*.log). So every validator reads the request that the hook returned, and the case now guards it; consumer no longer issues this upsert (canister writes by read and save()). | [o] |
| T203 | relation() and relations() run under a service scope (consumer). Cause: under a service scope both fail with CONFIG. | Acceptance: the CONFIG failure does not reproduce on current main. With every polyspec main clean, limepie (<limepie> and <polyspec> are the checkouts of limepie and polyspec) ran nice -n 10 make php-conformance APP=<limepie>/hyper/app PYTHON=<limepie>/hyper/python POLYSPEC=<polyspec> in modules/canister at canister main d308458 and got "conformance: 101 cases, 101 passed, 0 failed" and "PASS php-conformance conformance.php 202640.190ms", with no CONFIG line; the relation() and relations() calls at InventoryController.php:51 and :64 and Ledger/DrugController.php:48 run under the ScopeHook of the contracts. The cause of the earlier failure, likely a stale main or an unclean link, is not proven. The request hook case relation_scope (request_hook_run.mjs, test_request_hook.py, request_hook_test.go, request_hook_cases.php, request_hook.rs) runs relation() and relations() under a service scope, a hook that scopes the root of every read and each of its relation queries, in every client, and binds the parity unit coverage-request-hook-relation-scope | make request-hook-relation-scope-check, make parity-declarations-check UNITS=coverage-request-hook-relation-scope | [o] |
| T206 | orm uses the kit full-run guard that refuses make check and parity-check while a row is waiting or in progress (kit K233, K234). | Acceptance: one dry run of each in orm names the open rows and refuses. | Evidence (2026-10-19): RED: var/t206-red.log, make run-databases TARGETS=parity-check on 89cf4323, builds TypeScript and the Rust interfaces in interface-check (lines 87-147) before full-run.mjs gate refuses (line 149), because the check was a recipe line after the prerequisites. The fix is kit K236 (215202a5): kit.mk runs the check while make reads the makefiles when parity-check is a goal without UNITS; the check line orm had added to parity-prepare is removed. GREEN: var/t206-green.log, make parity-check with this Makefile, refuses with the 23 waiting rows and kit.mk:290: *** parity-check: the gate of full-run refuses the full run and runs no build (0 run-long, cargo, typescript-build or parity-bench lines). make check refuses the same rows (e095582f). | [o] |
| T207 | Database servers stop with their checkout. Cause: mysqld pairs of deleted worktrees and of an earlier start on the same datadir keep running for days. | Acceptance: removing a worktree, and starting a server pair on a datadir, first stop the servers that belong to it; a test red before and green after. The leftover processes running now are the user's to stop. | Evidence: a start of the test servers first stops the servers that an earlier start left on its datadir, known only by their pid files under .runtime/servers (mysqld --pid-file, postmaster.pid, the poolers' pid files), then starts afresh; make worktree-remove WORKTREE=<path> stops a worktree's servers before git worktree remove (AGENTS.md). The unit case in scripts/test-servers.test.mts stops a fake server by its pid file and starts no database. The leftover processes running now are left to the user. | [o] |
| T208 | A generated model whose metadata differs from its manifest is refused (consumer PlanBinds warning). Cause: consumer's PHP models were generated before orm-gen wrote the varchar length (adc4d518), and the runtime reads them with a PHP warning at PlanBinds.php:91 and a null length, instead of refusing them. | Acceptance: building the runtime model compares each generated model's metadata with the entity its manifest gives, and a difference fails with CONFIG naming the class, the entity, the first differing field and the orm-gen command; red with a stale model, green after; in every client whose generated code carries metadata. | Evidence: PHP RuntimeModel::fromModels builds each entity from the manifest and compares the generated class's meta() with it; a difference fails with CONFIG naming the class, the entity, the first differing path (such as columns.name.length) and the orm-gen command (RuntimeModelTest test_a_generated_model_that_its_manifest_contradicts_is_refused). Go, TypeScript, Python and Rust build the runtime model from the manifest text and look entities up by name, so their generated code cannot contradict it. docs/spec/dbspec.md "Runtime model" (+ko). Red: with RuntimeModel.php of 455fe6a0~1, phpunit --filter test_a_generated_model_that_its_manifest_contradicts_is_refused fails (the stale model is accepted, RuntimeModelTest.php:99); green on 455fe6a0: OK (1 test, 4 assertions). | [o] |
| T209 | A table of an external document is read through a read-only entity (consumer: the audits of rows across sets). Cause: one request carries one manifest hash and the runtime model has no entity for an external table, so no query can read another set's table, and collecting keys into IN is forbidden. | Acceptance: the using set's runtime model and generated code give each used external table an entity that can be selected, joined, used in subqueries and relations and given conditions; every write fails with CONFIG <table> is owned by another document set; docs/spec/dbspec.md "External documents" (+ko); a conformance vector red before and green after in the five clients. | Evidence: a used external table becomes an entity of the using set, built from its external definition only, with every write refused with CONFIG <table> is owned by another document set in the five request validators; the generators declare its model and the relation getters into it. The bench set gains bench_reader.dbs (owned) and bench_shared.dbs (external, table shared_note). Red with the implementation sources of the merge base: the conformance build fails in Go and Rust. Green: the vector external_read (a relation read through getSharedNoteModel(), an IN subquery across the sets, and a refused create) is recorded from the agreed output of the five clients on MySQL, PostgreSQL and SQLite, and the Python case that runs every vector passes it. The bench change was carried into every bench-reading test, each rerun green file by file: one install helper per client installs bench_shared.dbs as its own set before the bench set (PHP tests/bench.php, Go internal/benchset, TypeScript tests/bench-set.mjs, Python tests/bench_set.py, Rust tests/src/bench_set.rs), the introspection round trips read each schema document with the documents it uses, the generator tests expect the external models, and the Rust integration program reads dbspec-set.json. interface-check and parity-declarations-check pass. | [o] |
| T209-1 | The PHP generator test expects the read-only models of the used external tables (follows T209). Cause: since T209 the generators write a read-only model for each used external table, and OrmGenTest test_gen_generates_only_the_owned_tables_of_a_set_with_external_documents still expects only the owned models, so it fails with generated files ExtAccount.php, ExtAudit.php, ExtPost.php, ExtPostHistory.php, bootstrap.php | packages/php/tests/OrmGenTest.php | vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/OrmGenTest.php | [o] |
| T210 | The tests that pass a native time value pass. Cause: Go TestStatementTimeout and TestStatementTimeoutThroughAPooler fail with CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value (timezone_test.go:333 and 403), and every case of packages/ts/tests/model.mjs fails with CODEC_ENCODE: parameter 5 (object) is not a datetime value from its Date setup (model.mjs:176-177), on main before T200 as well; native time values are refused since 14f1796a. | Acceptance: each test passes the canonical text form, and they pass on MySQL, PostgreSQL and SQLite. Evidence (2026-10-19), run alone against the test servers (TZ=Asia/Seoul) with node tests/go-test.mts -count=1 ./packages/go/orm -run '^(TestStatementTimeout|TestStatementTimeoutThroughAPooler)$' and node packages/ts/tests/model.mjs: RED (test files of 9ce82e84^, var/t210-red.log): both Go tests fail at timezone_test.go:333 and :403 with CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value, and model.mjs fails with parameter 5 (object) is not a datetime value. The first green on 9ce82e84 still failed the 9 connectionsUseUtc cases: model.mjs:522 passed new Date('2026-01-02T00:00:00+09:00'); it now passes the canonical UTC text 2026-01-01 15:00:00, and the equality filter by an instant, which no client accepts any more, is removed. GREEN (var/t210-green.log): Go both PASS on MySQL and PostgreSQL, model.mjs 64 ok and 0 failing on SQLite, MySQL and PostgreSQL, the 9 connectionsUseUtc cases included. | — | [o] |
| T211 | The TypeScript scan refuses a getter call that no call attaches (consumer: getMatch of a nested relation alias). Cause: the scan declares get<Name>() only when it resolves both the alias<Name>() call and the getter's receiver to a generated model, and drops an unresolved call without a message; the alias of a relation nested in a relation and a receiver typed through a getter's generic argument are dropped. Rust already refuses ("no call attaches", generate.rs). | Acceptance: the five generators resolve a nested relation alias, and a scanned getter call that no alias, new<Name> or addColumn attaches stops generation naming the model and the method; red before and green after in each. | Evidence: red with the generator sources of HEAD~1 and the new cases kept (one targeted run per generator): Go TestGoUnattachedGetterStopsGeneration FAIL; PHP 2 of 3 fail; Python test_a_getter_call_that_nothing_attaches_stops_generation FAIL; Rust unattached_getter_stops_generation FAILED; TypeScript nested_alias_getter and unattached_getter fail. Green on the fix: Go ok, PHP OK (3 tests, 9 assertions), Python OK (3 tests), Rust 3 passed, TypeScript pass 3. The green run caught that the Rust scan read Option<T> as T and refused unwrap as a model method; an Option binding now resolves unwrap and expect to its model. Rust already resolved the nested alias, so its nested case passes before and after. | [o] |
| T212 | An update or delete with an expected revision fails with REVISION_CHANGED when the row moved on (consumer S-286-19: page/board ManageViews::submit and the board comment delete each rebuild it, which make duplicates reports). | Acceptance: a model declares a revision column (or the chain names it); update() and delete() given the expected revision lock the row in the given transaction, compare the revision, and on a match update with revision + 1 or delete; on a mismatch they fail with the new catalog code REVISION_CHANGED and change nothing. Docs (+ko) and contracts state it; a conformance vector runs red and then green in Go, PHP, Python, Rust and TypeScript on MySQL, PostgreSQL and SQLite. | Evidence: expectRevision(column, revision) (Python and Rust expect_revision) adds the revision to the key predicate of update() and delete(), and update() increments it; a write that matches no row fails with the new code REVISION_CHANGED and writes nothing. Red with the implementation sources of the merge base: the Go and Rust runners fail to build (ExpectRevision undefined, no method named expect_revision), and Python fails with CONFIG: ExpectRevision is not a column of author; the SQLite unit cases (revision_test.go, RevisionTest.php, revision.mjs, test_revision.py, revision.rs) fail before and pass after in each client. Green: the vector expect_revision (author like_count 42 to 43, both mismatches REVISION_CHANGED, the row kept, then deleted) is recorded from the agreed output of the five clients on MySQL, PostgreSQL and SQLite, and the Python case that runs every vector passes it; on SQLite its expectation leaves out the connection's first orm__row_lock statement (tests/conformance/README.md). interface-check, parity-declarations-check and documents-check pass. | [o] |
| T213 | Each generator's list of base Model methods is the Model's own (consumer: Python orm-gen refused restore_or_create, the T200 method). Cause: every generator kept a hand list of the base methods, and they drifted: Python _BASE_METHODS lacked restore_or_create and alias and named to_json, which Model lacks, so a scanned call refused generation; PHP FIXED_METHODS lacked restoreOrCreate, encodeValue and jsonText, so a json_text column generated a jsonText that clashes with Model::jsonText; TypeScript baseMethods lacked restoreOrCreate. Rust and Go already matched. Fix: Python reads the public methods of Model, PHP reflects Model (public and protected), TypeScript reads Model.prototype, and Rust keeps its list under a test that compares it with the public methods of fixed.rs.txt. Evidence (2026-10-19), each run alone: RED: Python test_orm_gen fails Author.restore_or_create is not a model method and Author.alias is not a model method; PHP RuntimeModelTest accepts the clashing json_text column; TypeScript generate-scan base_methods_match_model reports missing ["restoreOrCreate"]; the Rust guard fails FIXED differs from the public methods of the fixed impl with restore removed from FIXED (var/scan-methods-red-.log). GREEN: Python 14 OK, PHP RuntimeModelTest 11/11 and OrmGenScanTest 31/31, TypeScript generate-scan 7/7 and generate 1/1, Rust fixed_method_tests and the generate tests 30 passed (var/scan-methods-green-.log); make interface-check passes after recording Generator::modelMethods (var/scan-methods-interface.log). | — | — | [o] |
| T214 | The Mermaid round trip of the bench document expects the objects of the soft_link table (follows T200). Cause: T200 added soft_link to contracts/fixtures/bench/bench.dbs, and the round_trip case bench of tests/dbspec/mermaid.json still listed the drops of the document before it, so every implementation failed export dropped 22 objects and import dropped 11 objects. Fix: the case lists the comment, settings and unique uq_soft_link_code of soft_link that export drops, and the unique code that import drops. Evidence (2026-10-19), each round-trip test run alone: RED on 299f9976: Go TestMermaidRoundTripVectors/bench, Python test_dbspec_mermaid_round_trip, TypeScript dbspec-mermaid-round-trip.mjs, Rust dbspec_mermaid round_trip::bench, PHP and the PHP extension DbspecMermaidTest test_round_trip fail on these four objects (var/mermaid-red*.log). GREEN with the vector: all six pass (var/mermaid-green-*.log). | — | — | [o] |
| T215 | The PHPStan case of ClientGenericsTest passes after T204. Cause: ClientGenericsTest.php:100 fails because the closure now returns Author|null: get() returns null since T204, and the case still declares Author. | Acceptance: the case types the nullable result, red before and green after. | Evidence: the source already declared the nullable read (Model::get(): ?static, packages/php/src/Model.php:1485, and Collection::get returning TModel|null); the case declared fn(): Author and read the name without a null check. Red: calls.php:9: Anonymous function should return ...Author but returns ...Author|null [return.type], Failures: 1. The case now types ?Author and reads $author?->getName() ?? ''. Green: OK (1 test, 1 assertion), ClientGenericsTest.php alone. | [o] |
| T216 | PHP ModelTest test_insert_parts@mysql runs within the memory limit. Cause: it builds 65536 Service objects and exhausts the 128M limit at Model.php:403. | Acceptance: the cause is stated (the test's size or the client's memory per row) and the case passes, red before and green after. | Evidence: the cause is the test, not the client: on MySQL the case built bind limit + 1 = 65536 distinct Service models to reach BATCH_TOO_LARGE, about 1846 bytes each (memory_get_peak_usage 4,116,816 to 22,745,304 bytes for 10000), and PHP ran out of 128M at ModelTest.php:561 before creates ran; creates refuses an oversized batch from its row and column counts before any statement and keeps no copy per batch. The case now repeats one model (array_fill), which proves the same refusal through the same path; memory_limit is unchanged. Red: Allowed memory size of 134217728 bytes exhausted ... ModelTest.php on line 561. Green: --filter test_insert_parts OK (3 tests, 24 assertions) on sqlite, mysql and postgres, peak 28 MB. | [o] |
| T217 | The Rust integration case conditions passes. Cause: integration.rs:185 passes a native DateTime, which the parameter check refuses with CODEC_ENCODE ... is not a datetime value since native time values are text only. | Acceptance: it passes the canonical text, red before and green after. | Evidence: integration.rs:185 now passes the time compare through the file's txt() text helper, as T210 did in Go and TypeScript; no other Rust test passes a native time value as a parameter (conformance.rs already uses txt(), and the orm tests read time values or use fixed clocks). Red: packages/rust/target/debug/integration contracts/fixtures/bench/dbspec-set.json --case conditions panicked at integration.rs:185 with CODEC_ENCODE: parameter 0 (DateTime(...)) is not a datetime value. Green: the same command exits 0 on sqlite, mysql and postgres. | [o] |
| T218 | The PostgreSQL replica of the test servers stays in sync. Cause: its log repeats requested WAL segment ... has already been removed, so a commit waiting on synchronous replication hangs and the primary_and_replica postgres cases block. | Acceptance: the cause in the server setup is stated and fixed (WAL retention or a replication slot), with a case that keeps the replica streaming across a long run. | Evidence: the replica was made with pg_basebackup -R -X stream and no replication slot, with wal_keep_size 0, so a checkpoint removed WAL the stopped or lagging replica still needed, and every remote_apply commit waited forever. pg_basebackup now runs with -C -S orm_replica (postgresBaseBackupArguments in scripts/test-servers.mts): a physical replication slot keeps every segment the replica has not received, and -R writes its primary_slot_name; a start rebuilds both data directories, which drops the slot. docs/spec/config.md (+ko). The case in scripts/test-servers.test.mts builds its own primary and replica on free ports, stops the replica, writes 8 rounds of insert, pg_switch_wal() and CHECKPOINT, starts the replica and requires a remote_apply commit within 20 s and a streaming replica. Red: a synchronous commit did not return within 20 s ... requested WAL segment 000000010000000000000003 has already been removed. Green: the case passes (2431 ms). | [o] |
| T219 | The generator scan types a name by the classes it may hold and accepts getters of used-document entities (consumer python core requests models). Cause: the scans of PHP, Python and TypeScript kept the last textual binding of a name, so a name rebound in a nested scope or branch was attributed to the wrong class (Ip.set_blocked is not a model method on an IpBlock row), and the getter check accepted get_<entity>_model only for entities of the generated document (IpBlockHistory.get_audit_model is called, but no ... attaches it). Ruling, decided with an adversarial audit until no core disagreement: a name holds the set of classes it may hold (branches, switch, try, match and loops merged; a branch that returns or throws contributes nothing; a loop keeps its pre-loop state; null and an unassigned name hold no model; an unknown value hides the name), and a model method call is refused only when no class in that set declares it. The PHP generator also checks the argument count against those classes; TypeScript, Rust and Go rely on their compiler and Python resolves chains at call time (docs/spec/dsl.md). The used tables are entities of the set since T209, so their getters attach as local ones. Also fixed: the Rust generate test styled_setter_result_handling_preserves_model_calls matched T212's expect_revision with pub fn expect; it now matches pub fn expect(. Evidence (2026-10-19), each case run alone: RED on 299f9976 and on each earlier round of this row: the external getter and per-scope cases fail in Python, PHP, TypeScript, Rust and Go, and the round 2 to 4 cases (union, switch/try/match, loops, references, exits, finally, zero iterations, null, argument count) fail on the code before them (var/scan-scope-red-.log). GREEN rebased on 0950457a: Python test_orm_gen 26 OK, PHP OrmGenScanTest 45 OK, PHP-TS scan parity 16 OK, TypeScript generate-scan 11 pass, Rust generate 32 and the integration external getter 2 passed, Go scan tests 11 PASS (var/scan-scope-green-round5-.log); make interface-check green after recording (var/scan-scope-interface-round5.log). | — | — | [o] |
| T220 | An audited unit of work that writes no row writes no audit record (consumer S-286-19-11: an unblock that finds nothing to change must change nothing, requests.md:698). Cause: the executor inserts the audit record when an audited top-level transaction starts (PHP Db.php:519-526, and likewise in the other clients), so a transaction whose body writes nothing, such as the race path of the unblock, leaves one audit row. | Acceptance: the record is inserted right before the first write of the unit of work, which still precedes its changes as docs/spec/dbspec.md "Audit" requires, and a unit that writes nothing inserts none; docs/spec/dbspec.md (+ko) states it; a conformance vector runs red and then green in Go, PHP, Python, Rust and TypeScript on MySQL, PostgreSQL and SQLite. | Evidence: the pending audit record is inserted right before the first statement that binds the audit key: an insert inserts it first, and an update, delete or restore runs SAVEPOINT orm_audit, the audit insert and the write, releasing it when the write changed rows and rolling it back on 0 rows or an error; a nested savepoint that does not release restores the previous audit. The bench gains the audited table audited_note with audit_record and audited_note_history. docs/spec/dbspec.md Audit, docs/operations/usage.md and docs/spec/interfaces.md (+ko). Red with the client changes reverse-applied: the vector audit_lazy gives {before:1, read:0, unchanged:1, written:1, rolled_back:true, after:0} in all five clients on MySQL, PostgreSQL and SQLite, the record existing before any write. Green: {before:0, read:0, unchanged:0, written:1, rolled_back:true, after:0}, recorded from the five agreeing clients on the three dialects with SQLite write_cycle re-recorded beside it, since audit_lazy now makes the connection's first row lock. The Go case of audit_transaction_test.go fails on the eager code and passes. interface-check and documents-check pass. | [o] |
| T221 | The MySQL migration case of test-servers-check passes. Cause: "a MySQL setting other than the declared one migrates the data with equal counts, then nothing" fails on main: the output lacks RUN mysql-migration/preflight no-deadline, and the message concerns lower_case_table_names=2 against the declared value 1. | Acceptance: the cause is stated and the case passes, red before and green after. | Evidence: the migration was right and the case was stale: a004c343 (T118-7) made runLong print TAP lines (ok - <case>, not ok - <case> and # <reason>), while the case still expected RUN ... no-deadline; lower_case_table_names=2 is macOS's default, from which the case migrates to the declared 1 on purpose. The case (scripts/test-servers-mysql.test.mts) now requires exactly the eight lines ok - mysql-migration/{preflight,dump,stop,keep,start,restore,verify,record} in order and rejects a deadline line or Bail out!. Red: the regular expression /^RUN mysql-migration\/preflight no-deadline$/m did not match while the eight steps printed ok. Green: the case passes, and the file passes 6 of 6. The Linux-only collision case expects not ok - mysql-migration/preflight with its reason line; it returns early on macOS, so it is verified only on Linux. | [o] |
| T221-1 | The Linux lower_case_table_names collision branch of the MySQL migration case runs red and then green on Linux (follows T221). Cause: the branch returns early on macOS, so T221 verified only the macOS path. Hold: this machine has no Linux host for orm; its only Linux VM (lima soksak-linux-arm64) belongs to another project, and creating an orm VM is the user's decision. Retry: a Linux run of that case, red before and green after, with its log. | — | node --test --test-name-pattern='names that one lower case name would merge refuse the migration' scripts/test-servers-mysql.test.mts | [-] cause: the user's ruling of 2026-10-25 (local macOS only): orm only has to run locally on this Mac with plain symlinks, so a Linux-only validation run is out of scope |
| T222 | The Python scan binds a model only from the generated models module, and parameters start their own binding (follows T219; consumer python Module.holds is not a model method). Cause: scan.py ImportCollector bound a name to a model by the imported class name alone, so consumer's contracts/src/kit/harness_calls.py, which imports the ORM Module as ModuleRow from consumer.model.models and its own non-model Module from ..calls, had Module bound to the ORM model, and every module.holds/stock/run/products/packages/items/load/submit call was checked against it. The same review found parameters that leaked the outer binding: Python *args, **kwargs and lambda parameters, and a TypeScript callback parameter typed with another class, which took the receiver's model. Fix: a class name binds a model only when it is imported from a module named models (orm-gen writes models.py), under its alias, and a same-named import from another module unbinds the name; every Python parameter starts unknown unless its annotation names a model; a TypeScript annotated parameter takes the class it names. A model re-exported through another module is not bound, so its calls go unchecked instead of refused (T219 rule). PHP had no leak (guard case added). Evidence (2026-10-19), each run alone: RED on 033edc4d: consumer's make python-model command (read-only, output to a scratch dir) fails Module.holds is not a model method; Module.stock ... (var/scan-params-consumer-red.log); the minimal case test_a_class_named_like_a_model_but_imported_from_another_module_is_not_the_model fails Author.set_ghost_column is not a model method; the Python variadic and lambda cases and the TypeScript callback_parameters case fail (var/scan-scope-red-params-*.log). GREEN: consumer's model set (36 models) and requests set (8 models) generate (var/scan-params-consumer-green.log, -requests-green.log); Python test_orm_gen 30 OK, TypeScript generate-scan 12 pass, PHP OrmGenScanTest 46 OK, PHP-TS parity 16 OK; make interface-check green after recording (var/scan-params-interface-consumer.log). | — | — | [o] |
| T223 | A Korean page links to an English page at any depth. Cause: docs/.vitepress/config.mts writes the link of a Korean page to an English page as ../<path>.html, which is right only at the top level, so the vitepress build of docs-check fails with the dead links ./../plans/execution/plan and ./../plans/kit-standard/plan from the nested plans | docs/.vitepress/config.mts | make docs-check | [o] |
| T224 | A generated PHP model agrees with its manifest whatever order its unique keys and indexes are declared in (limepie: CONFIG generated model Limepie\Model\CommandRun of entity command_run differs from its manifest at unique). Cause: orm-gen builds the meta from the documents in declaration order (OrmGen.php, RuntimeModel::tableEntity), while the manifest text is canonical and sorts unique keys and indexes by name (Dbspec/Emitter.php), so fromModels compares two orders of one table and refuses it | packages/php/src/RuntimeModel.php, packages/php/src/OrmGen.php, packages/php/tests/RuntimeModelTest.php | vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/RuntimeModelTest.php | [o] |
| T225 | The built orm-gen bin of the TypeScript package is executable (fsm). Cause: scripts/typescript/build.mts publishes the tsc output, which writes dist/bin/orm-gen.js with mode 644, so a consumer's node_modules/.bin/orm-gen fails with Permission denied; the dbspec bins are committed with mode 755 and are not affected | scripts/typescript/build.mts, packages/ts/tests/bin-mode.mjs | npm run -s typescript:build && node --test packages/ts/tests/bin-mode.mjs | [o] |
| T226 | A scanned model call belongs to the set whose generated module its class is imported from, in every generator. Cause: the TypeScript scan attributes a call by class name only, so in limepie orm-gen gen --set requests fails on Account.getEnabledTotps() of the main set (node/src/accounts/store.ts:79), whose Account the requests set also uses | docs/spec, the scanners of the five generators, their tests, config/parity.json | node --test packages/ts/tests/generate-scan.mjs, go test -run Scan ./packages/go/generator, the scan tests of PHP, Python and Rust | [o] |
| T226-1 | The TypeScript scan resolves a receiver held in a reassigned variable through its initializer and its reassignments to its class and the import of that class (follows T226). Cause: after T226 limepie models.ts lacks ltSeq and andLtSeq, which modules/core/node/src/console/audit.ts:130 calls on let page = new Audit()..., reassigned from its own chain | the scanners of every generator, their tests | node --test packages/ts/tests/generate-scan.mjs | [o] |
| T227 | A file that orm-gen cannot parse while it scans is an error in every generator. Cause: packages/python/src/polyspec/orm/generate/scan.py:561 skips an unparsable file silently while the PHP and TypeScript scanners fail, a divergence and a dropped error | docs/spec/dsl.md and its Korean twin, the scanners of the five generators, their tests, config/parity.json | node --test packages/ts/tests/generate-scan.mjs, go test -run Scan ./packages/go/generator, the scan tests of PHP, Python and Rust | [o] |
| T227-1 | The Python test of the unparsable scan keeps the ruff format (follows T227). Cause: lint-python fails on main at packages/python/tests/test_orm_gen.py:584, the case T227 added | packages/python/tests/test_orm_gen.py | make lint-python, node --test packages/ts/tests/generate-scan.mjs | [o] |
| T228 | DBS-173 states that every generator writes the read-only model of each external table a set uses through a use line, whether or not a scanned source calls it, with regression cases in the five generators that guard the rule (follows T209). Cause: a consumer's stale generated models, from before T209, looked like a divergence of the generators, and the spec did not state the rule | docs/spec/dbspec.md and its Korean twin, the five generators, their tests, config/parity.json | node --test packages/ts/tests/generate-scan.mjs, go test -run External ./packages/go/generator | [o] |
| T229 | PostgreSQL LISTEN and NOTIFY are a client capability in every implementation: notify(channel, payload) is sent inside a transaction and delivered with its commit, and listen(channel) receives on a dedicated connection; MySQL and SQLite refuse both with a stated error, never a silent no-op (consumer: limepie drama design). Cause: docs/spec states neither, so no client offers them | docs/spec/dialects.md and protocol.md with their twins, every client, a PostgreSQL conformance case, config/parity.json | make parity-declarations-check UNITS=postgres-listen-notify | [o] |
| T230 | A MySQL lock wait timeout (1205) is CANCELED in every client, as the statement timeout of CFG-6 (consumer: limepie drama design). Cause: no client maps 1205, so it falls to DRIVER (python database.py:566-584, php OrmException.php:77, ts driver.ts:184), while errors.yaml names CANCELED for an expired timeout bound | docs/spec/errors.yaml, every client, a MySQL conformance case, config/parity.json | GOPROXY=off go test ./packages/go/orm -run TestMapMySQLErrClassifiesLockWaitTimeout -count=1, env PYTHONPATH=packages/python/src:packages/python packages/python/.venv/bin/python -m unittest tests.test_driver_errors.ServerDriverErrorTest.test_mysql_errors_have_the_codes_of_the_catalog, vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist --filter test_mysql_lock_wait_timeout_is_canceled packages/php/tests/OrmExceptionLockWaitTest.php, node --test --test-name-pattern lock-wait-timeout packages/ts/tests/driver-error-lock-wait.mjs, cargo test --manifest-path packages/rust/orm/Cargo.toml --lib mysql_lock_wait_timeout_is_canceled, make parity-declarations-check UNITS=mysql-lock-wait-canceled | [o] |
| T230-1 | orm main builds: the php-ext arginfo header matches its stub after T230 comment change | packages/php-ext/src/polyspec_orm_arginfo.h | make build-php-ext | [o] |
| T231 | errors.yaml names the duplicate key of every database for DUPLICATE_KEY: MySQL 1062, PostgreSQL 23505 and SQLite 2067 and 1555, as every client maps them. Cause: docs/spec/errors.yaml:87 names only MySQL 1062 / SQLSTATE 23000 | docs/spec/errors.yaml, config/parity.json | make text-absent FILE=docs/spec/errors.yaml TEXT="DUPLICATE_KEY # MySQL 1062 / SQLSTATE 23000" | [o] |
| T232 | docs/spec/dialects.md states the SQLite version of the pinned PHP 8.4 pdo_sqlite, measured by select sqlite_version() (owner ruling: PHP 8.5 goes to 8.4). Cause: dialects.md:76 and dialects.ko.md:71 state pdo_sqlite of PHP 8.5.10 | docs/spec/dialects.md and its twin | make text-absent FILE=docs/spec/dialects.md TEXT="PHP 8.5.10" | [o] |
| T233 | docs/operations/perf.md quotes no PHP 8.5 ratio (owner ruling: PHP 8.5 goes to 8.4). Cause: perf.md:42 and perf.ko.md:43 quote ratios measured on PHP 8.5.10; they are dropped, or measured again on 8.4 in the final benchmark step | docs/operations/perf.md and its twin | make text-absent FILE=docs/operations/perf.md TEXT="PHP 8.5.10" | [o] |
| T235 | Update the vulnerable markdown-it lock and advisory review. Cause: package-lock.json pins markdown-it 14.3.2 with high advisory 1242672 despite fixed 14.3.3. | package-lock.json, config/dependency-review.json | make dependency-policy-check; make dependency-policy-mutation-check | [-] cause: user 2026-10-22: no third-party advisory or pin work during development |
| T234 | The PHP client documents name no third-party YAML product in prose. Cause: the CHANGELOG entries of T142-1-1 and T142-1 and the packaging notes name the upstream YAML product where a generic description says the same fact, which is that the upstream 1.0.0 release does not read plain scalars by the YAML 1.2.2 core schema, and the fork release fixes it. Acceptance: CHANGELOG.md, CHANGELOG.ko.md, docs/operations/packaging.md and its Korean twin keep the dependency package name, the fork URL and the fix facts, and no prose line names the upstream product. Red: the four files hold the product name today. | CHANGELOG.md, CHANGELOG.ko.md, docs/operations/packaging.md, docs/operations/packaging.ko.md | make text-absent FILE=CHANGELOG.md TEXT="purrphp defect" make text-absent FILE=docs/operations/packaging.md TEXT="PurrPHP/yaml" make text-absent FILE=docs/operations/packaging.ko.md TEXT="PurrPHP/yaml" | [o] |
| T236 | Bind root LIMIT count and OFFSET values as parameters so pages with the same query shape share one plan in all six implementations: Go, PHP library, native PHP extension, Python, Rust, and TypeScript. Cause: literal page values change the plan-cache key, so repeated offsets fill Engine::$plans (cacheSize 256) after about 161 pages. Requested by the limepie peer session (P-41); related to T169-3. | PRT-78 specifies the canonical root limit pair, validation timing and errors, recursive parameter walk, dialect bind order, scalar-subquery exception, and count-query exception. All six implementations reuse one plan across 300 page offsets. The native extension owner test in packages/php-ext/tests runs the same four page-cache cases as the PHP owner and asserts native Request/canonicalize and Planner/compile. Fixed parameter-walk expectations cover Go, PHP, native PHP, Python, and TypeScript; Rust covers sorted scalar-subquery names. Provenance: the focused TypeScript case failed on pre-fix source 48b4f552b028 because the actual root limit remained {count: 20, offset: 0} instead of canonical {count_p: 2, offset_p: 1}. The recorded formal RED command was node --test --test-name-pattern "^root pages share one plan shape and bind offset then count$" packages/ts/tests/plan-cache-in.mjs and exited 1. | node --test --test-name-pattern "^root pages share one plan shape and bind offset then count$" packages/ts/tests/plan-cache-in.mjs make page-cache-unit-check | [o] |
| T237 | Declare the ORM worktree server stop target so integration and removal stop servers before deleting their data. Cause: the ORM Makefile has no Makefile-origin WORKTREE_STOP; kit land reads only Makefile-origin values, so an environment or command-line override does not register the stop target. Related: KSTD-12. | Add WORKTREE_STOP = orm-test-servers-stop to the ORM Makefile. | node --test scripts/repo/worktree-stop.test.mts verifies that the kit reads the ORM Makefile-origin stop target under an isolated environment. | [o] |
| T238 | Remove the obsolete prune dependency from the completed worktree-stop test. Cause: T237 still imports worktree-prune and blocks its removal; retain WORKTREE_STOP verification. | scripts/repo/worktree-stop.test.mts; paired T237 verification description | node --test scripts/repo/worktree-stop.test.mts; no worktree-prune import or prune-only case remains | [o] |
| T239 | Make the ORM test databases support the local served-test login on server initialization without weakening authentication or TLS checks. Cause: PostgreSQL trusts every loopback role, MySQL binary logging blocks audit-trigger creation by a non-SUPER login, and the replica certificate prevents a positive VERIFY_IDENTITY connection. Existing PostgreSQL authentication and MySQL daemon settings take effect after the existing stop and start lifecycle; the start command does not restart a leased server. | scripts/test-servers.mts; scripts/test-servers.test.mts | node --test --test-name-pattern="PostgreSQL trusts only|MySQL replica has a positive|test servers keep small redo logs" scripts/test-servers.test.mts runs three changed server configuration unit cases. node --test --test-name-pattern="the CI workflow provides every variable" scripts/repo/check.test.mts verifies the new server environment variable remains supplied by the declared server setup. | [o] |
| T240 | Refresh tracked Kit copies from the installed local Kit package and keep them regular files | .claude/settings.json, .editorconfig, PRINCIPLES.md, PRINCIPLES.ko.md, .github/workflows/release.yml, .github/workflows/gates.yml | make kit-check; confirm the six tracked copies are regular files matching the installed local Kit source. | [o] |
| T241 | Release 0.0.19. Cause: the kit 0.0.66 adoption (KSTD-5-101), the dependency target removal (KSTD-5-100), the lease-only release build (KSTD-5-98) and the declared wheel backend (KSTD-5-99) are on main. Acceptance: the version-bump commit sets every manifest to 0.0.19 and moves Unreleased to 0.0.19; make release-candidate-assets builds the eight archives, orm-gen included; the tag waits for the owner | VERSION, every manifest and lock that carries the version, CHANGELOG.md, CHANGELOG.ko.md | make release-versions TAG=v0.0.19 | [ ] |
| T242 | orm has no version check: scripts/version/check.mts, its test scripts/version/check.test.mts, the version-check target and PLN-5 are deleted, and kit make release-versions requires the tag version in every manifest at release. Cause: the PLN-5 test named the version as a literal, which every release commit had to change, and kit K449 refuses a test in a release commit | scripts/version/check.mts, scripts/version/check.test.mts, Makefile, contracts/check-inputs.json, config/rebuild.json, config/parity.json, docs/spec/plan.md, AGENTS.md, scripts/check/run.test.mts, scripts/harness/cargo-test.test.mts | make parity-declarations-check owner-validate | [o] |
| T243 | config/parity.json declares packages/rust/orm/tests/unit/temporal_args.rs, so make parity-declarations-check passes on main. Cause: on main 8685a2758 the check reports R5 undeclared-test for the file, which 4dedfb2d6 (KSTD-5-94) added with its owner target rust-temporal-args-check but without a parity declaration | config/parity.json | make parity-declarations-check | [o] |
| T244 | polyspec/orm holds orm-gen again: bin/orm-gen with OrmGen, Generator, Scan, ScanVisitor, ChainDeclare and UsageError, and their tests, live in packages/php as in 0.0.18, and the package packages/php-gen with its release entries is deleted. Cause: T182-6-19-4 (7f0249f84) split orm-gen into polyspec/orm-gen under the kit rule S10, which kit deletes, so a consumer of the polyspec/orm link has no Generator | packages/php, packages/php-gen, composer.json, composer.lock, config/release.json, Makefile, config/parity.json, contracts/check-inputs.json, contracts/symbols/php.json, docs/operations/packaging.md, docs/operations/usage.md | make php-gen-unit-check | [o] |
| T245 | The repository tests of the CI tool selection and of orm-install-python match the invoked install-tools target with its TOOLS, not the command text of a recursive make. Cause: kit K762 puts KIT and KIT_EXT on the command line of every recursive make, so the regexes of scripts/repo/check.test.mts and scripts/repo/python-venv.test.mts that pinned make --no-print-directory install-tools fail although the selection is right | scripts/repo/check.test.mts, scripts/repo/python-venv.test.mts | make repo-check/unit | [o] |
| T246 | The runtime path check of scripts/repo/testcases.mts accepts TEST_ENV ?= .runtime/servers/env, the server environment that one checkout shares and a run may override. Cause: KSTD-5-94 (b597a6c49) made TEST_ENV overridable with ?=, and the check still allowed only TEST_ENV = | scripts/repo/testcases.mts, scripts/repo/check.test.mts | node --test --test-name-pattern='the files of one run live in its run directory' scripts/repo/check.test.mts | [o] |
| T247 | Every tracked lock file has its owner again: packages/ts/testing/interfaces/typescript/package-lock.json is an input of interface-typescript-extractor-check, which runs the TypeScript it installs, and the release consumer locks tests/release-consumer/npm/package-lock.json and tests/release-consumer/composer/composer.lock declare their scope in contracts/check-inputs.json. Cause: KSTD-5-100 deleted dependency-policy-check, whose inputs **/package-lock.json and **/composer.lock owned them | contracts/check-inputs.json | make feature-unit-check/owners | [o] |
| T248 | The three TypeScript binding commands of config/parity.json run the node:test reporter of the kit that make includes: the parity targets export ORM_NODE_REPORTER = $(KIT)/parity-node-reporter.$(KIT_EXT), and the commands pass --test-reporter=${ORM_NODE_REPORTER}. Cause: KSTD-5-94 (16204d8e6) named node_modules/@polyspec/kit/dist/parity-node-reporter.mjs, a gitignored local build of the linked kit, so the commands run a stale build or fail where kit was not built | config/parity.json, Makefile | make parity-declarations-check | [o] |
| T249 | make duplication-check finds no clone, and the parity context test reads the ts setup of config/ci.json: SqliteWithoutMysqlTest.php runs its PHP child through one method, parity-context.test.mts builds its failing commands and checks its removed directories through one helper each and expects orm-install-interfaces-typescript in the ts setup, and performance-reference.mts reads git through the git reader of scripts/check/record.mts. Cause: KSTD-5-94, KSTD-5-12 and KSTD-7-6 left 5 clones, which the land gate reports for every branch that changes a TypeScript or PHP file, and 4dedfb2d6 added the interfaces TypeScript to the ts setup without its test | packages/php/tests/SqliteWithoutMysqlTest.php, scripts/check/parity-context.test.mts, scripts/check/record.mts, scripts/features/performance-reference.mts | make duplication-check; node --test scripts/check/parity-context.test.mts scripts/features/performance-reference.test.mts | [o] |
| T250 | packages/python/tests/coverage_catalog_connection.py is in the format of the pinned ruff format. Cause: KSTD-5-94 left two lines over the line length, which make lint-python reports for every branch that changes a Python file | packages/python/tests/coverage_catalog_connection.py | make lint-python | [o] |
| T251 | On MySQL the planners run a stage in SQL only when every later stage of the write order runs in SQL too, so a column of codec ip hex packs and unpacks its address on the client and binds its hex text without INET6_ATON or INET6_NTOA, in every implementation, with planner vectors. Cause: the planners chose SQL per stage, so on MySQL the client applied hex to the address text and INET6_ATON received that hex text (1411 Incorrect string value for function inet6_aton, found by the limepie MySQL run); hex runs on the client since T103 | docs/spec/dialects.md, contracts/fixtures/planner.json, contracts/fixtures/ip_hex.dbs, the planners of Go, PHP, the PHP extension, TypeScript, Python and Rust | node packages/ts/tests/coverage_planner.mjs planner_ip_hex_insert planner_ip_hex_select | [o] |
| T252 | The privilege test of packages/rust/tests/src/integration.rs reads ORM_TEST_POSTGRES_SERVER_DSN with a match that names the variable error, without Result::ok. Cause: KSTD-5-94 read it with ok(), which clippy disallows (P8), so make lint-rust fails for every branch that changes a Rust file | packages/rust/tests/src/integration.rs | make lint-rust | [o] |
| T253 | A value predicate on a codec column takes the equality operators when what it compares is a deterministic function of the value (the cell of a column whose every stage is hex or ip, or the blind index of an AES column), and only null checks otherwise; codec.md COD-43 states the rule, and Go ir.OpAllowed follows it, with planner cases of eq on hex and on ip hex in every implementation. Cause: Go allowed equality only on a single ip, so eq on a hex or an ip hex column was OPERATOR_NOT_ALLOWED in Go and accepted by PHP, the PHP extension, TypeScript, Python and Rust, and COD-43 limited styled predicates to null checks, against COD-4, PRT-22 and PRT-67 and every implementation | docs/spec/codec.md, packages/go/engine/ir/ir.go, contracts/fixtures/planner.json, contracts/fixtures/ip_hex.dbs | node tests/go-test.mts -tags featurecoverage -timeout 0 ./packages/go/orm -run '^TestCoveragePlanner(IpHex|Hex)' -count=1 | [v] |
| T254 | A codec column compares by equality exactly when every stage is a host stage (aes, hex, ip), the stages that encode a predicate value: hex and ip compare the stored cell, and aes compares the blind index, the HMAC of the value whatever its position; a column with an executor codec, such as ordered_json aes, takes only null checks. codec.md COD-43 states the rule, and Go, PHP, the PHP extension and Rust follow it, with planner cases for eq on aes hex, hex aes and ordered_json aes in every implementation. Cause: Go and PHP took equality when the first stage is aes, Rust when any stage is aes, and TypeScript and Python when every stage is aes, hex or ip, so eq on hex aes or ordered_json aes compiled in some implementations and was OPERATOR_NOT_ALLOWED in others | docs/spec/codec.md, packages/go/engine/ir/ir.go, packages/php/src/Validator.php, packages/php-ext/src/validator.c, packages/rust/schema/src/dbspec/mod.rs, contracts/fixtures/planner.json, contracts/fixtures/aes_chain.dbs | node tests/go-test.mts -tags featurecoverage -timeout 0 ./packages/go/orm -run '^TestCoveragePlanner(AesHex|HexAes|OrderedJsonAes)Eq$' -count=1 | [v] |
| T255 | A Python transaction whose start fails gives its pooled connection back: a failed BEGIN closes the connection and a later start step that fails rolls the opened transaction back, as TypeScript, Go and Rust do, and interfaces.md INT-72 states the rule. Cause: Db._begin marked the frame finished before it called _finish, so a failed later step returned at once without ROLLBACK, and a failed BEGIN re-raised without _give_back; the connection stayed in use, Db.close() could not close it, and under SQLite lock contention its lock stayed held until the garbage collector closed it (reported by limepie U-10-9) | packages/python/src/polyspec/orm/database.py, packages/python/tests/test_rollback_fault.py, docs/spec/interfaces.md | make transaction-failure-python-check | [v] |
| T256 | The Python client builds the model of a manifest once per process, as dbspec.md DBS-104 requires and TypeScript does: a registry keyed by the manifest hash and checked against the manifest and external texts returns the model already built, so register_model, Db.register, SchemaUtils.register and connect_schema parse a schema once, and RuntimeModel.load_set returns the documents it already parsed for the same texts. Cause: model_of_manifest and RuntimeModel.load_set parsed the full document set on every call, so limepie registering 9 schemas on 2 connections per request spent about 85 percent of a request in the dbspec parser (reported by limepie U-10-10) | packages/python/src/polyspec/orm/engine/model.py, packages/python/tests/test_load_set.py | packages/python/.venv/bin/python -m packages.python.tests.test_load_set | [v] |
| T257 | A row lock (forShare or forUpdate) is valid on a relation step that has no limit_per_parent and no group_by, in every implementation. Cause: every validator refuses a lock on any relation step with IR_INVALID "row lock is only valid on a root row select" (ts engine/validate.ts:252, php Validator.php:935, python engine/validate.py, go engine/ir/ir.go, rust orm/src/engine/validate.rs, php-ext validator.c), while the planner already appends the lock on the plain relation path (ts planner.ts:394-399); limepie must read the rows that reference a locked batch FOR SHARE, and neither hand-collected keys nor a subquery IN value (no limit, re-evaluates the parent condition) can. The per-parent window path (planner.ts:362-377) keeps refusing, since a window cannot hold a row lock. Acceptance: the spec states the rule; every implementation accepts the lock on a plain relation step and refuses it with limit_per_parent or group_by, with shared validate cases; a conformance case shows the relation statement carries the lock on MySQL and PostgreSQL and the row lock behaviour of the client on SQLite | docs/spec/dsl.md, docs/spec/dsl.ko.md, tests/ir/validate.json, tests/conformance/vectors.json | make run-databases TARGETS=conformance-check VECTOR=relation_row_lock | [!] cause: no new development before the orm 0.0.19 release; retry: after orm 0.0.19 is released |