Skip to content

실행 체크리스트 ​

IDTaskDeliverablesVerificationState
T8Mermaid schema source를 schema의 source of truth인 neutral schema language dbspec으로 교체한다. 우선순위: 최우선. 원인: Mermaid 방언은 physical schema를 손실 없이 표현하지 못하고, field 이름이 동작을 고르며, 생성된 이름이 변형되고, dialect 기능이 조용히 빠지며, hidden marker로 trigger와 .sql source를 복원한다. 완료 기준: T8.0–T8.8이 Go/PHP/Rust/TypeScript에서 conformance와 MySQL/PostgreSQL/SQLite evidence로 완료된다. 작업은 worktree orm-dbspec-T8에서 하고, 모든 항목이 통과한 뒤에만 main에 merge한다.—Evidence: T8의 모든 항목이 완료됐고 main이 dbspec line을 가진다(T8.8).[o]
T8.0MySQL, PostgreSQL, SQLite 사이의 schema 문법과 의미 차이를 모두 기록하고 기능별 neutral 지원 여부를 판정한다(T8 선행). 원인: dbspec은 공통 정의가 하나 있고, 세 DB에서 의미가 같고, introspection으로 복원되는 기능만 지원한다. 이 규칙을 적용하려면 완전한 사실이 필요하다. 완료 기준: docs/spec/dialects.md와 한국어 짝 문서가 type, length/precision, null, default, identity, generated column, key, index, FK, CHECK, comment, collation, namespace와 식별자 규칙, 시간 type과 time zone, enum, unsigned, trigger, DDL transaction, catalog 원천을 다룬다. 각 행은 DB별 문법과 의미, neutral 정의와 렌더링 또는 미지원 사유, introspection 원천을 적는다. 각 판정은 .runtime DB에 대한 추적되는 공유 tests/* case로 검증한다.—증거: make dialect-facts-check TEST_ENV=<main checkout>/.runtime/servers/env를 두 번 실행해 실패 없이 probe 239개(MySQL 8.4.11 98개, PostgreSQL 17.11 75개, modernc v1.58.0의 SQLite 3.53.4 66개)가 1.705초와 1.637초에 통과했고 매번 SQLite 파일 이름 case 4개도 통과했다. 각 probe는 자기 database, schema, file을 삭제하고 확인한다. docs/spec/dialects.md와 한국어 짝 문서는 기능별로 데이터베이스별 문법과 의미, probe ID, 공식 출처, 중립 rendering 또는 미지원 사유, introspection 출처를 기록하고 client별 SQLite library를 기록한다(Go, PHP, Node 3.53.4, Rust bundled 3.51.3은 probe하지 않음). dbspec은 UTC 연결 규칙을 가진 local datetime(p)(시점은 미지원, timestamptz 컬럼은 mapping하지 않고 변환)와 manifestHash에 속하는 실행기 수정 시각 기록을 지원한다. Audit context 정의는 T8.1 review로 남긴다. 사실과 맞지 않는 결함은 T8.0.1–T8.0.11, T8.0.13, T8.0.14다. checklist-check, docs-rules-check, docs-check, docs-verify-idempotent가 통과한다.[o]
T8.0.1PostgreSQL 컬럼 import를 schema별로 복원한다(T8.0 선행). 원인: 컬럼 질의가 pg_class를 relname만으로 join한다(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). Table 이름은 schema마다만 고유하므로(postgres.namespace.relname_per_schema) 다른 schema에 같은 이름의 relation이 있으면 import한 컬럼이 반복된다. PostgreSQL을 사용할 수 없어 import 자체는 실행하지 않았다. 완료 기준: T8.4가 relnamespace로 join하고, 두 번째 schema에 같은 이름의 table이 있는 추적 case가 모든 client에서 컬럼 집합 하나를 import한다.—Evidence: dbspec introspection은 column, constraint, index, trigger를 relnamespace가 현재 schema인 곳에서만 읽으므로 column이 반복되지 않았지만, foreign key는 참조 table을 relname만으로 읽었다. tests/dbspec/introspect.json에 postgres-other-schema를 더한다: 두 번째 schema {schema}_b에 다른 column, check, unique key, index를 가진 같은 이름의 orders table을 두고, 현재 schema의 lines table foreign key가 그것을 참조한다. harness 네 개는 {schema}를 case의 schema 이름 문자열로 치환하고 두 번째 schema를 지운다. 수정 전 Go, PHP, TypeScript, Rust는 foreign key fk_lines_order (order_id) references orders (id)를 복원해 "document differs"로 실패했다. 이제 각 client는 참조 table을 현재 schema에서만 읽고 그 key를 미지원으로 보고하며, make dbspec-introspect-check, dbspec-introspect-php-check(round trip 30개, 미지원 case 8개), dbspec-introspect-ts-check(test 41개), dbspec-introspect-rust-check가 orders의 column 한 벌로 통과한다. docs/dialects.md가 규칙과 placeholder를 적는다.[o]
T8.0.2import가 버리거나 거부하는 index 기능을 보고한다(T8.0 선행). 원인: ormgen import에서 관찰했다. MySQL functional index와 SQLite 식 index는 converting NULL to string is unsupported로 import를 중단한다(internal/ormgen/import.go:194-196, internal/ormgen/migrate.go:285-287). MySQL prefix index와 SQLite partial index는 전체 index로 import한다(import.go:187-207, migrate.go:263-283). PostgreSQL reader는 식 key(indkey 0)를 pg_attribute와 join하며 indpred와 indoption을 읽지 않는다(import.go:653-691, 실행하지 않음). 완료 기준: T8.4가 prefix, partial, 식 index를 위치와 함께 미지원 객체로 보고하며, 해당 데이터베이스의 추적 case가 모든 client에 있다.—Evidence: dbspec introspection이 import 경로를 대신한다. docs/spec/dialects.md "Keys, indexes and foreign keys"는 prefix, partial, expression, full-text index를 미지원으로 정하고, PostgreSQL index query는 expression key를 pg_attribute에 join하지 않고 indpred, indexprs, indoption을 읽는다. tests/dbspec/introspect.json은 이것들을 정확한 미지원 목록의 [index, table, name] 항목으로 검사한다: mysql-unsupported는 prefix index ix_name_prefix와 functional index ix_name_expr, postgres-unsupported는 expression index ix_lower와 partial index ix_partial, sqlite-unsupported는 expression index ix_lower와 partial index ix_partial이다(MySQL에는 partial index가 없다, mysql.index.partial_rejected). 어느 것도 introspection을 멈추지 않으며, ddl.json round trip의 내림차순 key placed_at desc는 각 database에서 복원된다. make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check, dbspec-introspect-rust-check가 Go, PHP, TypeScript, Rust에서 이 case를 통과한다.[o]
T8.0.3생성 컬럼을 일반 컬럼으로 import하지 않고 보고한다(T8.0 선행). 원인: 관찰 결과 MySQL b int AS (a+1) STORED는 GENERATION_EXPRESSION을 읽지 않아 일반 nullable int로 import된다(internal/ormgen/import.go:130-131). SQLite pragma_table_info는 생성 컬럼을 빼므로 import가 이를 버린다(internal/ormgen/migrate.go:225). 완료 기준: T8.4가 세 데이터베이스에서 생성 컬럼을 위치와 함께 미지원으로 보고하며 모든 client에 추적 case가 있다.—Evidence: dbspec introspection은 MySQL GENERATION_EXPRESSION, PostgreSQL attgenerated, SQLite pragma_table_xinfo.hidden을 읽고 generated column을 미지원 column으로 보고한다(docs/spec/dialects.md "Null, defaults, identity and generated columns"). tests/dbspec/introspect.json은 이제 dbspec이 달리 읽을 수 있는 type으로 세 database 모두에서 이를 검사한다: mysql-unsupported의 MySQL next_id bigint AS (char_length(name) + 1) STORED와 name_size bigint AS (char_length(name)) VIRTUAL, postgres-unsupported의 PostgreSQL next_id bigint GENERATED ALWAYS AS (id + 1) STORED, sqlite-unsupported의 virtual g 옆 SQLite s INTEGER … STORED. 각각은 [column, orders, <name>]으로 목록에 오르고 문서에서 빠진다. make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check, dbspec-introspect-rust-check가 Go, PHP, TypeScript, Rust에서 이 case를 통과한다.[o]
T8.0.4식별자 한도를 넘는 이름을 생성하지 않고 거부한다(T8.0 선행). 원인: 관찰 결과 65자 %% index 이름은 제한 없이 MySQL KEY로 생성되고 MySQL이 DDL을 거부한다(1059)(internal/ormgen/ddl.go:138, fulltext ddl.go:141). Trigger와 함수 이름은 table 이름에 접미사를 제한 없이 붙여(internal/ormgen/triggers.go:61-66, 101-102, 165-167) 77자 MySQL trigger 이름과 70자 PostgreSQL trigger 이름을 생성하며 PostgreSQL은 이를 63바이트로 자른다(postgres.ident.truncated_to_63). 완료 기준: T8.3이 63바이트를 넘는 선언·생성 이름을 생성 전에 거부하며 모든 client에 추적 case가 있다.—Evidence: docs/spec/dbspec.md "Names"는 선언한 모든 이름과 renderer가 쓰는 모든 이름(<table>$<column>, <table>$immutable_update, <table>$audit_insert 등)을 63 byte로 제한하고 더 긴 이름을 name.length 오류로 하므로, 그런 이름이 있는 집합에는 statement가 없다. tests/dbspec/cases.json은 정확히 64 byte인 이름을 써서 이를 검사한다: name-too-long(64 byte column 이름), generated-check-name-too-long(64 byte renderer CHECK 이름), generated-immutable-name-too-long(64 byte immutable trigger 이름), generated-audit-name-too-long(64 byte audit trigger 이름)이며 각각 line, column, rule을 가진다. make dbspec-go-check, php clients/php/tests/dbspec_test.php, clients/typescript/tests/dbspec.mjs, dbspec-rules.mjs, dbspec-render.mjs의 node --test(test 168개), make dbspec-rust-check, make dbspec-compare-check(실행 8번이 case 116개에서 일치)가 Go, PHP, TypeScript, Rust에서 이를 통과한다.[o]
T8.0.5foreign key 동작이 행을 바꿀 때 같은 audit 변경을 기록한다(T8.0 선행). 원인: MySQL은 foreign key 동작으로 바뀐 행에서 trigger를 실행하지 않고(mysql.fk.cascade_skips_triggers) PostgreSQL과 SQLite는 실행한다(postgres.fk.cascade_fires_triggers, sqlite.fk.cascade_fires_triggers). 따라서 mode=changes audit trigger(internal/ormgen/triggers.go:378-437)는 PostgreSQL과 SQLite에서만 cascade 변경을 기록한다. 완료 기준: T8.3이 cascade나 set_null foreign key의 자식 table에 대한 audit·immutable setting을 거부하며, 세 데이터베이스의 추적 case가 모든 client에 있다.—Evidence: docs/spec/dbspec.md "Settings"와 "Audit"은 cascade 또는 set_null foreign key의 child에 둔 immutable과 audit을 setting keyword 위치의 setting 오류로 거부한다. 그래서 그런 table에는 어떤 dialect의 statement도 렌더링되지 않고, MySQL trigger가 건너뛰는 cascade 행(mysql.fk.cascade_skips_triggers, 반면 postgres.fk.cascade_fires_triggers와 sqlite.fk.cascade_fires_triggers는 실행한다)이 생성 trigger와 만나지 않는다. 거부가 렌더링보다 먼저이므로 database와 무관하다. tests/dbspec/cases.json에 immutable-on-cascade-child(on delete cascade)와 audit-on-set-null-child(on update set_null)를 더하며, 각각 line과 column에 setting 오류 하나를 기대한다. 이 case는 make dbspec-go-check, php clients/php/tests/dbspec_test.php(invalid case 81개), clients/typescript/tests/dbspec.mjs, dbspec-rules.mjs, dbspec-render.mjs의 node --test, make dbspec-rust-check, make dbspec-compare-check에서 통과하고, compare에서 client 네 개가 일치한다.[o]
T8.0.6일치하는 행이 없는 문장에서 immutable guard를 같게 만든다(T8.0 선행). 원인: PostgreSQL guard는 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) 일치하는 행이 없는 UPDATE나 DELETE도 실패시킨다(postgres.trigger.statement_fires_on_zero_rows). MySQL과 SQLite row guard는 실행되지 않는다(mysql.trigger.zero_rows_not_fired, sqlite.trigger.zero_rows_not_fired). TRUNCATE도 PostgreSQL에서만 다룬다. 완료 기준: T8.3이 세 데이터베이스에서 row 수준 UPDATE·DELETE guard를 생성하고, 0행 추적 case가 모든 client의 세 데이터베이스에서 통과한다.—Evidence: docs/spec/dialects.md "Triggers"는 immutable과 audit의 delete 거부를 MySQL, PostgreSQL, SQLite에서 BEFORE UPDATE와 BEFORE DELETE FOR EACH ROW trigger로 렌더링하고, 어느 database에도 TRUNCATE trigger를 쓰지 않는다. tests/dbspec/ddl.json의 immutable-and-audit vector statement가 그것을 보여 준다. 그 vector에 0행 step을 더한다: 해당 UPDATE와 DELETE가 실패(MySQL 1644, PostgreSQL P0001, SQLite "table ledger is immutable")한 뒤, immutable table의 UPDATE ledger SET amount = 7 WHERE id = 999와 DELETE FROM ledger WHERE id = 999, audit table의 DELETE FROM service WHERE id = 999가 성공해야 한다. make dbspec-ddl-check가 MySQL 8.4, PostgreSQL 17, SQLite에서 step 13개를 실행해 통과하고, client 네 개가 이 statement를 byte 단위로 같게 렌더링한다(make dbspec-go-check, php clients/php/tests/dbspec_render_test.php, clients/typescript/tests/dbspec-render.mjs, make dbspec-rust-check, 그리고 네 client가 일치하는 make dbspec-compare-check).[o]
T8.0.7PostgreSQL에서 명시 key 뒤에도 자동 key를 유효하게 유지한다(T8.0 선행). 원인: 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)는 명시 key 뒤로 진행하지 않아 다음 생성 key가 23505로 실패하고(postgres.identity.by_default_not_advanced), MySQL과 SQLite는 계속 진행한다(mysql.auto_increment.explicit_value_advances, sqlite.autoincrement.explicit_value_advances). 완료 기준: 클라이언트가 명시 자동 key를 거부하고, 명시 key를 쓰는 PostgreSQL apply 단계가 identity sequence를 맞추며, 세 데이터베이스의 추적 case가 모든 client에 있다.—Evidence: dbspec은 PostgreSQL GENERATED BY DEFAULT AS IDENTITY를 렌더링하며(docs/spec/dialects.md "Automatic key"), 모든 client는 이미 statement 실행 전에 명시한 automatic key를 거부한다: request validator는 on_duplicate의 primary key나 identity column을("on_duplicate cannot assign"), planner는 insert의 identity column("cannot set identity column")과 update의 primary key나 identity column("cannot update")을 거부하므로, 어느 client도 PostgreSQL sequence를 지나는 key를 쓰지 않는다. insert와 update 규칙을 검사하는 test는 없었다. 새 tracked case는 네 request를 mysql, postgres, sqlite dialect에서 실행하고 message와 함께 IR_INVALID를 요구한다: Go TestKeyColumnsAreNotWritten(engine/query_forms_test.go), PHP clients/php/tests/engine_test.php, TypeScript clients/typescript/tests/engine.mjs, Rust engine::tests::key_columns_are_not_written이며 모두 통과하고, PHP와 TypeScript case는 기대 message를 바꾸면 세 dialect 모두에서 실패한다. PostgreSQL에서 명시한 key를 쓰는 plan statement는 없으므로(docs/plans.md는 SQLite rebuild에서만 행을 복사한다) reset할 PostgreSQL apply step이 없다. SQLite rebuild가 자기 counter를 되돌리는 문제는 T8.5.1.1로 기록한다. docs/dsl.md가 쓰기 규칙을 적는다.[o]
T8.0.8MySQL text 컬럼에 binary collation을 생성한다(T8.0 선행). 원인: DDL은 각 MySQL table 끝에 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) 이는 utf8mb4_0900_ai_ci를 선택한다(mysql.collation.default_accent_case_insensitive). 따라서 MySQL unique key와 비교는 a, A, á를 같게 보지만 PostgreSQL C와 SQLite BINARY는 그렇지 않다. 완료 기준: T8.3이 utf8mb4_0900_bin과 PostgreSQL COLLATE "C"를 생성하며, 세 데이터베이스의 unique key 추적 case가 모든 client에 있다.—Evidence: docs/spec/dialects.md "Inside CREATE TABLE"는 MySQL varchar(n)과 text를 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin으로, table을 COLLATE=utf8mb4_0900_bin으로, PostgreSQL text column을 COLLATE "C"로, SQLite text를 기본 BINARY로 렌더링하며 tests/dbspec/ddl.json statement가 그것을 보여 준다. keys-indexes-foreign-keys vector에 orders.code unique key case를 더한다: a, A, á, a (끝 공백)는 네 행으로 들어가고, 두 번째 a는 실패하며(MySQL 1062, PostgreSQL 23505, SQLite UNIQUE), code = 'a'는 한 행과 맞고, A가 a와 á보다 앞에 정렬된다. 이전 utf8mb4_0900_ai_ci에서는 A insert가 실패하고(mysql.collation.default_accent_case_insensitive) PAD SPACE collation에서는 a 가 실패한다(mysql.collation.utf8mb4_bin_pads). make dbspec-ddl-check가 MySQL 8.4, PostgreSQL 17, SQLite에서 step 15개를 실행해 통과하고, client 네 개가 이 statement를 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, 그리고 네 client가 일치하는 make dbspec-compare-check), introspection이 collation을 다시 읽는다(make dbspec-introspect-check round trip).[o]
T8.0.9datetime(p)를 UTC 연결 규칙의 local date-time으로 생성한다(T8.0 선행). 원인: datetime은 MySQL DATETIME, PostgreSQL timestamp with time zone, SQLite 텍스트로 생성되며(internal/ormgen/ddl.go:487과 client 포팅) introspection이 복원할 수 없고 이제 docs/spec/dialects.md가 거부한다. SQLite =now 기본값은 UTC를 쓰지만(ddl.go:553, sqlite.datetime.now_is_utc) dialect 규칙은 SQLite date-time이 연결 시간대를 쓴다고 기록한다. 완료 기준: T8.3이 MySQL DATETIME(p), PostgreSQL timestamp(p), renderer CHECK를 가진 SQLite 텍스트를 생성하고, 모든 client가 각 연결에 UTC를 설정하며, introspection이 timestamptz와 MySQL TIMESTAMP를 미지원으로 보고하고, 세 데이터베이스의 추적 case가 모든 client에 있다. orm은 기존 timestamptz 컬럼을 미지원으로 보고하며 변환하지 않는다.—Evidence: tests/dbspec/ddl.json은 모든 client에서 datetime(p)를 MySQL DATETIME(p), PostgreSQL timestamp(p), SQLite 텍스트로 생성한다(T8.3, make dbspec-ddl-check). tests/dbspec/introspect.json은 모든 client에서 MySQL timestamp(6) 컬럼과 PostgreSQL timestamptz 컬럼을 미지원으로 보고한다(make dbspec-introspect-check와 PHP, TypeScript, Rust 변형). T8.0.9.1이 모든 Go, PHP, TypeScript, Rust 연결에 UTC를 설정하고 conformance가 세 데이터베이스에서 25 vectors × 4 languages identical을 보고한다.[o]
T8.0.9.1Go, TypeScript, Rust의 모든 connection이 UTC로 읽고 쓰게 한다 (T8.0.9 후속). Cause: PHP client는 모든 connection에 MySQL time_zone = '+00:00'과 PostgreSQL TIME ZONE 'UTC'를 두고 timezone으로 UTC나 +00:00만 받지만, Go, TypeScript, Rust client는 어떤 zone이든 받고 parameter가 없으면 server나 process의 zone을 쓴다. 그래서 MySQL과 PostgreSQL clock default가 session wall clock(시험 server에서 KST)을 쓰고, Go는 offset이 있는 값을 PostgreSQL timestamp에 bind해 PostgreSQL이 그 offset을 버린다(postgres.timestamp.ignores_offset).—Evidence: Go, TypeScript, Rust client는 이제 PHP가 이미 지키던 규칙을 따른다: MySQL connection은 time_zone = '+00:00', PostgreSQL connection은 TimeZone=UTC를 두고, 값을 UTC로 읽으며, offset이 있는 값은 UTC wall clock으로 bind한다(Go는 더 이상 timestamp가 offset을 버리는 time.Time을 PostgreSQL에 넘기지 않는다). timezone은 UTC나 +00:00만 받고 그 밖에는 CONFIG "dsn timezone <value>: every connection reads and writes datetime values in UTC"를 반환한다. 이름 있는 zone 처리, MySQL 1298 대응, chrono-tz dependency는 지운다. 시험 server(MySQL SYSTEM = KST, PostgreSQL Asia/Seoul)에서의 Red: Go TestParseDSN과 TestParseDSNRejectsInvalidInput은 case 5개와 3개에서 실패했고, TestConnectionsUseUTC는 parameter 없이 SQLite, MySQL, PostgreSQL에서 instant 2026-01-02 00:00+09:00을 2026-01-02 00:00:00 +0900 KST로, timezone=UTC인 PostgreSQL에서 2026-01-02 00:00:00 +0000 UTC로 읽었다. TypeScript clients/typescript/tests/dsn.mjs는 timezone=%2B09:00을 받았고 clients/typescript/tests/model.mjs(connectionsUseUtc)는 start_dt 2026-01-02 00:00:00.000000과 9시간 앞선 MySQL created_ts를 읽었다. Rust connections_use_utc와 non_utc_time_zones_are_rejected는 실패했다. Green: 이 test들이 parameter 없음, UTC, +00:00에서 통과하고, PHP connection time zone case와 clients/php/tests/dsn.php가 통과하며, go test ./clients/go/orm ./clients/go/model, Rust zone, value_decode, sqlite_file_name test, clippy, tsc가 통과하고, MySQL, PostgreSQL, SQLite conformance 실행이 25 vectors × 4 languages identical을 보고한다. docs/spec/config.md, interfaces.md, dsl.md, protocol.md, dialects.md, features.md, interface-implementation.md, usage.md가 규칙을 적는다.[o]
T8.0.9.2모든 client에서 SQLite DSN path를 percent-decode해서 읽는다 (T8.0.9.1 후속). Cause: Rust parse_dsn은 url.path()를 percent-decode하지 않고 읽어서, 공백이나 다른 escape 문자가 든 sqlite:// path는 다른 file을 연다. acceptance: escape된 path를 가진 공유 DSN case가 Go, PHP, TypeScript, Rust에서 같은 file을 연다.—Evidence: docs/spec/config.md "Runtime connection"이 규칙을 정한다: SQLite DSN path는 file을 열기 전에 UTF-8로 percent-decode하며(%25, %23, %3F는 %, #, ?), 뒤에 hexadecimal 두 자리가 없는 %, decode한 NUL byte, UTF-8이 아닌 decode 결과는 CONFIG를 반환한다. tests/dsn/sqlite-paths.json은 공유 case 11개(plain, space, escape한 percent, hash, question mark, UTF-8, query가 있는 space, 거부하는 path 4개)를 담고, 각 client는 public connect로 sqlite://<새 directory>/<path>에 연결해 case가 정한 file(과 journal file)만 있거나, error와 빈 directory를 요구한다: Go TestSQLitePathCases(clients/go/orm/sqlite_path_test.go), PHP clients/php/tests/dsn.php, TypeScript clients/typescript/tests/dsn.mjs, Rust sqlite_path_cases(clients/rust/orm/tests/sqlite_file_name.rs). Red: Go는 decode한 path를 file: URI로 SQLite에 넘겨 다시 decode되고 #, ?, NUL에서 잘렸으므로 escaped-percent는 a b.sqlite를, escaped-hash, escaped-question-mark, nul은 a를 열었고, invalid-utf8은 CONFIG가 아닌 unable to open database file (14)로 실패했다. PHP는 space에서 글자 그대로의 a%20b.sqlite를 열었다. TypeScript는 invalid-escape에서 CONFIG가 아닌 URIError: URI malformed를 던졌다. Rust는 11개 중 10개 case에서 실패해 escape한 path마다 decode하지 않은 file을 열고 잘못된 path 4개를 받아들였다. Green: Go는 NUL과 UTF-8이 아닌 path를 거부하고 decode한 path를 escape해 file: URI에 넣으며, PHP는 path를 검사하고 rawurldecode하고, TypeScript는 decode 실패와 NUL을 CONFIG로 바꾸는 함수 하나에서 decode하고, Rust는 sqlite_path에서 decode한다. 네 test가 11개 case를 모두 통과한다. go test ./clients/go/orm, cargo test -p orm --lib --test sqlite_file_name --test zone, cargo test -p orm-build --features cli --test tool_limits가 통과한다. 통과한 gate: client-unit-check, ts-check, rust-check, rust-fmt-check, go-fmt-check, interface-check, docs-rules-check, checklist-check.[o]
T8.0.10SQLite primary key nullability를 선언대로 import한다(T8.0 선행). 원인: 관찰 결과 ormgen import는 k TEXT PRIMARY KEY를 NOT NULL로 쓰지만(internal/ormgen/migrate.go:238) SQLite는 이 컬럼에 NULL을 받는다(sqlite.pk.nullable_non_integer). 완료 기준: T8.4가 pk에서 추론하지 않고 notnull을 읽으며 모든 client에 SQLite 추적 case가 있다.—Evidence: dbspec introspection은 모든 column에 SQLite pragma_table_xinfo.notnull을 읽고 pk에서 추론하지 않는다(docs/spec/dialects.md "Types and columns", sqlite.pk.nullable_non_integer). tests/dbspec/introspect.json에 sqlite-primary-key-null을 더한다: renderer 형식으로 "k" varchar(8) NULL을 primary key로 선언한 table kp는 [table, kp, kp]로 보고되고(dbspec key column은 null일 수 없으므로 TypeScript 이유는 "key: primary key column k is null"이다), "k" varchar(8) NOT NULL인 table kq는 primary key (k)와 함께 k varchar(8)로 복원된다. make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check, dbspec-introspect-rust-check가 Go, PHP, TypeScript, Rust에서 이 case를 통과한다.[o]
T8.0.11import가 다른 정의로 바꾸는 catalog 타입과 동작을 보고한다(T8.0 선행). 원인: PostgreSQL timestamp와 timestamptz, time과 timetz, char(n)과 varchar(n), real과 double precision, json/jsonb와 텍스트를 각각 한 타입으로 import하지만(internal/ormgen/import.go:845-877) 저장 값이 다르다(postgres.char.padded, postgres.float.real_single_precision, postgres.json.text_jsonb_normalized). Foreign key update 동작은 읽지 않으며 RESTRICT, NO ACTION, SET DEFAULT는 기본값으로 import한다(import.go:518-527, 관찰 결과 MySQL ON UPDATE CASCADE가 빠진다). 완료 기준: T8.4가 지원하는 정의는 정확히 복원하고 나머지는 미지원으로 보고하며 모든 client에 추적 case가 있다.—Evidence: dbspec introspection은 docs/spec/dialects.md "Types and columns"의 catalog 형식만 대응시키고, foreign key의 두 action을 모두 읽어 RESTRICT, CASCADE, SET NULL만 받는다. make dbspec-introspect-check round trip은 MySQL, PostgreSQL, SQLite에서 on delete set_null on update cascade와 on delete cascade on update restrict를 복원하므로 MySQL ON UPDATE CASCADE가 유지된다. tests/dbspec/introspect.json은 이제 나머지 정의를 미지원으로 보고한다: PostgreSQL timestamptz(at), timetz(tz), char(4)(ch), real(r), json(j), jsonb(jb)와 key fk_refs_a(ON DELETE NO ACTION), fk_refs_b(ON UPDATE SET DEFAULT); MySQL timestamp(6)(ts), char(4)(ch), float(fl), json(doc), NO ACTION key fk_c, SET DEFAULT key fk_refs_b; SQLite key fk_refs_a(NO ACTION)와 fk_refs_b(SET DEFAULT)이며, 각 table의 나머지(refs의 column, key, index)는 복원된다. make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check, dbspec-introspect-rust-check가 Go, PHP, TypeScript, Rust에서 이 case를 통과한다.[o]
T8.0.12PostgreSQL probe 증거를 완성한다(T8.0 선행). 원인: T8.0 진행 중 TEST_ENV의 PostgreSQL primary와 replica가 삭제된 경로에 설정된 data directory 때문에 멈췄다. 이후 운영자가 현재 경로에서 서버를 다시 시작했고 BENCH_SQLITE_DSN도 현재 경로를 쓴다.—증거: 다시 시작한 서버에서 PostgreSQL probe 75개가 모두 두 번 통과했다. 이전에 실행하지 않은 postgres.index.varchar_640_four_byte_fits, postgres.render.time_of_day_check, postgres.collation.c_codepoint의 COLLATE "C" catalog 단계도 포함하며 기록과 다른 PostgreSQL 사실은 없었다.[o]
T8.0.13DSN query를 이름에 포함한 SQLite 파일을 만든 opener를 찾는다(T8.0 선행). 원인: 이전 .runtime/servers에 빈 파일 orm_bench.sqlite?_pragma=busy_timeout(5000)&timezone=%2B00:00(2026-09-26)이 있었으므로 어떤 프로그램이 query를 파일 이름에 포함한 채 BENCH_SQLITE_DSN을 열었다. TestSQLiteFileNameWithQuery가 방식을 재현한다. PHP PDO, node:sqlite, sqlite3 shell은 그 이름의 파일을 만들고 modernc.org/sqlite는 query를 해석한다. 현재 Go, PHP, TypeScript, Rust 적합성 실행기와 Go, PHP, TypeScript import 도구를 같은 DSN 형식으로 bench 파일 사본에 실행했을 때 그런 파일은 생기지 않았으므로 현재 tree에서 해당 경로를 찾지 못했다. 완료 기준: 각 client의 DSN 테스트와 SQLite DSN을 여는 각 도구가 경로만 파일 이름이 됨을 확인하며, 찾은 opener는 추적 RED case와 함께 수정한다.—Evidence: 현재 opener 중 DSN query로 SQLite file 이름을 짓는 것은 없으며, 이제 각 opener가 이를 검사한다. 모든 case는 관찰한 file 이름 형식인 sqlite://<임시 directory>/named.sqlite?_pragma=busy_timeout(5000)&timezone=%2B00:00를 열어 쓰거나 연결한 뒤 닫고, directory에 named.sqlite가 있고 named.sqlite와 그 -journal, -wal, -shm file 외의 이름이 없기를 요구한다. client opener: Go TestSQLiteFileNameIsThePath(clients/go/orm/sqlite_file_name_test.go, orm.Connect와 schema install), PHP clients/php/tests/dsn.php(Orm::connect), TypeScript clients/typescript/tests/dsn.mjs(Db.connect), Rust clients/rust/orm/tests/sqlite_file_name.rs(Db::connect, bench/install과 Rust tool database도 Go와 Rust client로 이것을 쓴다). DSN을 직접 여는 schema tool: Go TestToolSQLiteFileNameIsThePath(internal/ormgen openToolDB), PHP clients/php/tests/schema_tool_test.php(SchemaImport::connect), TypeScript clients/typescript/tests/sqlite-auto-import.mjs(openToolDb), Rust clients/rust/orm-build/tests/tool_limits.rs의 tool_sqlite_file_name_is_the_path(tool_db::open). 모두 통과하며, 새 Go와 Rust file은 contracts/features.json의 dsn_connection feature에 올라 있다. 따라서 2026-09-26에 그 file을 만든 opener는 현재 tree에 없다.[o]
T8.0.14Go client에서 실패한 MySQL context 초기화를 전달한다(T8.0 선행). 원인: MySQL user variable은 COMMIT과 ROLLBACK 뒤에도 남고(mysql.context.user_variable_session_scope), Go client는 transaction 끝의 SET @`orm.<key>` = NULL 오류를 버린다(clients/go/orm/utils.go:152). 따라서 초기화가 실패하면 audit operation id가 오류 없이 pool 연결에 남는다. PHP, Rust, TypeScript는 오류를 전달한다. 완료 기준: 초기화가 실패하는 추적 Go RED case가 오류를 보고하고 다른 client도 같은 case를 유지한다.—Evidence: Go client는 이제 실패한 SET @`orm.<key>` = NULL reset을 돌려준다: clearLocals가 대응한 오류를 돌려주고, commit은 rollback한 뒤 그 오류를 돌려주며, callback 오류 뒤의 rollback은 Rust client가 이미 쓰던 형식인 CONFIG transaction failed (<callback error>) and rollback failed (<cleanup error>)를 돌려준다. Go TestTransactionReportsFailedLocalReset(clients/go/orm/context_reset_test.go)는 MySQL driver를 감싸 reset만 실패시킨다. 수정 전에는 "commit: want the reset error, got <nil>"로 실패했고, 수정 후 MySQL에서 통과한다. 같은 case로 PHP는 callback 오류를 reset 오류로 바꾸고 TypeScript는 rollback 때 reset 오류를 버린다는 것을 찾았다: clients/php/tests/model_test.php(failed local reset/mysql, reset만 거부하는 PDO)는 "rollback reports the callback and the failed reset: reset rejected by the test driver"로, clients/typescript/tests/model.mjs(mysql/failedLocalReset, reset만 거부하는 transaction control)는 "…: callback failed"로 실패했다. 둘 다 이제 두 오류를 CONFIG 형식으로 보고하고 통과한다. Rust tx::tests::failed_local_reset_is_reported는 transaction connection을 끊고 finish와 rollback이 실패한 reset을 보고하는지 검사한다. go test ./clients/go/orm -run 'Transaction|Local|Cancel', go test ./clients/go/model, SQLite, MySQL, PostgreSQL에서의 PHP와 TypeScript model test, Rust tx::tests:: test가 통과하고, docs/interfaces.md가 규칙을 적는다. Go client가 아직 버리는 transaction 끝의 나머지 결과는 T8.0.14.1이다.[o]
T8.0.14.1Go client가 transaction 끝의 모든 오류를 보고하게 한다 (T8.0.14 후속). Cause: T8.0.14 뒤에도 Go client는 releaseLocks의 SELECT RELEASE_LOCK(?)(clients/go/orm/utils.go), abort의 tx.Rollback(), rollback의 finishSQLiteMode(clients/go/orm/tx.go), panic하거나 끝나 버린 callback이 일으키는 rollback의 결과를 버린다. PHP, TypeScript, Rust client는 실패한 RELEASE_LOCK과 rollback을 전파한다.Acceptance: 이 오류들이 호출자나 panic에 닿고, Go의 tracked Red case와 다른 client의 같은 case가 있다.Evidence: Go client는 이제 transaction 끝의 모든 실패를 보고한다. releaseLocks(clients/go/orm/utils.go)는 RELEASE_LOCK 결과를 읽고, 실패한 statement와 1이 아닌 결과를 CONFIG lock <key> was not held at transaction end로 돌려준다. rollback(clients/go/orm/tx.go)은 finishSQLiteMode, releaseLocks, clearLocals, native rollback을 모두 실행하고 모든 실패를 합친다. commit, begin, callback 실패는 rollbackAfter를 거쳐 rollback도 실패하면 CONFIG transaction failed (<cause>) and rollback failed (<transaction end error>)를 돌려준다. panic한 callback과 runtime.Goexit로 떠난 callback은 rollback이 실패하면 이 오류로 panic하고, 아니면 원래 panic 값을 유지한다. beginSQLiteMode는 첫 PRAGMA 전에 mode를 표시하므로 PRAGMA read_uncommitted = 1 뒤에 실패한 begin도 그것을 되돌린다. transaction은 pool connection을 *sql.Conn으로 잡고, driver가 닫은 connection의 native rollback은 완료다. TestWithContextCancelsInsideTransaction에서 transaction 안의 statement가 취소되면 두 driver 모두 connection을 닫았으므로(MySQL rollback은 "context canceled", PostgreSQL은 "conn closed"를 돌려줬다) rollback은 driver.ErrBadConn(clients/go/orm/pg가 pgconn.ErrConnClosed를 이것으로 대응한다)과 driver.Validator를 확인하고, 취소된 statement는 계속 CANCELED를 돌려준다. Go TestTransactionReportsFailedLockRelease, TestTransactionReportsFailedRollback, TestTransactionReportsFailedSQLiteModeReset(clients/go/orm/transaction_end_test.go)는 MySQL이나 SQLite driver의 고른 statement나 rollback을 거부하는 test driver(clients/go/orm/failing_driver_test.go)를 쓴다. 수정 전에는 11개 subtest 중 9개가 "commit: want the release error, got <nil>", "panic: want CONFIG … got <nil>", "rollback: want CONFIG … got callback failed" 등으로 실패했고, 수정 후 11개 모두 통과한다. 같은 case가 다른 client에서도 같은 빈틈을 찾았다. PHP(clients/php/tests/model_test.php의 failed lock release/mysql, failed rollback/sqlite, 고른 statement나 rollback을 거부하는 PDO)는 "a lock released early is reported: no error"와 "begin reports the cause and the failed transaction end: rollback rejected by the test driver"로 실패했다. Db::finish는 이제 모든 정리 단계를 실행하고 RELEASE_LOCK 결과를 확인하며 정리와 rollback 실패를 보고하고, 실패한 begin은 mode를 되돌리고 실패한 rollback을 원인과 함께 보고한다. TypeScript(clients/typescript/tests/model.mjs의 mysql/failedLockRelease, sqlite/failedRollback)는 "a lock released early is reported: no error"와 "commit reports the failed mode reset: DRIVER: sqlite: cannot start a transaction within a transaction" 등 5개 check가 실패했다. 실패한 begin PRAGMA가 SQLite transaction을 열어 둔 탓이다. Db.finish와 SQLite, PostgreSQL driver는 이제 모든 단계를 실행하고 실패한 begin을 rollback하며 rollback 실패를 버리지 않고 보고한다. Rust(clients/rust/orm/src/tx.rs의 tx::tests::)는 lock_not_held_at_transaction_end_is_reported, every_failed_cleanup_step_is_reported("left: 1, right: 2"), panicking_callback_ends_its_transaction("the panicked transaction released its lock: left: Some(0), right: Some(1)")이 실패했고, SQLite panic case는 "the panic carries the transaction failure"로 실패했다. finish는 이제 모든 단계를 실행하고 RELEASE_LOCK 결과를 확인하며, panic한 callback은 panic을 이어 가기 전에 rollback한다. sqlite_transaction_end_failures_are_reported는 SQLite authorizer로 ROLLBACK과 PRAGMA statement를 거부한다. go test ./clients/go/..., PHP model test(11 tests × 3 databases), SQLite, MySQL, PostgreSQL에서의 TypeScript model test, Rust tx::tests:: 9개가 통과하고, docs/interfaces.md가 규칙을 적는다. 찾은 결함은 T8.0.14.2, T8.0.14.3, T8.0.14.4다.[o]
T8.0.14.2버려진 Rust transaction connection을 TLS에서도 닫는다. Cause: CancellableConnection(clients/rust/orm/src/driver.rs)은 완료되지 않은 transaction connection을 spawn한 task의 close_hard로 닫는다. TLS로 연결한 MySQL test server에서 이 close는 시작한 뒤 5초가 지나도 끝나지 않았고 session은 named lock을 계속 가졌다. 따라서 실패한 begin, 실패한 commit 정리, 버려진 transaction future가 열린 transaction과 lock을 server에 남길 수 있다. acceptance: drop된 Rust transaction future가 TLS에서도 server session과 named lock을 끝내며, 추적되는 Red case가 있다.—Evidence: sqlx의 close_hard는 shutdown 전에 flush하고, sqlx-core 0.9.0의 rustls stream은 보낼 data가 없으면 server가 data를 보낼 때까지 읽으므로, spawn한 TLS connection close가 끝나지 않았다. CancellableConnection(clients/rust/orm/src/driver.rs)은 이제 떼어낸 connection을 바로 drop한다. MySQL과 PostgreSQL에서는 socket이 바로 닫히고, SQLite에서는 SQLite의 close_hard와 같이 handle이 닫힌다. spawn한 task와 버려지던 close 결과는 없어졌다. Rust tx::tests::dropped_transaction_closes_its_connection은 transaction에서 named lock을 잡고 transaction future를 drop한 뒤 다른 connection에서 그 lock을 잡으며, MySQL connection이 TLS를 쓰는지(Ssl_cipher가 비어 있지 않은지) 확인한다. 수정 전에는 MySQL에서 두 번(5.02 s, 5.03 s) "mysql: the dropped transaction released its lock: left: Some(0), right: Some(1)"로 실패했고(GET_LOCK(?, 5) 시간 초과), 수정 후 MySQL GET_LOCK은 1을 돌려주고 PostgreSQL lock_timeout = '5s'의 pg_advisory_xact_lock과 SQLite busy_timeout = 5000의 BEGIN IMMEDIATE가 성공하며 넣은 row는 rollback되었다. cargo test -p orm --lib의 28개 test가 통과하고 docs/interfaces.md가 규칙을 적는다.[o]
T8.0.14.3savepoint 끝의 모든 오류를 보고한다. Cause: Go savepoint(clients/go/orm/tx.go)는 panic, runtime.Goexit, callback 오류 뒤의 ROLLBACK TO SAVEPOINT와 RELEASE SAVEPOINT 결과를 버리고, PHP와 TypeScript savepoint는 실패한 savepoint rollback으로 callback 오류를 바꾼다. acceptance: 모든 client에서 savepoint 끝의 모든 오류가 호출자나 panic에 전달되며, 추적되는 Red case가 있다.—Evidence: 이제 모든 client는 callback 오류, panic, Go runtime.Goexit 뒤에 ROLLBACK TO SAVEPOINT와 RELEASE SAVEPOINT를 모두 실행하고, 둘 중 하나의 실패를 CONFIG transaction failed (<cause>) and rollback failed (<savepoint end error>)(panic이면 panic 값)로 보고하며, 성공한 callback 뒤에 실패한 RELEASE SAVEPOINT를 돌려준다: Go savepoint, rollbackSavepoint, endSavepoint(clients/go/orm/tx.go), PHP Db::savepoint와 Db::endSavepoint, TypeScript Database.savepoint, 그리고 transaction, transaction_send, transaction_once가 쓰는 Rust run_savepoint와 rollback_savepoint(clients/rust/orm/src/tx.rs)다. Rust savepoint도 callback 오류 뒤의 RELEASE SAVEPOINT 결과를 버렸고 panic에서는 savepoint를 끝내지 않았다. 이제 panic을 잡아 savepoint를 끝내고 panic을 이어 간다. 취소된 transaction context나 driver가 닫은 connection으로 이미 끝난 transaction의 Go savepoint는 transaction과 함께 끝났다. Go TestSavepointReportsFailedEnd(clients/go/orm/transaction_end_test.go, SQLite test driver가 고른 statement를 거부한다)는 수정 전 8개 subtest 중 7개가 실패했고, 그중에는 "callback error: want CONFIG … got statement rejected by the test driver"와 "panic: … got <nil>"이 있다. TestWithContextCancelsInsideTransaction에 더한 중첩 case는 닫힌 connection의 실패한 ROLLBACK TO SAVEPOINT가 CANCELED를 대신해서 MySQL과 PostgreSQL에서 "blocked read: driver: bad connection (code "")"로 실패했다. row를 잡기 전에 끝난 holder transaction은 이제 그 test를 멈추게 하지 않고 실패시킨다. PHP failed savepoint end/sqlite(clients/php/tests/model_test.php)와 TypeScript sqlite/failedSavepointEnd(clients/typescript/tests/model.mjs)는 각각 4개 check가 실패했고, 그중에는 "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(SQLite authorizer가 savepoint statement를 거부한다)는 "callback error 4: sqlx: error returned from database: (code: 23) not authorized"로 실패했다. 두 authorizer test는 process 전체의 거부 설정을 함께 써서 같이 실패했으므로 이제 하나의 lock을 잡는다. 수정 후 go test -p 1 ./clients/go/..., PHP model test(11 tests × 3 databases), make ts-check, cargo test -p orm --lib의 29개 test가 통과한다. 동시에 실행된 다른 run이 공유 database를 쓰고 있어 MySQL과 PostgreSQL run은 test server의 전용 database를 썼다. docs/interfaces.md가 규칙을 적는다.[o]
T8.0.14.4취소된 Go transaction의 MySQL session 상태를 지운다. Cause: Go transaction의 context가 취소되면 database/sql이 직접 transaction을 rollback하고, driver.SessionResetter와 driver.Validator를 구현한 go-sql-driver connection을 유지한다. 그래서 releaseLocks와 clearLocals가 그 connection에서 실행되지 못하고 named lock과 user variable이 pool connection에 남을 수 있다. database/sql을 이렇게 읽은 것이며 아직 실행한 case는 없다. acceptance: 추적되는 Go case가 취소된 transaction이 session 상태를 남기는지 보이고, 재현된 결함은 고친다.—Evidence: 결함이 재현되었다. Go TestCancelledTransactionClearsSessionState(clients/go/orm/cancelled_session_test.go)는 connection이 하나인 pool에서 callback이 named lock과 local 값(SQLite에서는 read-only transaction)을 둔 뒤 transaction의 context를 취소한다. 수정 전에는 releaseLocks, clearLocals, finishSQLiteMode가 취소된 context로 실행되어 모든 subtest가 "want CANCELED alone, got CONFIG: transaction failed (CANCELED: context canceled) and rollback failed (CANCELED: …)"를 돌려주었다. MySQL에서는 다른 connection의 GET_LOCK(?, 5)가 0을 돌려주었고 다음 transaction이 user variable "tester"를 읽었다. SQLite에서는 query_only = 1이 pool connection에 남아 다음 transaction이 "READ_ONLY: attempt to write a readonly database (8)"로 실패했다. rollback(clients/go/orm/tx.go)은 이제 취소된 transaction의 connection을 pool에 돌려주지 않고 closeSession으로 닫으므로, server가 session과 함께 lock, variable, mode를 끝내고 transaction은 CANCELED만 돌려준다. 수정 후 세 subtest가 두 번의 실행에서 통과하고, go test -p 1 ./clients/go/...가 test server의 전용 MySQL과 PostgreSQL database에서 통과하며, docs/interfaces.md가 규칙을 적는다.[o]
T8.1dbspec을 docs/spec/dbspec.md와 한국어 짝 문서에 명세한다(T8.0 선행). 완료 기준: grammar, 정의·settings·diagram 분리, schemaHash/manifestHash 소속을 적은 settings key 표, neutral type과 식 집합, 이름 교정표, 문서 간 import, canonical order, 한도, diagnostic, 기존 모든 schema 기능의 유지/이동/제거 판정(dialect SQL 원문을 가진 PhysicalGraph record 포함), 공유 vector와 client별 budget.—근거: docs/spec/dbspec.md와 한국어 짝 문서가 header와 문서 이름, 파일 경로 없이 선언된 문서 집합을 쓰는 use, 이름, column, 열세 가지 neutral type과 렌더링, key, index, foreign key, neutral check 식, schemaHash 또는 manifestHash 소속을 적은 setting 열 가지, operation column과 이력 table을 쓰는 audit(session 변수, transaction setting, context table을 쓰지 않으며 physical delete는 거부), diagram, literal 형식을 포함한 canonical form, 한도, 위치가 있는 diagnostic 규칙 열여섯 가지, 바뀐 이름, PhysicalGraph dialect text record와 주석 달린 Markdown 경로 제거를 포함한 모든 Mermaid 기능의 유지/이동/제거 판정을 정의한다. docs/spec/dialects.md가 audit 판정을 기록한다. tests/dbspec/cases.json은 canonical 4개, 정규화 2개, 잘못된 문서 17개의 공유 case와 계산한 error 위치를 갖는다. Rust parse budget은 2000-table case에서 300 ms이고 다른 client는 첫 측정에서 정한다. make docs-check, make docs-rules-check, make checklist-check가 통과한다.[o]
T8.1.1TypeScript, PHP, Rust parser가 서로 다르게 읽은 dbspec 규칙을 확정한다(T8.1 후속). 원인: 세 구현이 docs/dbspec.md가 말하지 않거나 여러 뜻으로 읽히는 곳 약 30군데를 보고했고 서로 다르게 정했다(예: 열의 단위, table 줄 순서, 닫는 괄호 앞과 문서 끝의 comment, f64와 decimal literal, use 순환, setting 반복, diagram 좌표 범위, 멈추는 error와 diagnostic 순서).—근거: docs/dbspec.md와 한국어 짝 문서가 이제 각 지점을 정한다(예약어, tab, 줄 끝, header 위치, constraint 순서, 닫히지 않은 block, 수식어와 action 순서, literal 형식, codec 저장 type, aes_version과 blind_index 규칙, audit table 형태, use 해석, table 이름과의 이름 범위, 적힌 순서, comment 위치, error 위치와 순서). tests/dbspec/cases.json에 이를 고정하는 canonical 1개, 정규화 4개, 잘못된 문서 16개를 더했다(합계 5, 6, 33). make docs-rules-check와 make checklist-check가 통과한다. client 항목들은 parser를 이 case에 맞춘다.[o]
T8.1.2Go parser가 열린 채로 보고한 dbspec 규칙을 확정한다(T8.1.1 후속). 원인: docs/dbspec.md가 반복된 blind_index 줄의 순서, 실패한 key나 index 줄이 가리는 규칙, tab으로 실패한 줄이 종류를 유지하는 방식을 말하지 않았다. Go는 한 가지로 읽었고, PHP와 TypeScript는 blind_index 줄을 적힌 순서로 두었으며 실패한 key 줄 뒤에 primary key 없음, identity 규칙, 선두 index 오류, 참조 key 오류를 보고했다.—근거: docs/dbspec.md와 한국어 짝 문서가 blind_index 줄을 AES column 순으로 정렬하고, 실패한 줄은 종류와 이름을 유지하며 그 column에 기대는 규칙을 가린다고 정한다. tests/dbspec/cases.json에 blind-index-sorted, failed-primary-key-line, failed-index-line-under-foreign-key, failed-target-key-line, tab-in-primary-key-line, leading-tab-column-line을 더했다(5, 7, 38 case). client 수정 전에는 PHP와 TypeScript가 실패했고, 이제 make dbspec-go-check, make dbspec-php-check, make dbspec-ts-check가 50 case를 모두 통과하며, 새 PHP와 TypeScript symbol을 기록한 뒤 interface-check가 go, php, typescript에서 통과한다.[o]
T8.1.3header error의 위치를 확정한다(T8.1.2 후속). 원인: docs/dbspec.md가 정확한 형식 dbspec 1 <name>을 요구하면서 그 error의 위치를 정하지 않았다. Go, PHP, TypeScript는 남는 단어를, Rust는 그 앞 공백을 위치로 썼고, TypeScript는 header의 이중 공백을 받아들였다.—근거: docs/dbspec.md와 한국어 짝 문서가 header error의 위치를 dbspec 1 <name>(<name>은 ASCII 문자, 숫자, _의 연속)에서 처음 벗어나는 문자, 또는 줄 끝 다음 열로 정한다. tests/dbspec/cases.json에 header-extra-word, header-double-space, header-tab, header-version, header-missing-name, header-leading-space, header-double-space-before-version을 더했다(5, 7, 45 case). 변경 전에는 Go, PHP, TypeScript가 header-extra-word에서, TypeScript는 이중 공백 case에서도 실패했다. 이제 네 client check(make dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check)와 네 언어 interface-check가 통과한다.[o]
T8.1.4세 database가 같게 평가하도록 check predicate에 type 규칙을 둔다(T8.1.3 후속). 원인: docs/dbspec.md가 +, -, *, /와 모든 column에 대한 모든 literal을 허용했지만, 정수 나눗셈(MySQL 2.5, PostgreSQL과 SQLite 2), 정수 overflow(SQLite에서 REAL), scale을 곱한 SQLite decimal은 다른 결과를 내며, 어떤 client도 operand type을 확인하지 않았다.—근거: docs/dbspec.md와 한국어 짝 문서가 predicate 형식(column이 있는 비교, in, between, is null, bool column, not, and, or, 괄호)을 정하고, 산술, 함수, null literal, bytes column을 없애며, column과 만나는 literal을 그 column type의 유효한 default로 하고 canonical default 형식으로 쓰게 하고, 만나는 column type을 나열하고, diagnostic 순서(읽기 먼저, 그다음 source 순서의 column과 type)를 정한다. tests/dbspec/cases.json에 canonical check-typed-predicates, normalize check-literals-take-column-form, invalid check-* case 26개를 더했고, every-line-kind는 이제 total >= 0.00을 쓴다(6, 8, 71 case). Go, PHP, TypeScript, Rust는 변경 전 처음 20개의 새 case에서 실패했고 이제 모든 case를 통과한다. 각 rule test는 산술을 쓰던 case를 바꾸고 type case를 더했다. make dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check(8번의 실행이 89 case에서 일치)가 통과한다. stress median은 Go 45 ms, PHP 205-211 ms, TypeScript 87-90 ms, Rust 30 ms다. symbol을 기록한 뒤 gofmt, rustfmt, clippy, 네 언어 interface-check가 통과한다.[o]
T8.1.5renderer가 생성하는 이름을 63 bytes 이내로 제한한다(T8.1.4 후속). 원인: docs/spec/dialects.md "Rendered statements"는 renderer CHECK를 <table>$<column>, trigger를 <table>$<event>로 이름 짓는데, 유효한 table과 column 이름에서도 63-byte identifier 한도를 넘을 수 있고, renderer는 이름을 줄이면 안 된다.—근거: docs/dbspec.md와 한국어 짝 문서가 "Names"에 생성 이름을 나열하고, 63 bytes를 넘는 이름을 column 이름이나 immutable, audit keyword에서 name.length error로 한다. tests/dbspec/cases.json에 canonical generated-names-at-63-bytes와 invalid case 3개(64 bytes인 check, immutable, audit 이름)를 더했다. Go, PHP, TypeScript, Rust는 변경 전 invalid case에서 실패했고 이제 모든 case를 통과한다(7, 8, 74 case). dbspec target 다섯 개, gofmt, rustfmt, clippy, 네 언어 interface-check가 통과한다. stress median은 load average 8.6에서 올랐다. 같은 load에서 Go stress는 변경 전 62-65 ms, 변경 후 53-63 ms였으므로 이 규칙이 parse 시간을 측정 가능하게 늘리지 않는다.[o]
T8.1.6and, or, not, in, between, is를 예약어로 한다(T8.1.4 후속). 원인: 이 단어들은 predicate keyword로 읽히지만 유효한 table과 column 이름이어서, and라는 column에 대한 check ck (and > 0)을 PHP parser는 column으로 읽고 Go parser는 거부한다. 예약하지 않으면 predicate 문법이 모호하다.—근거: docs/dbspec.md와 한국어 짝 문서가 and, or, not, in, between, is를 예약어에 더하고, 네 client가 이를 예약어 목록에 더한다. tests/dbspec/cases.json에 invalid reserved-predicate-keywords(or라는 table, between이라는 column, 둘 다 name.format)를 더했고, Go parser는 변경 전 이를 유효한 문서로 돌려주었다(다른 client는 함께 바꾸었고 변경 전에는 실행하지 않았다). 이제 네 client에서 모든 case가 통과한다. PHP check reader와 renderer는 table에 그 column이 있으면 keyword를 column으로 읽던 분기를 뺀다. 예약어 때문에 도달할 수 없는 분기다. dbspec target 다섯 개, rustfmt, 네 언어 interface-check가 통과한다.[o]
T8.1.7introspection이 복원하는 check predicate 형식만 남긴다(T8.1.4 후속). 원인: probe가 보여 주듯 MySQL은 NOT을 그것이 부정하는 비교로 바꿔 쓰고 bool column 하나를 CHECK로 거부하며(3812), PostgreSQL은 BETWEEN을 비교 두 개로 쓴다. 그래서 not, between, column 하나가 있는 문서는 catalog에서 복원할 수 없다. 두 catalog는 canonical form이 적힌 대로 두는 괄호도 더한다.—근거: dialect probe 6개가 catalog 동작을 기록하고 MySQL 8.4.11과 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, postgres.check.not_kept. docs/dialects.md의 CHECK 식 행이 이를 인용한다. docs/dbspec.md와 한국어 짝 문서는 비교, [not] in, is [not] null, and, or, 괄호를 남기고, not, between, column 하나를 이유와 함께 없애며, 읽기 위치를 정하고, canonical form이 and 안의 or에 필요한 괄호만 남기게 한다. tests/dbspec/cases.json은 check-typed-predicates를 다시 쓰고 normalize check-redundant-parentheses와 invalid check-not-operator, check-between, check-bool-column-alone, check-literal-before-not을 더했다(7, 9, 79 case). tests/dbspec/ddl.json은 checks-in-dialect-form을 다시 썼고, 그 case의 statement만 바뀌었으며 세 database에 계속 적용된다. Go, PHP, TypeScript, Rust는 변경 전 처음 네 개의 새 case에서 실패했다. check-literal-before-not은 TypeScript 변경에서 Go가 not 대신 그 literal을 보고한다는 것이 드러난 뒤 더했고 Go를 고쳤다. parser, typer, emitter, renderer는 없앤 형식을 지운다. dbspec target 다섯 개, make dbspec-ddl-check, gofmt, rustfmt, 네 언어 interface-check가 통과한다.[o]
T8.1.8audit이 soft_delete 없이 성립하게 한다 (T8.1.7 후속). Cause: audit은 introspection이 trigger에서 다시 읽는 schema setting이고, soft_delete는 어느 database에도 없는 manifest setting이다. 이를 요구하면 introspect한 모든 audit table이 잘못된 문서가 된다. physical delete는 이미 BEFORE DELETE trigger가 실패시킨다.—Evidence: canonical vector audit-without-soft-delete는 네 client에서 soft_delete diagnostic으로 실패했고, Go settings.go, PHP Parser.php, TypeScript parse.ts, Rust validate.rs에서 rule을 제거한 뒤 통과한다. client rule test에서 그 diagnostic을 뺐다. make dbspec-compare-check는 115 case에서 8 run 일치를 보고한다. TypeScript symbol을 다시 기록해 interface check가 통과한다.[o]
T8.2모든 client에 dbspec parse와 canonical emit을 구현한다(T8.1 선행). 완료 기준: canonical 입력에서 emit(parse(s)) == s, 위치가 있는 diagnostic, 2000-table/60000-column/10000-FK case가 budget 안, Mermaid source 경로와 T7.17.2.10.x 문서 경로 제거, 언어 공통 CLI 이름 하나.—Evidence: T8.2.1에서 T8.2.6까지 끝났다. 모든 client가 공유 case에서 emit(parse(s)) == s와 위치를 가진 diagnostic으로 dbspec을 parse하고 emit한다. 2000-table stress 문서가 make dbspec-<lang>-check에서 각 client budget을 통과한다. make dbspec-compare-check가 네 client가 같음을 확인한다. Mermaid source 경로와 T7.17.2.10.x 문서 경로를 제거했고 CLI 이름은 하나다.[o]
T8.2.1Rust client에 dbspec parse, 검증, canonical emit을 구현한다(T8.1 선행). 완료 기준: tests/dbspec/cases.json의 모든 case 통과, 2000-table/60000-column/10000-foreign-key case를 release mode에서 300 ms 안에 parse하고 두 번 같게 emit, error는 규칙·줄·열을 갖는다.—근거: orm-schema/src/dbspec가 contracts/interfaces.json이 선언한 signature로 parse, emit, Diagnostic을 구현한다. module이 없을 때 Red test가 실패했다. make dbspec-rust-check를 두 번 실행한다. tests/dbspec.rs가 공유 case 50개(canonical 5, normalize 7, invalid 38)를, tests/dbspec_rules.rs가 규칙 case 52개, comment 부착, 한도를 통과한다. T8.1.2의 case 6개는 blind_index 정렬과 실패한 key 규칙을 넣기 전에 실패했다. release stress example은 다섯 번 parse하며 darwin arm64의 rustc 1.98.1에서 median 29-31 ms(min 28 ms, max 42 ms), emit 8-9 ms로 300 ms budget 안에 들고, 두 번 모두 문서를 그대로 emit한다. orm-schema에서 cargo fmt --check와 cargo clippy -D warnings가 통과한다. contracts/symbols/rust.json이 이제 native symbol과 같다: 715개 추가, 10개 변경, 그리고 T7.17.1가 snapshot을 갱신하지 않은 채 지운 orm-gen db.rs와 introspect.rs의 37개 제거. go run ./tests/interfaces/check가 네 언어 모두에서 통과한다.[o]
T8.2.1.1Rust client가 Go, PHP, TypeScript처럼 parse한 dbspec model을 공개한다 (T8.2.1 후속). Cause: Rust model의 type과 field가 pub(crate)여서 crate 밖 도구는 table, column, key, diagram을 읽거나 바꿀 수 없었다. Go model은 field를 export하고, PHP model은 public property를 가지며, TypeScript client는 model type을 export한다.—Evidence: orm_schema::dbspec::model은 type과 field가 공개된 public module이며 literal의 Value를 함께 둔다. clients/rust/orm-schema/tests/dbspec_model.rs는 전에 build되지 않았고(module model is private, field tables of struct Document is private), 이제 parse한 문서를 읽고 column을 더해 다시 parse되는 canonical text를 emit한다. make dbspec-rust-check가 이를 실행한다. contracts/symbols/rust.json이 새 public symbol을 기록하고 interface-check가 통과한다.[o]
T8.2.2Go client에 dbspec parse, 검증, canonical emit을 구현한다(T8.1 선행). 완료 기준: 모든 공유 case 통과, 2000-table case를 측정하고 budget을 기록한다.—근거: engine/dbspec가 contracts/interfaces.json이 선언한 signature로 Parse, Emit, Diagnostic을 구현한다. package가 없을 때 Red test가 실패했다. go test ./engine/dbspec가 subtest 145개를 통과한다: 공유 case 44개(canonical 5, normalize 6, invalid 33), 규칙 case 86개, encoding과 한도 case 8개, canonical form 7개. 부하 문서는 두 번 모두 그대로 emit되며 darwin arm64의 go1.27.0에서 parse 48-53 ms, emit 6-10 ms로 T8.2.4.1의 100 ms budget 안에 든다. interface-check가 go에서 통과한다.[o]
T8.2.2.1primary key 줄이 없는 table에서도 Go가 identity 규칙을 보고한다 (T8.2.2 후속). Cause: Go는 primary key가 있는 table의 분기 안에서만 identity 규칙을 검사해서 table t { id i64 identity }에 key만 보고했다. docs/dbspec.md는 identity column이 유일한 primary key column이기를 요구하고 실패한 primary key 줄만 그 규칙을 가린다. PHP, TypeScript, Rust는 identity에 column도 보고하며, 이를 덮는 공유 case가 없어서 네 client 비교가 차이를 보지 못했다.—Evidence: tests/dbspec/cases.json에 invalid case identity-without-primary-key(key 3:7, column 4:10)를 더했다. Go TestSharedVectors는 이 case에서 key만 내며 실패했고, validate.go가 primary key 줄 없는 table에도 identity 규칙을 보고하자 통과한다. plan parse case target-diagnostic-located는 이제 두 diagnostic을 적는다. make dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check가 통과한다.[o]
T8.2.3PHP client에 dbspec parse, 검증, canonical emit을 구현한다(T8.1 선행). 완료 기준: 모든 공유 case 통과, 2000-table case를 측정하고 budget을 기록한다.—근거: clients/php/src/Dbspec가 contracts/interfaces.json이 선언한 그대로 Orm\Dbspec\Dbspec::parse, ::emit과 diagnostic record를 구현한다. class가 없을 때 Red test가 실패했다. make dbspec-php-check는 각 test를 두 번 실행한다. 공유 case 44개(canonical 5, normalize 6, invalid 33)와 집중 case 57개가 통과하고, 부하 문서는 두 번 모두 그대로 emit되며 darwin arm64의 PHP 8.5.10에서 parse 218-308 ms, emit 24-32 ms, 128 MiB 안의 peak 57 MiB로 T8.2.4.1의 400 ms budget 안에 든다. interface-check가 php에서 통과한다.[o]
T8.2.4TypeScript client에 dbspec parse, 검증, canonical emit을 구현한다(T8.1 선행). 완료 기준: 모든 공유 case 통과, 2000-table case를 측정하고 budget을 기록한다.—근거: clients/typescript/src/dbspec(index, model, parse, emit)가 contracts/interfaces.json이 선언한 그대로 parseDbspec, emitDbspec, DbspecDiagnostic을 구현한다. module이 없을 때 Red vector test가 ERR_MODULE_NOT_FOUND로 실패했다. make dbspec-ts-check가 89개 test를 통과한다: 공유 case 44개(canonical 5, normalize 6, invalid 33)와 집중 case 45개. 부하 문서(2,490,985 bytes)는 두 번 모두 그대로 emit되며 Node 26.8.1, darwin arm64에서 parse 105-156 ms, emit 15-26 ms다. TypeScript parse budget은 160 ms다. contracts/symbols/typescript.json이 dbspec symbol 279개를 기록하고 hash를 갱신했다. interface-check는 TypeScript dbspec 실패를 보고하지 않으며, 기록되지 않은 physical_*와 introspect symbol 164개는 이전부터 남아 있고 T8.2.6에서 사라진다.[o]
T8.2.4.1client budget과 symbol snapshot을 사실과 맞춘다(T8.2.4 후속). 원인: T8.2.4가 어떤 test도 assert하지 않고 docs/dbspec.md도 정하지 않은 TypeScript budget 160 ms를 근거로 들었고, snapshot이 기록하지 않은 TypeScript 164개, PHP 107개, Go 514개 symbol 때문에 interface-check가 실패하는 상태로 commit했다.—근거: docs/dbspec.md와 한국어 짝 문서가 budget(Rust 300 ms, Go 100 ms, TypeScript 250 ms, PHP 128 MiB 안에서 400 ms)을 정하고, Go, TypeScript, PHP stress test가 이를 넘으면 실패한다. Go test는 1 ms budget에서 실패했고(parse 50 ms) 100 ms에서 통과한다(52 ms). TypeScript는 121-132 ms, PHP는 222-234 ms와 peak 57 MiB로 통과한다. contracts/symbols/{go,php,typescript}.json이 T8.2.6이 제거할 physical_* 를 포함한 모든 native symbol을 기록하고, go run ./tests/interfaces/check -language가 go, php, typescript에서 통과한다.[o]
T8.2.4.2parse budget을 parse 다섯 번의 median으로 판정한다(T8.2.4.1 후속). 원인: TypeScript stress test가 parse 한 번을 쟀다. 다른 test binary가 core 하나를 다 쓰는 동안 parse 한 번이 250 ms budget에 대해 602 ms가 걸렸고, 이어진 11번은 111-131 ms였다. 즉 느려진 parse 한 번이 결과를 정했다.—근거: docs/dbspec.md와 한국어 짝 문서가 다섯 번의 median을 정한다. Go, TypeScript, PHP stress test가 다섯 번 parse해 가장 짧은 시간, median, 가장 긴 시간을 출력하고 median이 budget을 넘으면 실패한다. 같은 부하에서 median은 Go 45-47 ms(부하가 큰 한 번 80 ms), TypeScript 94-107 ms, PHP 208-220 ms(peak 59 MiB)였다.[o]
T8.2.5공유 dbspec case에서 네 client를 하나의 추적되는 target으로 비교한다(T8.2.1-T8.2.4 선행). 완료 기준: target이 각 client를 두 번 실행하고 emit이나 diagnostic이 하나라도 다르면 실패한다.—근거: make dbspec-compare-check가 TypeScript client와 release Rust example을 build한 뒤 tests/dbspec/compare/go, php.php, typescript.mjs, orm-schema/examples/dbspec_compare.rs를 각각 두 번 실행한다. 각 runner는 공유 case 57개와 부하 문서에 대해 emission이나 rule line column diagnostic을 출력하고, check.mjs는 첫 Go 실행과 다른 첫 줄에서 실패한다. 비교 test(check.test.mjs)는 compare.mjs가 없을 때 ERR_MODULE_NOT_FOUND로 실패했고 이제 4 case를 통과한다. TypeScript runner를 한 열 밀었을 때 target은 invalid/missing-header에서 exit 1로 끝났고, commit한 상태에서는 8번의 실행이 58 case에서 1.5 s 안에 일치한다. dbspec target 다섯 개가 make check에 들어 있다.[o]
T8.2.6Mermaid schema source 경로, PhysicalGraph dialect text record, 주석 달린 Markdown 경로를 제거하고 CLI 이름을 모든 언어에서 하나로 한다(T8.2.5 선행).—Evidence: T8.2.6.1에서 T8.2.6.4까지 끝났다. dbspec document set이 모든 generator, runtime, schema tool, 설치의 유일한 schema source다. CLI는 Go, PHP, TypeScript에서 orm-gen이며, Rust는 CLI 없이 build.rs로 model을 생성한다.[o]
T8.2.6.1generator가 dbspec 문서에서 읽는 manifest와 schemaHash, manifestHash algorithm을 정하고, dbspec text에서 manifest와 hash로 가는 공유 vector를 둔다(T8.2.6 후속). 원인: 모든 client의 generator는 Mermaid source에서 만든 manifest인 schema/schema.json(auto, raw, pk field)을 읽는다. docs/dbspec.md는 두 hash의 이름과 범위만 정하고 manifest 형식과 hash 계산은 정하지 않으므로, Mermaid 경로를 지우면 generator의 입력이 사라진다.—근거: docs/dbspec.md와 한국어 짝 문서에 "Manifest와 hash"를 더했다. 선언된 문서 집합이 유일한 schema 원천이고 다른 manifest 파일은 없다. manifest text는 comment와 diagram을 뺀 canonical emission이며, 집합은 문서 이름 순으로 이어 붙인다. schema text는 immutable과 audit setting만 남긴다. 각 hash는 sha256:과 그 text의 소문자 16진수 SHA-256이다. 생성 코드는 manifest text와 hash를 담고, runtime은 process 시작 때 한 번 parse하며, PHP generator는 PHP array로 써서 어떤 request도 parse하지 않는다(PHP parser는 2000 table에 208-220 ms, table당 약 0.1 ms). 이는 Go 형식 JSON에 대한 16자리 hash와 네 client의 manifest struct 9개를 대체한다. tests/dbspec/cases.json에 hashes case 3개(comment, diagram, manifest setting 제거; schema text의 audit 유지; 이름 순 두 문서 집합)를 더했다. text와 hash는 text 규칙으로 계산했고, 문서는 TypeScript에서 그대로 emit된다. dbspec target 다섯 개가 계속 통과한다.[o]
T8.2.6.1.1Korean manifest heading에 ASCII anchor를 준다 (T8.2.6.1 후속). Cause: VitePress는 ## Manifest와 hash의 id를 분해된 Hangul로 쓰므로 docs/dbspec.ko.md와 docs/dialects.ko.md의 #manifest와-hash link가 어느 anchor에도 닿지 않고 make docs-check가 실패한다.—Evidence: make docs-check는 Missing anchor: …/ko/dbspec.html#manifest%EC%99%80-hash로 실패했고, heading ## Manifest와 hash {#manifest-and-hashes}와 그 id를 가리키는 두 link로 통과한다.[o]
T8.2.6.1.1.1한국어 protocol link가 ASCII manifest anchor를 쓰게 한다 (T8.2.6.1.1 후속). Cause: docs/protocol.ko.md는 dbspec.ko.md#manifest와-hash로 link하는데 T8.2.6.1.1이 그 id를 manifest-and-hashes로 바꿨으므로, 현재 commit에서 npm run docs:check가 Missing anchor: …/ko/dbspec.html#manifest%EC%99%80-hash로 실패한다.Acceptance: link가 #manifest-and-hashes를 쓰고 make docs-check가 통과한다.Evidence: T8.2.6.1.3이 docs/protocol.ko.md의 같은 두 link를 #manifest-and-hashes로 고쳤다. docs/protocol.ko.md에는 manifest와-hash가 없고 make docs-check가 내부 link와 asset 573개를 통과한다.[o]
T8.2.6.1.2schemaHash가 table에만 달려 있게 한다 (T8.2.6.1.1 후속). Cause: schema text가 집합의 문서를 header와 use 줄째로 이어 붙여서, table을 다른 문서로 옮기거나 문서 이름을 바꾸면 database가 그대로인데도 schemaHash가 바뀌어 정의와 어긋났고, introspect한 database를 hash로 집합과 비교할 수 없었다. migration plan이 이 hash를 기록한다.—Evidence: docs/dbspec.md는 schema text를 집합의 모든 table을 이름 순으로 담은 문서 schema 하나로 정의한다. 새 hash vector same-tables-in-one-document는 document-set-in-name-order의 두 table을 한 문서에 두고 같은 schema text와 schemaHash sha256:9d2367df…f140을 요구하며, 이전 정의로는 그렇지 않았다. Go, PHP, TypeScript, Rust가 hash vector 네 개를 통과하고, make dbspec-compare-check와 introspection check 네 개가 통과하며, Go round trip은 ManifestOf(documents).SchemaText와 바로 비교한다.[o]
T8.2.6.1.3Korean protocol 문서가 manifest anchor를 가리키게 한다 (T8.2.6.1.1 후속). Cause: client 전환 merge(T8.2.6.3)가 docs/protocol.ko.md에 어느 heading에도 없는 anchor인 dbspec.md#manifest와-hash link를 더해서 make docs-check가 "Missing anchor"로 실패했다. 그 merge는 docs-check를 실행하지 않았다.—Evidence: make docs-check는 docs/protocol.ko.md의 Missing anchor: …/ko/dbspec.html#manifest%EC%99%80-hash로 실패했고, 두 link가 #manifest-and-hashes를 가리키자 static page 48개와 내부 link·asset 573개로 통과한다.[o]
T8.2.6.2Go, PHP, TypeScript, Rust client에서 dbspec 문서 집합의 manifest text, schema text, 두 hash를 계산한다. 모든 client가 tests/dbspec/cases.json의 hashes case를 통과하고 make dbspec-compare-check가 네 결과를 비교한다(T8.2.6.1 필요).—근거: Go ManifestOf, PHP Dbspec::manifest, TypeScript dbspecManifest, Rust manifest가 각 문서를 comment와 diagram을 뺀 view(manifest)나 immutable과 audit 외의 setting도 뺀 view(schema)로 emit하고, 문서 이름 순으로 이어 붙여 SHA-256으로 hash한다. 반복된 문서 이름은 1:10의 name.duplicate diagnostic 하나를 돌려준다. contracts/interfaces.json이 Dbspec.manifest rule과 DbspecManifest owner를 선언하고, interface-check가 네 언어에서 통과한다. 새 test(Go manifest_test.go, PHP dbspec_manifest_test.php, TypeScript tests/dbspec.mjs, Rust tests/dbspec_manifest.rs)는 함수가 없을 때 실패했고, hashes case 3개를 두 입력 순서로, 그리고 반복 이름 규칙을 통과한다. 각 client target이 이를 실행한다. compare runner가 모든 hashes case의 hash와 두 text를 출력하고, 8번의 실행이 61 case에서 일치한다. 다른 process가 Rust를 compile하는 동안 TypeScript stress 한 번이 median 320 ms였으나 parse code는 바뀌지 않았고, 이어진 실행은 86-104 ms였다.[o]
T8.2.6.2.1manifest와 렌더링에서 잘못된 문서 집합을 같은 방식으로 거부한다(T8.2.6.2 후속). 원인: ManifestOf는 반복된 문서 이름은 보고하지만 집합에 없는 쓰이는 문서는 보고하지 않고, Go renderer는 없는 쓰이는 문서를 조용히 건너뛰며, PHP와 TypeScript renderer는 둘 다 throw한다. docs/dbspec.md는 이 함수들이 받는 집합의 규칙을 정하지 않는다.—근거: docs/dbspec.md와 한국어 짝 문서가 "Manifest와 hash"에 집합 규칙(문서마다 한 번, 쓰이는 문서는 모두 존재, 뒤쪽 문서나 쓰는 문서의 1:10에 name.duplicate와 use, 문서와 쓰이는 이름은 이름 순, diagnostic이 있으면 manifest와 statement 없음)을 정하고, docs/dialects.md가 renderer를 그 규칙으로 안내한다. 이제 하나의 집합 확인(Go checkSet, PHP DocumentSet::check, TypeScript checkSet, Rust check_set)이 manifest와 renderer를 함께 맡고, renderer는 모든 client에서 statement나 diagnostic을 돌려준다(Go ([]string, []Diagnostic), PHP RenderResult, TypeScript DbspecRenderResult, Rust Result). tests/dbspec/cases.json에 sets case 3개(반복 이름, 없는 쓰이는 문서, 완전한 집합)를 더했다. 변경 전에는 Go가 새 signature에 compile되지 않았고, PHP와 TypeScript는 diagnostic 대신 throw했으며, TypeScript와 Go는 use diagnostic을 주지 않았고, Rust는 잘못된 집합을 렌더링했다. 이제 모두 통과한다. TypeScript DbspecDiagnostic이 model.ts로 옮겨져 contracts/interfaces.json의 owner 위치도 model.ts다. dbspec target 다섯 개, gofmt, rustfmt, clippy, 네 언어 interface-check가 통과한다.[o]
T8.2.6.3모든 generator, runtime, schema tool, schema 설치가 dbspec 문서 집합을 읽게 한다. 생성 코드는 manifest text와 manifestHash를 담고(PHP는 array로), runtime은 schema 파일 없이 그것으로 model을 만들며, schema/schema.json, schema/bench.mmd, contracts/와 client test의 manifest fixture의 원천을 dbspec 문서로 바꾼다(T8.2.6.2와 T8.3 필요).—근거: T8.2.6.3.1-T8.2.6.3.7이 완료되었고, T8.2.6.3.2가 옛 test를 위해 남긴 Mermaid source와 manifest를 T8.2.6.4가 제거했다.[o]
T8.2.6.3.1client가 dbspec 문서 집합으로 만드는 runtime model과 각 언어의 생성 코드가 담는 것을 정한다: entity와 field 이름, dbspec type별 값 type, codec stage, 기본 select 집합, updated, soft_delete, aes_version, blind_index, audit operation 동작, 그리고 navigation setting으로만 만드는 relation. Mermaid manifest의 모든 field를 판정한다(T8.2.6.2.1 필요). 원인: runtime은 manifest struct 9개를 읽는데, 그 field(auto, lazy, styles, _seq 이름에서 만든 relations, fulltext, unsigned, enum)는 dbspec에 대응이 없거나 이름이 다르다.—근거: docs/dbspec.md와 한국어 짝 문서에 "Runtime model"을 더했다: 이름과 문서 순서의 table별 entity와 entity나 table에서 오는 이름, dbspec type 13개의 언어별 값 type 표가 있는 field(새 i16 경로, TypeScript date는 YYYY-MM-DD text이며 지금은 date-time text를 돌려줌, time(p)는 모두 text), key, select explicit로만 정하는 기본 select 집합, 생략된 column의 database default, stage로 정하는 codec 값 type(ordered_json, serialize, yaml, gz, base64는 styled 값이며 Rust는 지금 gz, base64를 styled 없이 돌려줌, aes, hex, ip는 문자열), audit operation id를 포함한 모든 setting의 runtime 동작, match method join, schema 경로 없이 manifestHash를 담는 connection. 마지막 문단은 Mermaid manifest의 모든 field를 대응시키고 fulltext 조건과 point 값을 없앤다. navigation setting은 생성 코드가 읽지 않는다고 적으며, 이는 네 generator가 manifest relation 이름을 쓰지 않는 사실과 맞는다.[o]
T8.2.6.3.2schema/bench.mmd, Go test와 contracts/fixtures의 Mermaid fixture, Rust manifest fixture를 dbspec 문서로 다시 쓴다. 이름 관례로 정해지던 동작은 모두 setting으로 선언하고 모든 foreign key에 index를 둔다(T8.2.6.3.1 필요).—근거: schema/bench.dbs가 schema/bench.mmd를 다시 쓴다: is_* column은 bool, updated_ts는 updated setting, aes_key_version은 aes_version setting, 이름 prefix가 있던 column은 codec(aes hex, gz, json_과 jsons_ 모두 ordered_json, base64, serialize, ip)을 선언하고, Mermaid builder가 type이나 style로 기본 select에서 빼던 column은 select explicit에 나열하며, unsigned는 check ck_author_counts, enum(open_done)은 ck_task_state가 있는 varchar(8), 모든 relation 줄은 선두 index가 있는 이름 있는 foreign key가 되고(index 5개 추가), fulltext directive는 뺀다. contracts/fixtures에 decimal_schema.mmd와 Rust aes_json, zone_schema manifest에서 온 decimal_schema.dbs, secret_config.dbs, zone.dbs와, 이력 table audit용 audit.dbs를 더했다. Rust audit_* manifest는 제거된 operation, change table을 기술하므로 다시 쓰지 않는다. 모든 문서는 parse 후 그대로 emit되고, make dbspec-ddl-check의 새 TestSchemaDocumentsApply가 5개 문서를 Go renderer로 렌더링해 MySQL, PostgreSQL, SQLite에 적용한다: probe 15개가 통과한다. .mmd 파일과 이전 manifest는 T8.2.6.4까지 이전 test를 위해 남는다.[o]
T8.2.6.3.3Go client와 engine에서 dbspec으로 runtime model과 생성 코드를 만든다(T8.2.6.3.2 필요).—Evidence: engine/runtimemodel가 model을 만들고 ormgen gen --document가 manifest를 넣는다. Install(manifestText)는 dbspec을 render한다. db.Transaction(fn, orm.Operation(id))가 audit operation id를 쓴다. 네 전환은 interface check가 일치를 요구하는 IR record(manifest_hash, match 없음)를 함께 바꾸므로, 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, conformance-check를 MySQL, PostgreSQL, SQLite에서 통과하는 tree의 merge 하나로 통합했다.[o]
T8.2.6.3.4PHP client에서 dbspec으로 runtime model과 생성 코드를 만든다(T8.2.6.3.2 필요).—Evidence: RuntimeModel이 model을 만들고 generator가 PHP array를 쓴다. orm-gen gen <files.dbs>, install(array $documents), transaction($fn, operation: $id)가 audit operation id를 쓴다. 네 전환은 interface check가 일치를 요구하는 IR record(manifest_hash, match 없음)를 함께 바꾸므로, 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, conformance-check를 MySQL, PostgreSQL, SQLite에서 통과하는 tree의 merge 하나로 통합했다.[o]
T8.2.6.3.5TypeScript client에서 dbspec으로 runtime model과 생성 코드를 만든다(T8.2.6.3.2 필요).—Evidence: src/engine/model.ts가 model을 만들고 생성 model은 MANIFEST_TEXT와 MANIFEST_HASH를 export한다. install(texts)는 dbspec을 render하고 transaction(cb, { operation })가 audit operation id를 쓴다. host stage만 가진 column은 create() 후 문자열 값을 가진다. 네 전환은 interface check가 일치를 요구하는 IR record(manifest_hash, match 없음)를 함께 바꾸므로, 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, conformance-check를 MySQL, PostgreSQL, SQLite에서 통과하는 tree의 merge 하나로 통합했다.[o]
T8.2.6.3.6Rust client에서 dbspec으로 runtime model과 생성 코드를 만든다(T8.2.6.3.2 필요).—Evidence: orm-schema/src/dbspec/runtime.rs가 model을 만들고 Builder::new([dbspec paths])가 manifest를 넣는다. install(&Schema)는 dbspec을 render하고 transaction(f).operation(id)가 audit operation id를 쓴다. 네 전환은 interface check가 일치를 요구하는 IR record(manifest_hash, match 없음)를 함께 바꾸므로, 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, conformance-check를 MySQL, PostgreSQL, SQLite에서 통과하는 tree의 merge 하나로 통합했다.[o]
T8.2.6.3.7dbspec bench schema의 conformance vector를 네 client로 MySQL, PostgreSQL, SQLite에서 기록한다(T8.2.6.3.3-T8.2.6.3.6 필요).—Evidence: scripts/bench-db.sh가 손으로 쓴 bench/sql/author*.sql 대신 go run ./bench/install로 schema/bench.dbs를 설치하고 seed한다. 모든 conformance runner는 --dsn URI만 받는다. Go, Rust, TypeScript는 request가 AES key version column을 고르면 그것을 한 번만 읽는다(engine TestAllColumnsSelectAESVersionOnce, TypeScript tests/engine.mjs, Rust all_columns_select_the_aes_version_once가 먼저 두 번 선택으로 실패했다). tests/conformance/vectors.json, vectors.postgres.json, vectors.sqlite.json은 네 client가 일치한 출력에서 기록한 vector 25개씩을 가지며, SQL을 결과로 돌려주는 get_query 밖의 결과는 바뀌지 않았다. make conformance-check가 세 database에서 통과한다.[o]
T8.2.6.4Mermaid schema source 경로, PhysicalGraph dialect-text record, annotated Markdown 경로를 제거하고 CLI 이름을 모든 언어에서 하나로 한다(T8.2.6.3 필요).—근거: 파일 246개를 제거했다: Go 84개(engine/schema, internal/ormgen, clients/go/orm의 physical record, tests/schema/record), PHP 46개, TypeScript 45개, Rust 45개, contracts/의 fixture 17개, schema/bench.mmd, schema/schema.json, tests/schema, scripts/schema, scripts/db-test.sh. 이것은 Mermaid parser, manifest JSON과 orm-schema-v1 SQL, 그것을 읽던 모든 CLI 명령(build, import, validate, ddl, diff, migrate, plan, apply, verify, recover, rollback)과 PhysicalGraph record이며, 명령의 dbspec 대응은 각 client의 render, introspect, plan, apply, recover 함수다. CLI는 Go(gen과 errors를 가진 cmd/orm-gen, model generator는 package generator로 옮김), PHP, TypeScript(gen만)에서 orm-gen이다. Rust orm-gen binary는 Mermaid 명령만 있어 제거했고 Rust는 build.rs에서만 model을 생성한다. 생성 파일은 Code generated by orm-gen이라고 쓴다. interface check는 entity rule을 schema/bench.dbs로 펼치고, scripts/decimal-db-setup.php는 contracts/fixtures/decimal_schema.dbs를 렌더링하며, PHP 예제는 제거된 schema 경로를 넘기지 않는다. schema-check, schema-cross-language-check, db-test와 physical-*-check target을 제거했고 CI는 schema-check 대신 ts-model-check를 실행한다. contracts/symbols에서 Go 315개, PHP 334개(OrmGen 8개 추가), TypeScript 824개, Rust 682개 symbol이 빠지고, contracts/features.json은 point_type을 빼고 schema_definition을 dbspec test에 연결한다. 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(MySQL, PostgreSQL, SQLite에서 vector 25개와 4개 언어), docs-check(page 48개, 내부 link 545개)와 generator, CLI, interface, conformance, dialect, engine package의 go test가 통과한다. feature-check는 이전 commit과 같이 contracts/features.json과 다른 docs/features.md의 dsn_connection 줄과 T15의 coverage 오류로 실패한다. feature manifest 오류는 156건에서 69건으로 줄었고 추가된 것은 없다. checklist의 raw 인용 두 개(<value>, <nil>)는 VitePress가 멈춰 docs-check가 실패했으므로 code span으로 바꿨다.[o]
T8.2.6.4.1Rust lock file의 낡은 libc 항목을 지운다 (T8.2.6.4 후속). Cause: T8.2.6.4의 merge가 libc를 쓰던 graph test와 orm-schema의 libc 의존성을 지웠지만 clients/rust/Cargo.lock은 orm-schema 아래에 libc를 그대로 두어, make rust-check의 cargo check --locked가 "cannot update the lock file … because --locked was passed"로 멈췄다. acceptance: lock file이 manifest와 맞고 cargo check --locked가 통과한다.—Evidence: commit된 lock file에서 cargo check --locked --offline은 그 오류로 실패했고, 항목을 지운 뒤 통과하며 make rust-check가 통과한다.[o]
T8.3모든 client에서 dbspec으로부터 dialect DDL을 렌더링한다(T8.2.6.2 선행). 순서의 원인: runtime test는 DDL로 schema를 설치하므로, generator와 runtime은 dbspec이 DDL을 렌더링한 뒤에야 dbspec으로 바뀐다(T8.2.6.3). 완료 기준: neutral 정의와 trigger 계열 settings가 T8.0 표대로 렌더링되고, 미지원 정의는 명시적으로 실패하며, 기존 audit/immutable directive 경로가 교체된다.—Evidence: 모든 하위 항목이 끝났다. tests/dbspec/ddl.json이 네 client에서 T8.0 표대로 neutral 정의와 trigger 기반 setting을 생성하고, 미지원 정의는 명시적으로 실패하며, make dbspec-ddl-check가 vector와 모든 schema 문서를 MySQL, PostgreSQL, SQLite에 적용한다. 옛 audit와 immutable directive 경로는 Mermaid source와 함께 제거됐다(T8.2.6.4).[o]
T8.3.1각 dialect가 dbspec 문서 집합에서 렌더링하는 statement(statement 순서, type text, renderer CHECK 이름과 text, collation, key, index, foreign key, immutable과 audit의 trigger)를 공유 vector로 정하고, 모든 vector를 dialect probe 환경의 MySQL 8.4, PostgreSQL 17, SQLite에 적용한다(T8.2.6.2 필요).—근거: docs/dialects.md와 한국어 짝 문서에 "Rendered statements"를 더했다: use와 문서 순서의 statement 순서, 따옴표 identifier, statement당 한 줄, dialect별 column type과 default, identity, dialect 형식의 literal(SQLite decimal은 10^s 곱, MySQL backslash는 두 번), key, index, foreign key(MySQL과 PostgreSQL은 모든 table 뒤 ALTER TABLE, SQLite는 안쪽), 선언된 check, 모든 type과 precision의 정확한 text를 가진 <table>$<column> 이름의 renderer CHECK, PostgreSQL function이 있는 <table>$<event> 이름의 immutable과 audit trigger. tests/dbspec/ddl.json은 문서 집합 5개(모든 type과 default, 앞선 참조가 있는 key, index, foreign key, dialect 형식의 check, immutable과 audit, 문서 사이의 use)를 세 dialect의 statement와 behavior step과 함께 담는다. make dbspec-ddl-check가 이를 MySQL 8.4.11, PostgreSQL 17.11, SQLite 3.53.4에 적용하고 step을 실행한다: probe 15개가 141 ms에 통과한다. 첫 실행에서 PostgreSQL이 timestamp 입력의 24시를 다음 자정으로 저장한다는 것을 찾아 기록과 vector에 적었고, behavior query가 MySQL keyword change를 따옴표 없이 쓴 것을 고쳤다.[o]
T8.3.2Go client에서 T8.3.1 statement를 렌더링한다(T8.3.1 필요).—근거: engine/dbspec/render.go의 Render(documents, dialect)가 docs/spec/dialects.md "Rendered statements"의 statement를 쓴다. test보다 먼저 작성했으므로 앞선 Red test는 없다. 이 renderer가 tests/dbspec/ddl.json의 statement를 만들었고, make dbspec-ddl-check가 그것을 behavior step과 함께 세 database에 적용했다(T8.3.1). 이제 make dbspec-go-check의 TestRenderVectors가 그 출력을 15개 dialect 렌더링 모두와 비교한다. Go symbol을 기록한 뒤 네 언어 interface-check가 통과한다. contracts/interfaces.json은 다른 client가 렌더링한 뒤 Dbspec.render를 선언한다(T8.3.6).[o]
T8.3.3PHP client에서 T8.3.1 statement를 렌더링한다(T8.3.1 필요).—근거: clients/php/src/Dbspec/Renderer.php와 Dbspec::render(array $documents, string $dialect): array가 docs/spec/dialects.md "Rendered statements"의 statement를 쓴다. 각 canonical check text를 다시 읽어 모든 literal을 그 column type에 맞게 렌더링한다. clients/php/tests/dbspec_render_test.php는 renderer가 없을 때 "Call to undefined method Orm\Dbspec\Dbspec::render()"로 실패했고, 이제 tests/dbspec/ddl.json의 5개 case를 세 dialect에서 byte 단위로 맞추며, 문서 목록을 뒤집어도 같고, 모르는 dialect를 거부한다. make dbspec-php-check가 이를 두 번 실행하고 통과하며, load 아래 stress median은 237-287 ms다. PHP symbol을 기록한 뒤 네 언어 interface-check가 통과한다.[o]
T8.3.4TypeScript client에서 T8.3.1 statement를 렌더링한다(T8.3.1 필요).—근거: clients/typescript/src/dbspec/render.ts의 renderDbspec(documents, dialect)와 DbspecDialect가 docs/spec/dialects.md "Rendered statements"의 statement를 쓴다. check.ts는 canonical check text를 다시 tree로 읽어 각 literal을 그 column type에 맞게 렌더링한다. clients/typescript/tests/dbspec-render.mjs는 renderer가 없을 때 "does not provide an export named renderDbspec"으로 실패했고, 이제 tests/dbspec/ddl.json의 5개 case를 세 dialect에서 맞추며, 문서 목록을 뒤집어도 같고, 모르는 dialect를 거부한다. make dbspec-ts-check가 test 156개를 통과하며 load 아래 stress median은 129-130 ms다. package root(src/index.ts)가 이제 renderDbspec과, T8.2.6.2가 dist/dbspec에서만 닿게 둔 dbspecManifest를 export한다. clients/typescript의 tsc --noEmit이 통과하고, TypeScript symbol을 기록한 뒤 네 언어 interface-check가 통과한다.[o]
T8.3.5Rust client에서 T8.3.1 statement를 렌더링한다(T8.3.1 필요).—근거: clients/rust/orm-schema/src/dbspec/render.rs의 render(documents, dialect)와 Dialect { MySql, Postgres, Sqlite }가 docs/spec/dialects.md "Rendered statements"의 statement를 쓴다. tests/dbspec_render.rs는 renderer가 없을 때 compile되지 않았고 빈 stub에서는 내용으로 실패했다. 이제 tests/dbspec/ddl.json의 5개 case를 세 dialect에서 맞추며 문서 목록을 뒤집어도 같고, make dbspec-rust-check가 이를 실행해 stress median 29-31 ms로 통과한다. 파일을 다시 나누는 동안 한 번의 실행이 exit 1로 끝났는데, log를 읽기 전에 덮어썼고 이후 네 번의 실행(그때의 Makefile로 한 번 포함)은 통과했다. Rust symbol을 기록한 뒤 fmt, clippy, 네 언어 interface-check가 통과한다.[o]
T8.3.6make dbspec-compare-check에서 네 client의 렌더링 statement를 비교한다(T8.3.2-T8.3.5 필요).—근거: contracts/interfaces.json이 Dbspec.render rule(Go Render, PHP Dbspec::render, TypeScript renderDbspec, Rust render, 각각 statement나 집합 diagnostic을 돌려줌)을 선언하고 네 언어 interface-check가 통과한다. 네 compare runner가 tests/dbspec/ddl.json을 읽어 모든 case의 statement를 세 dialect로 출력하며, make dbspec-compare-check가 각 client를 두 번 실행해 8번의 실행이 15개 렌더링을 포함한 108 case에서 일치한다.[o]
T8.4모든 client에서 MySQL, PostgreSQL, SQLite를 dbspec으로 introspect한다(T8.3 선행). 완료 기준: query 수가 table 수와 무관, CHECK 정규형과 SQLite CREATE parsing, marker 없이 renderer 결과와의 정확한 일치로 렌더링된 settings 복원, 미지원 객체를 위치와 함께 보고.—Evidence: T8.4.1–T8.4.6과 T8.4.2.1–T8.4.2.3, T8.4.3.1: 네 client는 table 수와 무관하게 catalog query 9, 7, 3개로 MySQL, PostgreSQL, SQLite를 introspect하고, 모든 ddl.json vector와 schema 문서의 schema text를 복원하며, tests/dbspec/introspect.json의 미지원 case를 보고하고, 2000 table stress database에서 각각 5초 안에 일치한다.[o]
T8.4.1각 dialect를 dbspec 문서로 introspect하는 방법(table 수와 무관한 수의 catalog query, type, default, identity, key, index, foreign key, check 읽기, renderer 출력과 같을 때 알아보는 renderer CHECK와 trigger, 위치와 함께 보고하는 미지원 객체)을 정하고, round-trip vector를 둔다: tests/dbspec/ddl.json의 모든 vector와 모든 schema 문서를 MySQL, PostgreSQL, SQLite에서 렌더링, 적용, introspect하고 schema text로 비교하며, dbspec이 지원하지 않는 객체를 보고한다(T8.3.1 필요).—Evidence: docs/spec/dialects.md "Introspection"과 Korean pair는 catalog query, dialect별 type 표, null, identity, default 읽기, key, index, foreign key, renderer CHECK 형식, dialect별 check 해독, trigger 인식, 미지원 kind와 그 순서, 빠진 객체를 참조하는 객체의 제거를 정한다. tests/dbspec/introspect.json은 dialect마다 문서, statement, 기대 문서, 미지원 목록을 가진 case 하나를 둔다.[o]
T8.4.2Go client에서 dbspec으로 introspect한다(T8.4.1 필요).—Evidence: engine/dbspec/introspect*.go Introspect(ctx, querier, dialect, name)가 문서와 미지원 객체를 돌려준다. 이것 없이 physical test는 build되지 않았다(undefined: dbspec.Introspect). make dbspec-introspect-check는 ddl.json vector 5개와 schema 문서 5개를 MySQL, PostgreSQL, SQLite에서 30번 round trip해 같은 schema text와 미지원 객체 없음을 확인하고, 모든 집합에서 query 수가 8, 7, 3임을 확인하며, tests/dbspec/introspect.json의 case 3개를 통과한다. 기대 항목 하나를 빼면 그 확인은 실패한다.[o]
T8.4.2.1모든 client에서 각 미지원 객체를 한 번만 보고하고 SQLite table 항목을 정확히 읽는다 (T8.4.2 후속). Cause: 같은 parse에서 table과 그 객체가 함께 거부되면 Go introspector가 nil table을 참조했다. 미지원 action을 가진 여러 column의 MySQL foreign key가 column마다 보고됐다. 알 수 없는 SQLite table 항목이 그 text를 이름 삼은 kind check로 보고됐고, table 수준 UNIQUE가 자동 index로 다시 보고됐다. inline CHECK나 REFERENCES clause가 있는 SQLite column은 그 clause 없이 읽혔다. PHP, TypeScript, Rust port도 같은 동작을 따랐다.—Evidence: tests/dbspec/introspect.json에 mysql-dependents-once(index가 있는 int unsigned primary key column과 두 column NO ACTION foreign key)와 sqlite-table-items(이름 없는 CHECK, table 수준 UNIQUE, inline CHECK, inline REFERENCES)를 더했다. 수정 전에는 Go와 PHP가 nil table에서 멈추고 TypeScript와 Rust가 두 case에서 실패했으며, 수정 후 make dbspec-introspect-check, dbspec-introspect-php-check, dbspec-introspect-ts-check, dbspec-introspect-rust-check가 각각 미지원 case 5개와 round trip 30개를 통과한다. docs/dialects.md가 rule을 적는다.[o]
T8.4.2.2모든 client에서 ALTER TABLE 후의 MySQL renderer CHECK을 알아본다 (T8.4.2.1 후속). Cause: 2000 table 비교가 MySQL stress database의 모든 uuid와 time column을 미지원으로 보고했다. ALTER TABLE로 foreign key를 더하자 MySQL이 CHECK_CLAUSE를 uuid pattern에는 _ascii introducer를, time literal에는 introducer 없이 다시 써서 renderer 형식과 같지 않았다.—Evidence: probe mysql.check.alter_rewrites_introducers는 ALTER TABLE t ADD INDEX가 regexp_like(...,_utf8mb4\'...)를 _ascii로 다시 쓰고 time literal의 introducer를 뺀다는 것을 기록한다. vector mysql-checks-after-alter는 Go, PHP, TypeScript, Rust에서 "document differs"로 실패했고, 각 client가 introducer를 뺀 renderer CHECK을 비교하면 통과한다. make dialect-facts-check와 introspection check 네 개가 통과한다.[o]
T8.4.2.3모든 client에서 느린 catalog join 없이 MySQL check를 읽는다 (T8.4.2.2 후속). Cause: 2000 table 비교에서 MySQL introspection이 4분 23초 걸렸다. stress database에서 CHECK_CONSTRAINTS와 TABLE_CONSTRAINTS를 join하는 query 하나가 63.7초였고, 각 catalog view를 따로 읽으면 0.1초 미만이었다.—Evidence: 2000 table MySQL stress database에서 join한 check query는 63.7초, 이를 대신하는 두 query는 0.09초와 0.08초였다. 네 client는 CHECK_CONSTRAINTS와 TABLE_CONSTRAINTS를 따로 읽고 database 안에서 유일한 check 이름을 기준으로 이으며, clause가 없는 check에서는 실패한다. introspection check 네 개가 통과하고 모든 집합에서 MySQL query 9개를 확인한다.[o]
T8.4.2.4모든 client에서 ALTER TABLE 후 time(p) column의 MySQL time CHECK을 알아본다 (T8.4.2.3 후속). Cause: column을 time(3)으로 올린 첫 migration plan이 MySQL에서 그 column을 미지원으로 보고했다. 다시 쓴 CHECK_CLAUSE가 \'00:00:00.000\'이고, T8.4.2.2는 이를 빼지 않았다.—Evidence: probe mysql.check.alter_writes_time_precision는 ALTER TABLE t ADD INDEX가 time(3) column CHECK의 time literal에 세 자리 0 소수를 쓴다는 것을 기록한다. vector mysql-time-precision-after-alter는 Go, PHP, TypeScript, Rust에서 실패했고, 각 client가 비교 전에 time literal의 0인 소수도 빼면 통과한다. introspection check 네 개와 make dialect-facts-check가 통과한다.[o]
T8.4.3PHP client에서 dbspec으로 introspect한다(T8.4.1 필요).—Evidence: clients/php/src/Dbspec의 Dbspec::introspect(PDO, dialect, name)가 문서와 미지원 객체를 담은 IntrospectResult를 돌려준다. 이것 없이 clients/php/tests/dbspec_introspect_test.php는 실패했다(Call to undefined method Orm\Dbspec\Dbspec::introspect()). make dbspec-introspect-php-check는 ddl.json vector 5개와 schema 문서 5개를 MySQL, PostgreSQL, SQLite에서 30번 round trip해 같은 schema text와 미지원 객체 없음을 확인하고, 모든 집합에서 query 수가 8, 7, 3임을 확인하며, tests/dbspec/introspect.json의 case 3개를 통과한다. 기대 항목 하나를 빼면 그 확인은 실패한다. 알 수 없는 dialect와 exception, silent error mode에서 실패하는 catalog query는 error다.[o]
T8.4.3.1기본 memory limit 안에서 PHP로 2000 table을 introspect한다 (T8.4.3 후속). Cause: 2000 table 비교에서 PHP runner가 "Allowed memory size of 134217728 bytes exhausted"로 멈췄다. catalog가 column 60000개마다 ColumnType 객체를, 문서 줄 약 90000개마다 배열을 들고 있었고 그 위에 문서 parse가 더해졌다.—Evidence: clients/php/tests/dbspec_introspect_stress_test.php는 stress 문서를 SQLite로 렌더링하고 적용한 뒤 memory_limit=128M에서 introspect한다. 이전 catalog로는 "Allowed memory size of 134217728 bytes exhausted"로 멈췄고, catalog가 type마다 readonly ColumnType 하나를 나눠 쓰고 줄 객체를 문자열 하나로 두자 peakBytes=118292480, 원본 schema text, 미지원 객체 없음으로 통과한다. make dbspec-introspect-php-check가 이를 실행한다.[o]
T8.4.4TypeScript client에서 dbspec으로 introspect한다(T8.4.1 필요).—Evidence: clients/typescript/src/dbspec/introspect*.ts introspectDbspec(connection, dialect, name)가 mysql2, pg, node:sqlite connection을 읽어 문서와 미지원 객체를 돌려준다. 이것 없이 physical test는 load되지 않았다(does not provide an export named 'introspectDbspec'). make dbspec-introspect-ts-check는 ddl.json vector 5개와 schema 문서 5개를 MySQL, PostgreSQL, SQLite에서 30번 round trip해 같은 schema text와 미지원 객체 없음을 확인하고, 모든 집합에서 query 수가 8, 7, 3임을 확인하며, tests/dbspec/introspect.json의 case 3개를 통과하고(기대 항목 하나를 빼면 그 확인은 실패한다), 알 수 없는 dialect와 실패한 catalog query를 reject한다.[o]
T8.4.5Rust client에서 dbspec으로 introspect한다(T8.4.1 필요).—Evidence: clients/rust/orm-schema/src/dbspec/introspect/*.rs가 catalog query의 row를 읽고(catalog_queries, read_catalog), clients/rust/orm/src/dbspec.rs의 introspect(connection, dialect, name)이 sqlx MySQL, PostgreSQL, SQLite connection에서 이를 실행해 문서와 미지원 객체를 돌려준다. 이것 없이 physical test는 build되지 않았다(unresolved import orm::dbspec). make dbspec-introspect-rust-check는 MySQL, PostgreSQL, SQLite에서 ddl.json vector 5개와 schema 문서 5개의 round trip 30개를 같은 schema text와 미지원 객체 없이 통과하고, 모든 집합에서 query 수가 8, 7, 3임을 확인하며, tests/dbspec/introspect.json의 case 3개를 통과한다. 기대 항목 하나를 빼면 그 확인은 실패한다.[o]
T8.4.6세 database에서 네 client가 introspect한 문서를 비교한다(T8.4.2-T8.4.5 필요).—Evidence: make dbspec-introspect-compare-check는 2000 table, 10000 foreign key stress 문서를 MySQL, PostgreSQL, SQLite에 적용하고 tests/dbspec/introspect의 runner를 실행한다. 네 출력(각 2490991 bytes)이 같고 원본 schema text를 가지며 미지원 객체가 없고, 각 introspection이 5초 안에 끝난다: MySQL Go 2.52초, PHP 2.84초, TypeScript 1.36초, Rust 1.67초, PostgreSQL 0.72, 1.94, 0.92, 1.00초, SQLite 0.72, 1.96, 0.92, 0.98초. 첫 실행에서 T8.4.2.2, T8.4.2.3, T8.4.3.1을 찾았다.[o]
T8.5모든 client에서 schema diff와 migration plan chain을 만든다(T8.4 선행). 완료 기준: 의존 순서 plan, plan chain 안의 명시적 rename, destructive 변경에 대한 명시적 허용.—Evidence: T8.5.1에서 T8.5.7까지와 그 후속이 끝났다. docs/plans.md가 plan 문서, chain, diff, statement 순서, 비교를 정한다. 네 client가 공유 plan, invalid, chain, parse, comparison case를 통과하고 make dbspec-compare-check에서 일치한다. make dbspec-plan-check와 client 변형이 모든 case를 세 database에 적용한다.[o]
T8.5.1plan 문서, plan chain, diff, dialect별 statement, SQLite rebuild를 docs/plans.md와 Korean pair에 정하고 tests/dbspec/plans.json의 공유 case를 둔다 (T8.4 필요).—Evidence: docs/plans.md와 docs/plans.ko.md는 plan 문서와 canonical form, chain, 변경·넓히기·거부·강제 재생성을 담은 diff, statement 열 단계, SQLite rebuild를 정한다. tests/dbspec/plans.json은 변경과 MySQL, PostgreSQL, SQLite statement를 가진 case 9개와 diagnostic을 가진 invalid case 8개를 둔다. docs-rules-check가 문서 23쌍을 통과한다.[o]
T8.5.1.1rebuild 뒤에도 SQLite automatic key counter를 유지한다 (T8.5.1 후속). Cause: docs/plans.md의 rebuild는 INSERT … SELECT로 행을 "$rebuild"에 복사하고 table을 지운 뒤 복사본의 이름을 바꾸므로 sqlite_sequence가 복사한 가장 큰 key에 머문다. 가장 큰 key의 행을 지운 뒤라면 다음 insert가 지운 key를 다시 쓴다(sqlite3로 관찰: key 1, 2, 3, 3 삭제, rebuild, insert가 3을 준다). 반면 docs/spec/dialects.md "Automatic key"는 key를 다시 쓰지 않는다고 정한다.Acceptance: rebuild가 sqlite_sequence의 counter를 옮기며, rebuild 전에 가장 큰 key를 지우는 tracked plan case가 모든 client에 있다.Evidence: docs/spec/plans.md "Statements"가 counter 이전을 정한다: CREATE TABLE "$rebuild" 뒤에 identity column이 있는 table은 INSERT … SELECT 전에 INSERT INTO sqlite_sequence (name, seq) SELECT '$rebuild', seq FROM sqlite_sequence WHERE name = '<table>'를 실행하므로, 복사본은 지금까지 준 가장 큰 key를 유지하고 SQLite는 row를 더하지 않고 그 row를 고친다(sqlite3 3.51.0으로 확인: key 1, 2, 3, 3 삭제, rebuild, insert는 4이고, 모든 row를 지운 경우에도 4). tests/dbspec/plans.json은 identity table의 SQLite rebuild 9개에 이 statement를 더하고 case rebuild-keeps-key-counter를 더한다: table k에 check를 더해 SQLite에서 다시 만들고, before에서 row 1, 2, 3을 넣고 3을 지우며, after에서 insert 뒤 SELECT MAX(id) FROM k가 4이고 id 3인 row가 없어야 한다. Red: Go TestPlanVectors, PHP clients/php/tests/dbspec_plan_test.php, TypeScript clients/typescript/tests/dbspec-plan.mjs, Rust plan_vectors가 SQLite statement에서 실패했고(Go, TypeScript, Rust는 8개 case, PHP는 첫 case rename-table-and-column), 각 client 자신의 statement를 적용하는 make dbspec-plan-php-check, make dbspec-plan-ts-check, make dbspec-plan-rust-check는 SQLite에서 SELECT MAX(id) FROM k: got "3"; want "4"로 실패했다. Green: 네 planner가 이 statement를 쓰며, 네 vector test가 통과하고, make dbspec-plan-check(vector statement)와 PHP, TypeScript, Rust plan check가 MySQL, PostgreSQL, SQLite에서 rebuild-keeps-key-counter를 통과한다. 통과한 gate: 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, checklist-check.[o]
T8.5.2Go engine에서 plan을 parse, chain, diff하고 쓴다 (T8.5.1 필요).—Evidence: engine/dbspec/plan*.go의 ParsePlan, EmitPlan, Chain, Diff, PlanStatements. 이것 없이 plan test는 build되지 않았다(undefined: ParsePlan). make dbspec-go-check는 tests/dbspec/plans.json의 case 9개(변경, 세 dialect statement, canonical emission), invalid case 8개, chain case 4개를 통과한다. make dbspec-plan-check는 27 run을 통과한다. 각 case를 MySQL, PostgreSQL, SQLite에 적용하면 data step과 함께 target schema text에 이른다.[o]
T8.5.2.1Go plan writer에서 버린 결과와 쓰지 않는 상태를 없앤다 (T8.5.2 후속). Cause: EmitPlan이 ManifestOf의 diagnostic을 _로 버렸고, Render와 PlanStatements가 채우던 renderer field tables는 읽히지 않아 PlanStatements가 쓸데없이 renderer를 하나 더 만들었다.—Evidence: ParsePlan은 검증한 target schema text를 두고 EmitPlan이 그것을 쓰므로 버리는 결과가 없다. renderer에는 tables field가 없고 PlanStatements는 renderer 하나를 쓴다. make dbspec-go-check가 plan case, invalid case, chain을 통과하고 make dbspec-plan-check가 27 run을 통과한다.[o]
T8.5.2.263 bytes를 넘는 plan 이름을 거부하고 plan parse error를 공유 case로 덮는다 (T8.5.2.1 후속). Cause: Go ParsePlan은 dbplan 1 <name>의 이름을 예약어로만 확인해서, docs/plans.md가 이름 규칙을 요구하는데도 64 bytes 이름을 받았다. tests/dbspec/plans.json에는 ParsePlan diagnostic case가 없었다.—Evidence: tests/dbspec/plans.json에 parse case 12개([rule, line, column, message], message는 rule plan에만)를 더했다. TestPlanParseErrors는 수정 전 name-too-long에서 diagnostic 없이 실패했고, ParsePlan이 63 bytes를 넘는 이름을 거부하자 통과한다. make dbspec-go-check가 이를 실행한다.[o]
T8.5.3PHP client에서 plan을 parse, chain, diff하고 쓴다 (T8.5.1 필요).—Evidence: clients/php/tests/dbspec_plan_test.php는 plan class 없이 실패하고(Class "Orm\Dbspec\PlanStatements" not found) make dbspec-php-check에서 cases=9 invalid=8 chains=4로 통과한다. make dbspec-plan-php-check는 case 9개를 MySQL, PostgreSQL, SQLite에 적용하고(runs=27) 각 target을 PHP introspection으로 다시 읽는다.[o]
T8.5.4TypeScript client에서 plan을 parse, chain, diff하고 쓴다 (T8.5.1 필요).—Evidence: clients/typescript/src/dbspec/plan*.ts의 parsePlan, emitPlan, chainPlans, diffPlan, planStatements. 이것 없이 plan test는 dist/dbspec/index.js가 chainPlans를 export하지 않아 실패했다. make dbspec-ts-check는 test 190개를 통과하며, 그 가운데 tests/dbspec/plans.json의 case 9개(변경, 세 dialect statement, canonical emission), invalid case 8개, chain case 4개와 알 수 없는 dialect의 TypeError가 있다. make dbspec-plan-ts-check는 27 run을 통과한다. 각 case를 MySQL, PostgreSQL, SQLite에 적용하면 data step과 함께 target schema text에 이른다. contracts/symbols/typescript.json이 새 symbol 107개를 기록하고 interface-check가 통과한다.[o]
T8.5.5Rust client에서 plan을 parse, chain, diff하고 쓴다 (T8.5.1 필요).—Evidence: clients/rust/orm-schema/src/dbspec/plan*.rs의 orm_schema::dbspec parse_plan, emit_plan, chain, diff, plan_statements. 이것 없이 plan test는 build되지 않았다(unresolved imports). make dbspec-rust-check는 tests/dbspec/plans.json의 case 9개(변경, 세 dialect statement, canonical emission), invalid case 8개, chain case 4개를 두 번 통과한다. make dbspec-plan-rust-check는 27 run을 통과한다. 각 case를 Rust renderer, plan statement, orm::dbspec::introspect로 MySQL, PostgreSQL, SQLite에 적용하면 data step과 함께 target schema text에 이른다.[o]
T8.5.6make dbspec-compare-check에서 네 client의 plan을 비교한다 (T8.5.2-T8.5.5 필요).—Evidence: PHP, TypeScript, Rust plan test가 tests/dbspec/plans.json의 parse case 12개를 Go TestPlanParseErrors처럼 실행한다([rule, 줄, 칸, message], message는 rule plan일 때만). 수정 전 세 client 모두 name-too-long에서 diagnostic 없이 실패했고, 이제 PHP, TypeScript, Rust가 63 byte를 넘는 plan 이름을 Go와 같은 message로 거절한다. make dbspec-php-check는 cases=9 invalid=8 chains=4 parse=12를 두 번 통과하고, make dbspec-ts-check는 203개 test를 모두 통과하고, make dbspec-rust-check는 plan parse errors: 12 cases를 두 번 통과하고, make dbspec-go-check는 TestPlanParseErrors subtest 12개를 통과한다. Go, PHP, TypeScript, Rust compare runner는 tests/dbspec/plans.json을 네 번째 인자로 받아 plan 항목 69개를 출력한다(case 9개의 emit한 plan, change, 세 dialect의 statement, invalid case 8개와 parse case 12개의 diagnostic, chain 4개의 순서나 diagnostic이며 plan과 chain diagnostic은 message까지). make dbspec-compare-check는 run마다 186 case로 8 runs agree를 통과하고, check.test.mjs는 다른 plan message를 포함한 5개 test를 통과하며, table과 name을 바꿔 출력한 Go runner는 plans/cases/create-from-empty/changes에서 실패했다.[o]
T8.5.7모든 client에서 두 schema의 차이를 나열한다 (T8.5.2-T8.5.5 후속). Cause: plan diff는 type, identity, primary key, column 순서 변경을 나열하지 않고 거부하며 drop마다 allow drop 줄을 요구해서, database를 model과 비교하는 도구가 모든 차이를 보여 줄 수 없다. acceptance: 공유 비교 API와 vector가 두 schema text의 모든 차이를 [kind, table, name]으로, plan이 거부하는 변경까지 네 client에서 같게 나열한다.—Evidence: docs/spec/plans.md "Comparison"과 한국어 pair가 비교를 정한다. plan diff의 kind와 넓히기를 함께 쓰므로 plan 문서에 둔다. 양쪽은 schema text의 문서이고, 아닌 쪽은 그쪽을 밝히는 compare diagnostic이다. table과 column은 같은 이름끼리 맞추고, 21개 kind(create_table, drop_table, drop_column, add_column, alter_column, change_column_type, change_column_identity, reorder_columns, change_primary_key, unique, index, foreign_key, check, immutable, audit의 drop_/add_)는 table 이름 순, kind 순, 이름 순으로 오며, 비교는 schema text가 같을 때에만 비어 있다. tests/dbspec/plans.json은 comparisons case 11개(같은 schema, 빈 schema에서와 한쪽에만 있는 table, column, identity와 primary key, column 순서, 객체, setting, schema text가 아닌 쪽 세 경우)를 가진다. Go CompareSchemas, PHP Dbspec::compareSchemas, TypeScript compareSchemas, Rust compare_schemas가 이를 구현한다. 구현 전 차이를 돌려주지 않는 stub으로 Go TestCompareSchemas는 11 case 중 10개가 실패했고, PHP plan test는 plan/comparison/tables-from-empty에서 멈췄고, TypeScript plan test는 45개 중 10개가 실패했고, Rust plan_comparisons는 실패 10개를 보고했다. 이제 TestCompareSchemas는 subtest 11개를 통과하고, PHP는 comparisons=11을 출력하고, TypeScript는 45개를 모두 통과하고, Rust는 plan comparisons: 11 cases를 통과한다. 네 compare runner는 모든 comparison을 plans/comparisons/<id>와 | kind table name 줄이나 ! compare 1 1 <message>로 출력한다. 앞 commit의 TypeScript runner로는 make dbspec-compare-check가 typescript 1 differs from go 1 in plans/comparisons/same-schema로 실패했고 runners.test.mjs는 새 TypeScript case 두 개(comparisons 없음, comparisons[0].target이 array가 아님)에서 실패했다. 이제 runner test 85개가 통과하고 8 run이 213 case에서 일치한다. TestCaseHarnessReportsOnlyFailure는 comparison case의 FAIL 줄도 요구한다. 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, checklist-check가 통과한다.[o]
T8.5.6.1dbspec compare runner가 빠진 vector section이나 field를 거절한다 (T8.5.6 후속). Cause: Go runner는 vector를 json struct로 decode하고 Rust runner는 into_iter().flatten(), unwrap_or_default, filter_map으로 읽어서, 빠진 section, id, 줄이 error가 아니라 빈 값이 되고, 빈 값에 일치하는 runner들은 비교를 통과한다.Acceptance: 모든 runner가 빠지거나 type이 틀린 section, id, document, 줄을 위치를 담은 error와 0이 아닌 exit로 거절한다. section 하나와 field 하나를 지운 test에서 각 runner가 실패한다.Evidence: tests/dbspec/compare/runners.test.mjs는 Go, PHP, TypeScript, Rust runner(check.mjs와 함께 쓰는 tests/dbspec/compare/runners.mjs)를 section이나 field 하나를 지우거나 type을 바꾼 vector 사본 19개(canonical, hashes, canonical[0].id, invalid[0].main, main document, document 줄, crlf, hashes[0].documents, ddl cases와 cases[0].id, plans chains, cases[0].source, invalid[0].plan, chains[0].plans[0], parse[0].id, mermaid round_trip, export[0].documents, import[0].mermaid, invalid[0].mermaid[0])로 실행하고, 0이 아닌 exit와 stderr의 <file>: <location> <problem>을 요구한다. 변경 전에는 77개 test 중 76개가 실패했다: 48번은 exit 0으로 끝났고(Go 13, PHP 12, TypeScript 6, Rust 17), 나머지 28번은 위치를 담은 error 없이 실패했다(file이 없는 Go json decoder 메시지, PHP warning과 TypeError, TypeScript TypeError, Rust mermaid vectors: round_trip is not an array). 이제 각 runner는 모든 vector file을 reader로 읽어 각 section, case object, id, main과 그 document, document set, 줄 array와 string 줄, source key(null 또는 줄)를 요구하고, 없는 crlf나 mixed는 false로, boolean이 아닌 값은 거부하며, JSON 문법 error에 file 경로를 쓴다. 77개 test가 통과한다. make dbspec-compare-check는 build 뒤에 이 test를 실행하고 8번의 실행이 202개 case에서 일치한다. 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, checklist-check가 통과한다.[o]
T8.6모든 client에서 lock, step journal, step event, 검증, 복구를 갖춘 plan apply를 만든다(T8.5 선행). 완료 기준: 세 DB 모두에서 apply → introspect → diff가 비어 있고, 중단된 non-transactional step을 catalog로 감지한다.—Evidence: T8.6.1에서 T8.6.6까지와 그 후속이 끝났다. docs/spec/plans.md "Apply"가 lock, history, state, statement, 검증, event, recovery, 실패 형태를 정한다. make dbspec-apply-check와 PHP, TypeScript, Rust 변형이 MySQL, PostgreSQL, SQLite에서 각각 17 run을 통과하며, 그 가운데 catalog로 하는 MySQL recovery가 있다. make dbspec-apply-pairs-check는 client 순서쌍 전부에서 chain을 48 run으로 이어 간다.[o]
T8.6.1plan 적용을 docs/spec/plans.md "Apply"와 Korean pair에 정한다: lock, introspection이 빼는 history table dbspec$plans, state와 drift 검사, dialect별 transaction, 검증, event, statement 효과에 따른 MySQL recovery (T8.5.1 필요).—Evidence: docs/spec/plans.md "Apply"와 docs/plans.ko.md는 MySQL과 PostgreSQL lock과 SQLite transaction, history table과 column, state·drift·chain error, dialect별 transaction, 미지원 객체 없는 검증, event 다섯 가지, 멱등한 apply와 recover, 중단된 MySQL statement의 catalog 효과에 따른 recovery를 정한다. docs/dialects.md는 introspection에서 dbspec$plans를 뺀다. docs-rules-check가 통과한다.[o]
T8.6.2Go engine에서 plan을 적용하고 복구한다 (T8.6.1 필요).—Evidence: engine/dbspec/apply.go의 Apply, Recover, ApplyEvent, ApplyError. 이것 없이 apply test는 build되지 않았다(undefined: dbspec.ApplyError). make dbspec-apply-check는 16 run을 통과한다: MySQL, PostgreSQL, SQLite에서 두 plan chain과 done history row 두 개, 맞는 event, event 없는 두 번째 apply; 세 database의 drift, lock, verify error; PostgreSQL과 SQLite의 실패 뒤 rollback; statement 앞과 뒤에서 멈춘 뒤의 MySQL recovery이며, 효과 확인을 끄면 실패한다. introspection은 dbspec$plans를 빼며 make dbspec-introspect-check, dbspec-plan-check, dbspec-ddl-check가 통과한다.[o]
T8.6.2.1실패한 Go apply의 정리 error를 보고한다 (T8.6.2 후속). Cause: apply 본문이 실패하면 Go는 RELEASE_LOCK, pg_advisory_unlock, ROLLBACK, COMMIT, PRAGMA foreign_keys = ON의 error를 버렸고, 실패한 BEGIN IMMEDIATE 뒤의 복원을 무시했으며, 아무것도 풀지 않은 advisory unlock을 받아들였고, row 없는 MySQL 효과 query를 효과 없음으로 읽었고, row를 닫는 error를 버렸다. 모든 error를 보고한다는 규칙에 어긋난다.—Evidence: engine/dbspec/apply_test.go의 TestApplyReportsCleanupErrors와 TestMySQLEffectRequiresRow는 SQLite connection을 감싸 실패를 주입한다. 수정 전에는 실패했고(stop, want both errors; locked: … begin failed, want locked and the restore error; no error), apply가 모든 정리 error를 errors.Join으로 합치고, 아무것도 풀지 않은 unlock을 거부하고, 효과 row가 없음을 보고하자 통과한다. make dbspec-go-check가 이를 실행하고 make dbspec-apply-check는 여전히 16 run을 통과한다.[o]
T8.6.2.2모든 client에서 apply의 정리 error를 모두 확인한다 (T8.6.2.1 후속). Cause: 아무것도 풀지 않은 PostgreSQL unlock의 case(unlock_not_held)는 PHP에만 있고, Go, TypeScript, Rust의 정리 동작은 같은 case로 확인되지 않는다. PHP Dbspec::apply 문서는 모든 실패가 ApplyError라고 하지만 history 쓰기 error와 event handler 예외는 그대로 전달된다. acceptance: Go, PHP, TypeScript, Rust가 각각 정리 case(실패한 ROLLBACK, BEGIN IMMEDIATE 뒤 실패한 복원, row 없는 효과 query, 아무것도 풀지 않은 unlock)를 실행하고 client 문서가 실패 형태를 적는다.—Evidence: tests/dialects/apply_physical_test.go, clients/typescript/tests/dbspec-apply-physical.mjs, clients/rust/orm/tests/dbspec_apply.rs의 새 PostgreSQL run unlock_not_held는 event에서 advisory lock을 푼다. pg_advisory_unlock의 결과를 무시하던 TypeScript(Missing expected rejection)와 Rust(Ok(()))에서 실패했고, 둘 다 아무것도 풀지 않은 unlock을 거부한 뒤(Rust는 ApplyError::LockNotHeld) 통과한다. Go run은 확인을 한 번 끈 상태에서 실패(<nil>)하고 되돌린 뒤 통과한다. clients/typescript/tests/dbspec-apply-cleanup.mjs와 clients/rust/orm/tests/dbspec_apply_cleanup.rs는 각각 3 case(실패한 ROLLBACK, 실패한 BEGIN IMMEDIATE 뒤 실패한 복원, script한 MySQL connection의 row 없는 효과 query)를 실행한다. 처음부터 통과했고, 정리 error 결합과 row 확인을 한 번 깨면 실패(Error: stop alone, Err(Event(Stop)), unexpected query UPDATE / unexpected statement UPDATE)하며 되돌린 뒤 통과한다. Go는 정리 error가 없는 실패도 errors.Join으로 감싸 돌려줬다. TestApplyReportsCleanupErrors의 새 확인이 실패(&errors.joinError{…}, want the event error itself)했고, joinCleanup이 error 하나를 그대로 돌려준 뒤 통과한다. Rust ApplyError::Cleanup은 정리 error를 ApplyError로 담는다. docs/spec/plans.md "Apply"는 모든 client의 실패 형태를 적고, PHP Dbspec::apply docblock은 그대로 전달되는 error를 나열한다. make dbspec-go-check, dbspec-ts-check, dbspec-rust-check(3 case 2회), dbspec-php-check, dbspec-apply-check(17 run), 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, docs-rules-check가 통과한다.[o]
T8.6.3PHP client에서 plan을 적용하고 복구한다 (T8.6.1 필요).—Evidence: Dbspec::apply와 Dbspec::recover 뒤의 clients/php/src/Dbspec/PlanApply.php, 그리고 ApplyEvent와 ApplyError(code locked, interrupted, drift, chain, failed, verify). 이것 없이 apply test는 Call to undefined method Orm\Dbspec\Dbspec::apply()로 실패했다. make dbspec-apply-php-check는 16 run을 통과한다. MySQL, PostgreSQL, SQLite에서 두 plan의 chain이 done history row 2개, plan 2개, verified 2개, done 2개와 같은 수의 statement와 applied event를 남기고, 다시 apply하면 event가 없다. 세 database에서 drift, lock, verify error가 나고, PostgreSQL과 SQLite는 멈춘 statement 뒤에 rollback한다. MySQL은 statement 전과 후에 멈춘 뒤 복구하며, effect 검사가 늘 effect가 없다고 하면 recover_after_applied가 Unknown column 'mail'로 실패한다. PHP introspection은 Go engine과 byte 단위로 같은 table query로 dbspec$plans를 뺀다. make dbspec-php-check, dbspec-introspect-php-check, dbspec-plan-php-check와 interface-check가 통과한다.[o]
T8.6.3.1실패한 PHP apply의 정리 error를 보고한다 (T8.6.3 후속). Cause: PlanApply는 예전 Go 동작을 따라 apply 본문이 실패하면 RELEASE_LOCK, pg_advisory_unlock, ROLLBACK, COMMIT, PRAGMA foreign_keys = ON의 error를 버렸고, 실패한 BEGIN IMMEDIATE 뒤의 복원을 무시했으며, 아무것도 풀지 않은 advisory unlock을 받아들였고, row 없는 MySQL 효과 query를 값 false로 읽었고, result를 닫는 결과를 확인하지 않았다. 모든 error를 보고한다는 규칙에 어긋난다.—Evidence: clients/php/tests/dbspec_apply_cleanup_test.php는 SQLite PDO를 감싸(FailingPdo, FailingStatement) 실패를 주입한다. 수정 전에는 4 case가 실패했고(RuntimeException "stop" alone; want it with the cleanup errors; locked error를 잃은 PDOException "restore failed" alone; UnexpectedValueException "Catalog value 0 is bool, not an integer"; 닫을 수 없는 result에 no error), clients/php/tests/dbspec_apply_test.php의 새 PostgreSQL run unlock_not_held는 succeeded로 실패했다. PlanApply::finish가 모든 정리 단계를 실행하고 error 하나는 그대로, 여럿이면 previous throwable이 첫 실패이고 cleanup이 뒤의 error를 담은 ApplyCleanupError를 던지며, queryValue가 row 없음과 닫기 실패를 보고하고, PostgreSQL unlock이 잡지 않은 lock을 거부하자 통과한다. make dbspec-php-check가 cleanup test를 두 번 실행하고 make dbspec-apply-php-check는 17 run을 통과한다. 기록한 PHP symbol로 interface-check가 통과한다.[o]
T8.6.4TypeScript client에서 plan을 적용하고 복구한다 (T8.6.1 필요).—Evidence: clients/typescript/src/dbspec/apply.ts의 applyPlans, recoverPlans, DbspecApplyEvent, DbspecApplyError. 이것 없이 apply test는 load되지 않았다(does not provide an export named 'DbspecApplyError'). make dbspec-apply-ts-check는 16 run을 통과한다: MySQL, PostgreSQL, SQLite에서 두 plan chain과 done history row 두 개, plan·verified·done event 각 두 개, 같은 수의 statement와 applied event, event 없는 두 번째 apply; 세 database의 drift, lock, verify error; PostgreSQL과 SQLite의 중단 뒤 rollback; statement 앞과 뒤에서 멈춘 뒤의 MySQL recovery이며, 효과 확인이 늘 없음을 알리면 recover_after_applied가 실패한다. introspection은 Go와 같은 query text로 dbspec$plans를 빼며 make dbspec-introspect-ts-check, dbspec-plan-ts-check, dbspec-ts-check, ts-check, interface-check가 통과한다.[o]
T8.6.5Rust client에서 plan을 적용하고 복구한다 (T8.6.1 필요).—Evidence: clients/rust/orm/src/dbspec/apply.rs의 orm::dbspec apply, recover, ApplyConnection, ApplyEvent, ApplyError. 이것 없이 apply test는 build되지 않았다(unresolved imports orm::dbspec::apply, orm::dbspec::recover). make dbspec-apply-rust-check는 16 run을 통과한다: MySQL, PostgreSQL, SQLite에서 두 plan chain과 done history row 두 개, 맞는 event, event 없는 두 번째 apply; 세 database의 drift, 두 번째 session의 lock, verify error; PostgreSQL과 SQLite에서 멈춘 statement 뒤 rollback; statement 앞과 뒤에서 멈춘 뒤의 MySQL recovery이며, 효과 확인을 끄면 recover_after_applied가 실패한다. Rust introspection은 Go와 같은 query text로 dbspec$plans를 빼며 make dbspec-introspect-rust-check(10 set, 7 case), dbspec-plan-rust-check(27 run), dbspec-rust-check, rust-check, interface-check가 통과한다.[o]
T8.6.6다른 client가 적용한 chain을 모든 client 쌍에서 이어 적용한다 (T8.6.2-T8.6.5 필요).—Evidence: tests/dbspec/apply에 client마다 apply runner(Go, PHP, TypeScript, clients/rust/orm/examples/dbspec_apply.rs)가 있고 action은 apply-first, apply, stop, recover다. tests/dialects/apply_clients_physical_test.go TestApplyChainAcrossClients는 pid와 random 값으로 이름 붙인 run별 database에서 MySQL, PostgreSQL, SQLite의 순서쌍 12개(client A가 create-from-empty를, client B가 chain을 적용)와 MySQL recovery 12개(A가 rename-table-and-column의 statement 1 뒤에 멈추고, row가 running 1이며, B의 apply가 error interrupted이고, B가 recover)를 실행하며, hash, done, step, applied_at을 가진 history row 2개와 chain target의 introspect한 schema text를 요구한다. 처음부터 통과했다. PHP가 empty 대신 빈 from_hash를 쓰게 한 번 바꾸면 PHP가 먼저 적용하는 run 12개가 실패하고(go apply: "error chain" after 20ms, want "ok"와 다른 history row) 나머지 36개는 통과했으며, 되돌린 뒤 모두 통과한다. make dbspec-apply-pairs-check(48 run, check aggregate에 포함), go-fmt-check, rust-fmt-check, rust-check, interface-check, docs-rules-check가 통과하고, MySQL과 PostgreSQL에 dbspec_pair_ database가 남지 않는다.[o]
T8.6.7apply lock을 database 하나로 한정하고 빈 plan chain을 받아들인다 (T8.6.6 후속). Cause: MySQL GET_LOCK('dbspec$plans')는 server 전체이고 PostgreSQL advisory lock은 database의 모든 schema를 덮어서, 두 database나 schema에 대한 apply가 locked로 부딪힌다. 또 chain은 빈 목록을 "no plan starts from empty"로 거부하지만, plan 없는 project는 table 없는 database의 올바른 빈 chain이다. acceptance: docs/plans.md가 database나 schema별 lock과 빈 chain을 정하고 모든 client가 공유 case로 따른다.—Evidence: docs/spec/plans.md "Lock"은 database 하나의 lock을 정한다: MySQL GET_LOCK(CONCAT('dbspec$plans$', LEFT(SHA2(DATABASE(), 256), 51)), 0)은 database 이름 길이와 상관없이 MySQL 한도인 64자 이름이고, PostgreSQL은 pg_try_advisory_lock(hashtext('dbspec$plans'), hashtext(current_schema()))다. MySQL 1/0이나 PostgreSQL true/false가 아닌 결과는 error이며, 이전에는 Go, PHP, TypeScript, Rust가 MySQL NULL을, PHP가 PostgreSQL NULL을 locked로 읽었다. "Chain"과 "Recovery"는 빈 chain을 정한다: plan이 없으면 올바른 chain이고, 그 apply는 history table을 만들고 table 없는 database를 요구하며 event를 알리지 않는다. tests/dbspec/plans.json에 chain case empty(plans: [], order: [])를 더했다. Red: Go TestPlanChains/empty, PHP clients/php/tests/dbspec_plan_test.php, TypeScript plan chain empty, Rust plan_chains가 "no plan starts from empty"로 실패했다. lock을 잡은 apply의 event에서 두 번째 database(MySQL), schema(PostgreSQL), file(SQLite)에 chain을 적용하는 새 other_database scenario는 Go(make dbspec-apply-check), PHP, TypeScript, Rust의 MySQL과 PostgreSQL에서 locked: another session holds GET_LOCK('dbspec$plans')와 locked: another session holds the advisory lock of dbspec$plans로 실패했고, 새 key를 잡거나 푸는 lock과 unlock_not_held scenario는 이전 key에서 실패했다. Green: 네 chain 함수는 plan이 없으면 빈 chain을 돌려주고 네 applier는 새 lock 이름과 key를 쓴다. make dbspec-apply-check가 통과하고, make dbspec-apply-php-check는 runs=23, make dbspec-apply-ts-check는 apply runs: 23, make dbspec-apply-rust-check는 23 runs on three databases를 보고하며, 각각 MySQL, PostgreSQL, SQLite에서 other_database와 empty_chain(빈 chain을 event 없이 apply와 recover, history row 0개, table을 만든 뒤 drift)을 포함한다. Go, PHP, Rust probe harness는 두 번째 database, schema, file <name>_b를 만들고 지운다(tests/dialects/README.md). 통과한 gate: 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, checklist-check.[o]
T8.6.8MySQL, PostgreSQL, SQLite에서 하나의 단계별 보장과 rollback statement로 plan을 적용한다 (T8.6.2 후속). Cause: docs/spec/plans.md "Apply"는 database마다 다른 보장을 준다. PostgreSQL은 plan을 transaction 하나로 되돌리고, SQLite는 apply 전체를 되돌리며, MySQL은 DDL이 암묵적으로 commit되므로 statement마다 commit해서, 같은 plan이 database마다 다르게 실패하고 복구된다. transaction 하나로 하는 PostgreSQL apply는 바뀌는 모든 table에 commit까지 ACCESS EXCLUSIVE lock을 잡아 apply 내내 그 table의 모든 읽기와 쓰기를 막으며, 2000-table round trip의 첫 plan(statement 20014개)에서 out of shared memory로 실패했다. 전부 또는 전무인 schema 변경은 점검 시간이나 blue-green 전환이 필요하고, 이는 운영자가 고르며 orm이 강제할 수 없다. acceptance: docs/plans.md가 세 database에 하나의 보장을 정한다. DDL은 transaction 밖에서 statement 단위로 실행하고 각 statement 뒤에 history 단계를 기록한다. 중단된 apply는 알려진 단계에서 멈추고, recover는 catalog로 마지막 statement의 적용 여부를 판단해 이어 간다. 모든 statement는 rollback statement를 가지며, rollback은 적용된 단계를 역순으로 정확히 되돌리고 그 자체도 중단 뒤 이어 갈 수 있다. data를 지우는 statement는 data를 옆으로 옮기고 마지막 단계가 지우므로 그 단계 전까지 모두 되돌릴 수 있다. lock 대기는 대기열을 만들지 않고 바로 실패한다. 문서는 원자성이 운영자의 선택(점검 시간이나 blue-green)이라고 적는다. 모든 client가 세 database에서 각 단계를 중단하고 되돌리는 공유 case로 따르고, 추적되는 case가 세 database 모두에서 2000-table plan을 적용한다.—Evidence: docs/spec/plans.md "Steps"와 "Apply", 그리고 한국어 쌍은 세 database에 하나의 보장을 정한다: 모든 statement는 transaction 밖에서 따로 commit하고 그 step을 기록하며, 모든 step은 rollback statement와 효과를 가진다. 지우는 table과 column은 finalize까지 dbspec$hold$<h>$<n> 이름을 붙여 숨기고, 더한 column은 rollback에서 숨겼다가 다시 적용할 때 돌려주며, SQLite 다시 만들기는 dbspec$rebuild를 거쳐 row를 양쪽으로 옮기고, 적용한 plan의 rollback은 non-null로 되돌릴 column의 NULL row를 source default로 채우거나 nulls로 멈춘다. 되돌릴 수 없는 step은 time과 datetime의 정밀도 넓히기뿐이고, lock 대기는 5초에 끝나며, 원자성은 운영자의 선택이다. Go PlanSteps, Apply, Recover, Rollback, Finalize, PHP Dbspec::planSteps, apply, recover, rollback, finalize, TypeScript planSteps, applyPlans, recoverPlans, rollbackPlans, finalizePlans, Rust plan_steps, apply, recover, rollback, finalize가 PlanStatements, plan transaction, MySQL statement 효과 표를 대신하며, 네 client의 introspection은 dbspec$ table과 column을 모두 뺀다. tests/dbspec/plans.json은 새 case representative와 drop-required-column을 포함한 12 case의 세 dialect step을 적는다. Red: 이전 구현에서 새 test는 build되거나 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), recover와 rollback이 catalog 효과를 무시하면 make dbspec-apply-check가 119 run 중 69 run에서 실패했다(mysql.apply.interrupt_00: rollback이 미지원 trigger를 남김). Green: make dbspec-apply-check가 119 run을 통과한다: history와 event를 가진 chain, drift, lock, 두 번째 database, 네 명령의 빈 chain, 아무것도 풀지 않은 unlock, 검증, rows_between(적용 뒤 쓴 row가 숨긴 column의 값이나 default와 함께 rollback 뒤에도 남고, 다시 적용하면 더한 column의 값이 돌아오며, finalize 뒤 dbspec$ table과 column이 없고 그 뒤 rollback은 irreversible), nulls(has 1 NULL rows, 바뀐 것 없음), 그리고 representative의 모든 step(MySQL 25, PostgreSQL 31, SQLite 38) 뒤의 중단마다 source로의 rollback, target으로의 recover, 멈춘 rollback의 계속을 history row와 함께 확인한다. TestApplyStressPlan은 2000 table 문서를 첫 plan으로 MySQL에서 22000 step(15m9s), max_locks_per_transaction 64인 PostgreSQL에서 22000 step(1m54s), SQLite에서 12000 step(46s)으로 적용하고 각각 history row applied로 끝난다. make dbspec-apply-php-check, dbspec-apply-ts-check, dbspec-apply-rust-check가 같은 119 run을 통과하고, make dbspec-plan-check와 그 PHP, TypeScript, Rust 변형이 각각 36 run을 통과하며 되돌릴 수 있는 모든 case를 되돌렸다가 다시 적용한다. make dbspec-apply-pairs-check가 108 run(세 database에서 모든 client 순서쌍의 chain, recover, rollback)을 통과하고, make dbspec-compare-check가 네 client의 step과 그 rollback, 효과, restore, null 검사가 같기를 요구한다. MySQL과 PostgreSQL은 datetime(6) column을 datetime(0)으로 좁힐 때 03:04:05.6을 error 없이 03:04:06으로 바꾸고, 긴 varchar를 좁히면 실패하므로(MySQL ERROR 1265, PostgreSQL value too long) 정밀도 넓히기가 되돌릴 수 없는 step이다. SQLite의 ALTER TABLE … RENAME TO는 legacy_alter_table이 꺼져 있을 때만 다른 table의 foreign key를 데려가며, 모든 명령이 이를 끈다. 통과한 gate: 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, checklist-check.[o]
T8.7표준 Mermaid erDiagram export와, 버린 정보의 전체 목록을 보고하는 import를 만든다(T8.2 선행).—Evidence: T8.7.1에서 T8.7.6까지 끝났다. docs/mermaid.md가 export와 import를 정한다. Go, PHP, TypeScript, Rust가 tests/dbspec/mermaid.json의 case 13개를 통과하며, 그 가운데 export가 객체 17개, import가 9개를 빼는 schema/bench.dbs round trip이 있다. make dbspec-compare-check는 모든 Mermaid case에서 네 client가 byte 단위로 같음을 확인한다.[o]
T8.7.1Mermaid export와 import를 docs/mermaid.md와 Korean pair에 정하고 tests/dbspec/mermaid.json의 공유 case를 둔다 (T8.2 필요).—Evidence: docs/mermaid.md와 docs/mermaid.ko.md는 export 형식(decimal(p-s) type, PK/FK/UK key, column 뒷부분 comment, relationship 줄과 label)과 export가 빼는 것, import 문법과 import가 옮기는 것, 보고 kind를 정한다. tests/dbspec/mermaid.json은 export case 1개, import case 2개(다시 읽은 export와 다른 도구의 diagram), invalid case 4개를 둔다. docs-rules-check가 24쌍을 통과한다.[o]
T8.7.2Go engine에서 Mermaid diagram을 export하고 import한다 (T8.7.1 필요).—Evidence: engine/dbspec/mermaid.go의 ExportMermaid와 ImportMermaid. 이것 없이 test는 build되지 않았다(undefined: ExportMermaid). make dbspec-go-check는 tests/dbspec/mermaid.json의 export, import, invalid case를 통과한다. schema/bench.dbs를 export하고 다시 import하면 table, column, key, foreign key가 되살아나고 export가 뺀 객체 16개를 보고한다.[o]
T8.7.2.1모든 client의 Mermaid 결함을 고친다 (T8.7.2 후속). Cause: Go ImportMermaid는 column 수와 참조 column 수가 다른 relationship label에서 panic(index out of range)한다. Go mermaidImportType은 strconv.Atoi error를 버려 int 범위를 넘는 length나 precision이 0이 되고, client마다 그런 type을 판정하는 지점이 달라 key column에서 dropped 목록이 다르다. export는 table과 column comment만 보고하고 primary key, unique, index, foreign key, check, settings, use 줄의 comment, block의 닫는 comment, 문서 끝 comment를 말없이 뺀다. import comment pattern은 nullidentity를 null identity로 받는다. 같은 FK column의 relationship 두 개는 index ix_<table>_<columns>를 두 번 더하고 두 번 보고한다. T8.7.2 evidence는 schema/bench.dbs round trip이 객체 16개를 보고한다고 하지만 engine은 17개를 보고하고 이를 assert하는 test가 없다.—Evidence: tests/dbspec/mermaid.json의 case는 export 2개, import 6개, invalid 4개, round trip 1개가 되었다. 수정 전 Go는 import/label-column-count에서 panic(index out of range [1] with length 1)했고, 그것을 고친 뒤 export/every-comment, import/type-ranges(table c와 d를 뺌), import/comment-spacing(nullidentity를 null identity로 읽음), import/repeated-relationship(ix_b_a_id를 세 번 보고)에서 실패했다. PHP는 export/every-comment에서 실패하고, import/label-column-count에서 TypeError(Argument #2 ($n) must be of type string, null given)로 멈췄으며, import/type-ranges, import/comment-spacing, import/repeated-relationship에서 실패했다. TypeScript와 Rust는 export/every-comment, import/type-ranges(varchar(16384) code PK가 있는 table e를 뺌), import/comment-spacing, import/repeated-relationship에서 실패했다. 각 client가 column 수와 참조 column 수가 다른 label의 column을 보지 않고, key를 읽기 전에 dbspec type 범위(varchar 1-16383, time과 datetime 0-6, decimal p 1-18과 s 0-p)를 판정하고, export가 빼는 모든 comment를 docs/mermaid.md가 이제 정하는 kind로 보고하고, comment 부분 사이에 공백 하나를 요구하고, column 목록의 index를 한 번 더하자 네 client 모두 13개 case를 통과한다. round trip case는 모든 client에서 schema/bench.dbs를 export하면 나열된 객체 17개를 정확히 빼고, export를 import하면 나열된 객체 9개를 정확히 빼며 schema/bench.dbs의 table, column, primary key, foreign key가 되살아남을 assert한다. 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, checklist-check가 통과한다.[o]
T8.7.3PHP client에서 Mermaid diagram을 export하고 import한다 (T8.7.1 필요).—Evidence: Dbspec::exportMermaid와 Dbspec::importMermaid 뒤의 clients/php/src/Dbspec/Mermaid.php. 이것 없이 Mermaid test는 Call to undefined method Orm\Dbspec\Dbspec::exportMermaid()로 실패했다. make dbspec-php-check는 clients/php/tests/dbspec_mermaid_test.php를 두 번 실행하고, 각 실행은 tests/dbspec/mermaid.json의 case 7개를 통과한다. export case 1개는 Mermaid text와 dropped 객체 7개, import case 2개는 emit한 문서와 dropped 객체 2개와 7개, invalid case 4개는 mermaid diagnostic이 같다. interface-check가 통과한다.[o]
T8.7.4TypeScript client에서 Mermaid diagram을 export하고 import한다 (T8.7.1 필요).—Evidence: clients/typescript/src/dbspec/mermaid.ts의 exportMermaid, importMermaid. 이것 없이 test는 load되지 않았다(does not provide an export named 'exportMermaid'). make dbspec-ts-check는 clients/typescript/tests/dbspec-mermaid.mjs에서 tests/dbspec/mermaid.json의 export case 1개, import case 2개, invalid case 4개를 통과하며, Mermaid text, emit한 문서, 뺀 [kind, table, name], diagnostic [rule, line, column]이 vector와 같다.[o]
T8.7.5Rust client에서 Mermaid diagram을 export하고 import한다 (T8.7.1 필요).—Evidence: clients/rust/orm-schema/src/dbspec/mermaid.rs의 orm_schema::dbspec export_mermaid, import_mermaid, RULE_MERMAID. 이것 없이 Mermaid test는 build되지 않았다(unresolved imports orm_schema::dbspec::export_mermaid, orm_schema::dbspec::import_mermaid). make dbspec-rust-check는 tests/dbspec/mermaid.json의 case 7개를 두 번 통과한다: Mermaid text와 뺀 객체가 맞는 export case 1개, emit한 문서와 뺀 객체가 맞는 import case 2개, [rule, line, column] diagnostic이 맞는 invalid case 4개. rust-check와 interface-check가 통과한다.[o]
T8.7.5.1Rust dbspec test를 네 번이 아니라 두 번 실행한다 (T8.7.5 후속). Cause: Rust Mermaid test와 public model test를 merge하면서 make dbspec-rust-check에 같은 cargo test 줄이 두 쌍 남았다. 한 쌍은 dbspec_model로, 다른 쌍은 dbspec_mermaid로 끝나서 나머지 Rust dbspec test가 모두 네 번 실행됐다.—Evidence: recipe는 --test dbspec_model --test dbspec_mermaid를 가진 cargo test 줄 하나를 두 번 둔다. make dbspec-rust-check가 통과하고 PASS mermaid/export/shop과 pass dbspec_model을 각각 두 번 출력한다.[o]
T8.7.6make dbspec-compare-check에서 네 client의 Mermaid 결과를 비교한다 (T8.7.2-T8.7.5 필요).—Evidence: tests/dbspec/compare/check.mjs는 tests/dbspec/mermaid.json을 다섯째 인자로 받고, Go, PHP, TypeScript, Rust runner(tests/dbspec/compare/go, php.php, typescript.mjs, clients/rust/orm-schema/examples/dbspec_compare.rs)는 모든 export, import, invalid, round trip case를 출력한다: Mermaid text나 emit한 문서, reason 없는 = kind<TAB>table<TAB>name 형식의 뺀 객체, ! rule line column 형식의 diagnostic, 그리고 round trip은 schema/bench.dbs의 export와 그 import다. make dbspec-compare-check는 run마다 186개 대신 200개 case를 출력하고 8개 run이 일치한다. T8.7.2.1 이전 commit의 TypeScript Mermaid source로 build하면 typescript 1 differs from go 1 in mermaid/export/every-comment로 실패한다. tests/dbspec/compare/check.test.mjs는 다른 Mermaid 뺀 객체를 포함한 6개 test를 통과하고, docs/mermaid.md와 그 한국어 쌍이 비교를 설명한다. 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, checklist-check가 통과한다.[o]
T8.7.6.1dbspec test harness가 실패와 없는 입력을 보고한다 (T8.7.6 후속). Cause: Go TestMermaidVectors는 t.Errorf 뒤에도 PASS mermaid/<case>를 기록해서 실패한 case도 PASS 줄을 출력하고, tests/dbspec/compare/php.php는 입력을 (string) file_get_contents(...)로 읽어 읽지 못한 file이 빈 text가 된다. acceptance: 실패한 case는 실패만 출력하고, 모든 runner는 읽지 못한 입력을 path와 함께 0이 아닌 exit로 거부한다.—Evidence: engine/dbspec/harness_test.go TestCaseHarnessReportsOnlyFailure는 TestFailingCaseFixture를 DBSPEC_FAILING_CASE_FIXTURE=1인 하위 go test로 실행한다. 이 fixture는 첫 Mermaid export, import, invalid, round trip case와 첫 plan, invalid plan, chain, parse case를 각각 기대값 하나만 틀리게 검사하고, test는 8개 모두에 FAIL <case> 줄이 있고 PASS <case> 줄이 없기를 요구한다. 변경 전에는 Mermaid와 plan 검사가 t.Errorf 뒤에 PASS를 기록하고 FAIL 줄은 쓰지 않아서 8개 모두에서 실패했다(no FAIL line, PASS line for the failing case). Mermaid 검사는 deadline 없이 deadline=5s도 기록했다. 이제 TestMermaidVectors, TestPlanVectors, TestPlanChains, TestPlanParseErrors의 검사는 불일치를 errors.Join으로 묶어 runTimed에 돌려주고, runTimed가 5초 deadline을 지키며 PASS나 FAIL 중 하나만 기록한다. JSON encoding 실패도 보고한다. test가 통과하고 make dbspec-go-check가 실행한다. 다른 Go dbspec test는 이미 runTimed나 logResult로 기록했다. tests/dbspec/inputs.test.mjs는 네 compare runner를 5개 input과 round trip document 자리마다 없는 file과 directory로, 네 apply runner를 없는 plans file과 directory로, Rust stress example을 없는 document와 directory로 실행하고, 0이 아닌 exit와 stderr의 <path>: 를 요구한다. 변경 전에는 58개 test 중 11개가 실패했다: PHP compare runner는 directory인 stress document와 round trip document를 빈 text로 읽어 exit 0으로 끝났고, PHP stress document, PHP apply plans file, TypeScript compare stress와 round trip document, TypeScript apply plans file은 <path>: 없이 실패했다(PHP TypeError나 cannot read <path>, EISDIR message에 path가 없는 잡히지 않은 Node error). tests/dbspec/input.php dbspec_read_input과 tests/dbspec/input.mjs readInput은 directory나 읽을 수 없는 file을 <path>: <reason>과 exit 1로 거부하고, PHP와 TypeScript compare, apply runner는 모든 input을 이것으로 읽는다. 58개 test는 Rust apply와 stress example을 build하는 make dbspec-compare-check에서 통과한다. make dbspec-compare-check(8번의 실행이 202개 case에서 일치), 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, checklist-check가 통과한다.[o]
T8.7.6.2engine test vector를 엄격하게 읽고 harness 주석을 한국어로 쓴다 (T8.7.6.1 후속). Cause: engine/dbspec/plan_test.go와 mermaid_test.go는 tests/dbspec vector를 json struct로 decode해서 없는 field가 빈 값이 되고, tests/dbspec/compare/{check.mjs, typescript.mjs, php.php, go/main.go}, clients/rust/orm-schema/examples/dbspec_compare.rs와 dbspec Makefile block의 주석은 한국어 주석 규칙과 달리 영어다. acceptance: engine test가 없거나 type이 틀린 vector field를 위치와 함께 거부하고, 그 주석이 한국어다.—Evidence: engine/dbspec/vector_reader_test.go는 tests/dbspec vector file을 JSON 값으로 읽고 version 1, 문자열 id를 가진 case object의 비지 않은 array인 모든 section, engine test가 쓰는 모든 field의 type을 요구한다: 문자열 줄, 줄 map, [kind, table, name] 세 값, 4개([rule, line, column, message 또는 null])나 Mermaid의 3개 항목인 diagnostic, {line, column, rule} error, null이거나 줄인 source의 존재, 없거나 boolean인 crlf와 mixed, order와 errors 중 정확히 하나를 가진 chain case다. 없거나 type이 틀린 값은 tests/dbspec/<file>: <location> <problem>이다. plans.json(plan_test.go, apply_test.go도 사용), mermaid.json(mermaid_test.go), cases.json(vectors_test.go, manifest_test.go), ddl.json(render_test.go)의 loader는 json struct 대신 이를 통해 decode한다. TestVectorLoadersRejectMalformedVectors는 네 file에서 값 15개를 빼거나 type을 바꾸고 그 error를 정확히 요구한다. json struct decode로는 15개가 모두 실패했고, 12개는 error가 없었고 3개는 file과 위치를 밝히지 않는 json error였다. 이제 make dbspec-go-check에서 모든 vector test와 함께 15개를 통과한다. tests/dbspec/compare/{check.mjs, typescript.mjs, php.php, go/main.go}, clients/rust/orm-schema/examples/dbspec_compare.rs, dbspec Makefile block 20개, engine/dbspec/{vectors, render, manifest, harness}_test.go의 영어 주석이 한국어다. 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, checklist-check가 통과한다.[o]
T8.9dbspec 파일 이름을 .dbs로 하고 header를 파일 signature로 삼는다(T8.7 선행). Cause: dbspec 파일은 DbSchema도 .dbs를 쓴다는 이유로만 확장자 dbspec을 썼다. header signature가 있으면 .dbs도 두 형식을 구별한다. 완료 기준: 모든 client, CLI, build tool, fixture, shared vector, 문서가 .dbs를 쓰고 확장자가 dbspec인 경로가 남지 않는다. docs/dbspec.md와 한국어 쌍은 첫 줄 dbspec 1 <document>가 파일 signature이며, 파일을 읽는 tool은 parse 전에 파일이 dbspec byte로 시작하는지 확인하고, 그렇지 않은 파일은 그 파일이 dbspec 문서가 아니라고 알리는 signature 오류라고 적는다. DbSchema project 파일(XML)과 빈 파일의 shared vector가 네 client 모두에서 signature 오류로 보고된다.—Evidence: 추적하는 문서 10개(schema/bench, contracts/fixtures의 audit, clock, clock_mark, decimal_schema, refusal, rollback, secret_config, zone, clients/rust/tests/nonnull)를 git mv로 .dbs로 옮기고 모든 참조를 따라 바꿨다: Go orm-gen gen --document, PHP orm-gen gen, TypeScript orm-gen gen --schema 사용법, orm-build MANIFEST_FILE orm_manifest.dbs, Makefile stress 문서 clients/rust/target/dbspec/stress.dbs, contracts/features.json, contracts/rules.json, contracts/interfaces.json, tests/dbspec/mermaid.json, script, test, README와 문서 및 그 한국어 쌍. 확장자가 dbspec인 경로를 찾는 git grep은 과거 변경을 적은 CHANGELOG 한 줄만 보인다. client마다 reader가 하나 있다: Go dbspec.ReadFile(engine/dbspec/file.go), ReadResult를 쓰는 PHP Dbspec::readFile, TypeScript readDbspecFile(src/dbspec/file.ts), ReadError를 쓰는 Rust orm_schema::dbspec::read_file. dbspec bytes로 시작하지 않는 파일은 parse 전에 text 없이 1:1의 signature diagnostic 하나와 message <path> is not a dbspec document를 돌려준다. tool이 이것을 쓴다: Go runtimemodel.LoadFiles(orm-gen, interface checker), PHP RuntimeModel::files(orm-gen)와 scripts/decimal-db-setup.php, TypeScript orm-gen, Rust orm-build와 integration binary, stress와 round trip 문서를 읽는 네 compare runner, Rust stress harness, schema 문서를 읽는 Go, PHP, TypeScript, Rust test. Dbspec.readFile은 contracts/interfaces.json의 interface 항목이며 네 symbol record가 reader를 기록한다. tests/dbspec/cases.json에 files section과 tests/dbspec/files/dbschema.dbs(table이 있는 DbSchema project XML), empty.dbs, shop.dbs를 더했다. reader가 없을 때의 Red: 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. tool을 거치면 XML과 빈 파일에 대해 Go LoadFiles는 1:1 header: document ?>, PHP orm-gen은 1:1: header, TypeScript orm-gen은 a dbspec text does not start with its header, orm-build는 1:1 header를 보고했다. Green: Go TestFileVectors와 TestLoadFilesRejectsFileWithoutSignature, PHP dbspec_test.php(files=3)와 orm_gen_test.php, TypeScript dbspec.mjs와 generate.mjs, Rust shared_dbspec_files와 orm-build rejects_a_file_without_the_signature가 통과한다. dbspec-compare-check에서 네 runner가 ! signature 1 1과 = <path> is not a dbspec document를 같게 출력하고(8 run 일치), inputs.test.mjs는 네 runner와 Rust stress harness가 signature 없는 stress 문서를 거부하는지 확인한다. docs/spec/dbspec.md "Files"와 rule 표가 signature를 적는다. main checkout의 TEST_ENV로 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, 네 dbspec-introspect check, dbspec-introspect-compare-check(CPU idle 75.6%, 7.3 MB/s에서 시작했고 client와 database마다 2000 table이 0.74-2.45 s), dbspec-plan-check와 그 PHP, TypeScript, Rust 변형, dbspec-apply-check(325 s)와 그 PHP, TypeScript, Rust 변형, dbspec-apply-pairs-check, conformance-result-check가 통과한다. make conformance-check는 세 database에서 vector 25개 × 4개 언어가 같게 통과한다.[o]
T8.9.1주어진 bytes에서 dbspec signature를 확인한다 (T8.9 후속). Cause: 각 client의 file reader는 path만 받으므로, 자기 규칙으로 파일을 읽어야 하는 호출자(예: root 아래에서 O_NOFOLLOW로 읽고 symbolic link를 따라가지 않는 호출자)는 모든 파일을 두 번 읽는다. 두 번째 읽기는 호출자의 규칙을 무시하고, 호출자가 parse하는 것과 다른 version을 판단할 수 있으며, 그 message는 자기가 연 path를 쓴다. signature 뒤의 UTF-8이 아닌 bytes도 reader마다 달랐다: Go와 PHP는 parser에 넘겼고, TypeScript는 U+FFFD로 바꿨으며, Rust는 I/O error를 돌려줬다. docs/dbspec.md는 parse 전의 encoding error를 요구한다. 완료 기준: 각 client는 파일 bytes와 호출자가 준 이름을 받아 1:1의 signature diagnostic <name> is not a dbspec document, 첫 잘못된 byte의 줄과 열의 encoding diagnostic <name> is not valid UTF-8, 또는 UTF-8 text를 돌려주는 byte check를 둔다: Go dbspec.ReadBytes, PHP Dbspec::readBytes, TypeScript readDbspecBytes, Rust dbspec::read_bytes. 각 client의 path reader는 파일을 읽어 그 bytes와 path를 byte check에 준다. 공유 file vector는 두 reader로 실행되고 make dbspec-compare-check가 둘을 비교한다. docs/dbspec.md와 한국어 쌍, contracts/interfaces.json(rule Dbspec.readBytes), 네 symbol 기록이 이를 적는다.—Evidence: tests/dbspec/cases.json은 file vector invalid-utf8-file(tests/dbspec/files/invalid-utf8.dbs, 3:8의 encoding)을 더한다. Red: Go file test는 build되지 않았고(undefined: ReadBytes), 그 파일에서 compare runner는 Go와 PHP parser의 ! encoding 3 8을 reader message 없이, byte를 U+FFFD로 decode한 TypeScript의 ! name.format 3 7을 출력했으며, Rust runner는 "invalid utf-8 sequence of 1 bytes from index 22"로 끝났다. Green: 모든 runner는 path reader의 files/<id>와 byte check의 files/<id>/bytes를 같은 rule, 위치, <name> message로 출력하고, make dbspec-compare-check가 통과한다("8 runs agree", run마다 출력 case 1116개). 네 file vector는 두 reader로 Go TestFileVectors(subtest 8개), PHP dbspec_test.php(files=8), TypeScript dbspec.mjs(test 245개, 실패 0), Rust shared_dbspec_files에서 통과한다. 실행한 owner check: 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, git-check. node scripts/features/check.mjs는 feature contract 17개로 통과한다.[o]
T8.8feat/dbspec-T8을 main에 merge하고 worktree를 제거한다(T8.0–T8.7, T8.9 선행). 완료 기준: 낡은 개발 규칙(auto, COLUMN_UNSELECTED, Mermaid가 source라는 규칙, full suite 실행 시점)을 다시 쓴다.—Evidence: T8.8.1-T8.8.5가 main의 N19.1까지 branch에 옮긴 뒤 T8.8가 feat/dbspec-T8을 main에 merge한다. 마지막 전체 make check는 T30에서 step 44개 중 43개를 통과했고, 남은 feature-check는 T30 뒤에 통과했다. AGENTS.md(+ko)의 낡은 auto 규칙은 dbspec identity 규칙으로 바뀌었고(T8.8), COLUMN_UNSELECTED와 Mermaid diagram 규칙은 여전히 현재 규칙이다. worktree orm-dbspec-T8과 branch는 지운다.[o]
T8.8.1main을 feat/dbspec-T8에 통합한다 (T8.0–T8.7 선행). 원인: 분기 이후 main에 11개 commit(N3.1..N15)이 생겼다: N3.1은 여러 schema의 PHP model을 함께 load하고, N3.2는 모든 client에 하나의 schema 규칙을 쓰고, N3.3은 설치된 schema를 등록하며, N10, N14, N15는 client clock, database clock, migration ledger를 microsecond로 쓰고, N11은 모든 driver error에 code를 붙이고, N12는 TypeScript scan에서 model chain 호출만 type으로 만들고, N13은 실패한 rollback을 두 error로 보고한다. 그중 여러 commit이 T8.2.6.4가 제거한 path(Mermaid fixture와 schema.json, PHP SchemaMigrate, TypeScript tools/migrate.ts와 tools/plan.ts, Rust orm-gen migrate, internal/ormgen migrate와 그 ledger orm_schema_migrations)를 고치고, N13과 T8.0.14.1/T8.0.14.3은 같은 transaction 종료 보고를 바꾸며, N10, N14, N15의 microsecond clock은 T8.0.9와 T8.0.9.1의 local datetime(p)와 UTC connection 규칙과 만난다. 완료 기준: main이 chore/integrate-T8.8에 merge되고 모든 conflict가 의미로 해결된다: Mermaid source path는 제거된 채로 남고, N3.1과 N3.3의 schema set과 등록은 dbspec document set과 manifest에서 동작하며 N3.2는 그에 맞게 다시 쓰이고, 모든 client에 transaction 종료 error 형태가 하나만 있고, clock 규칙은 한 번 쓰이며 T8.0.9와 맞고, 11개 commit의 모든 test가 그 path에서 실행되거나 이름이 있는 dbspec test로 대체되며, 네 symbol 기록, 이 항목 evidence의 gate와 전체 Go test suite가 통과한다.—Evidence: merge commit "Merge branch 'main' into chore/integrate-T8.8"의 parent는 T8.8.1와 N15다. 해결: (1) Mermaid fixture와 schema.json path, PHP SchemaMigrate, TypeScript tools/migrate.ts와 tools/plan.ts, Rust orm-gen migrate와 cli.rs, internal/ormgen migrate, rollback, ledger는 제거된 채로 남는다; (2) schema set은 process 단위로 동작한다: PHP Registry와 Engine은 manifest hash로 나뉘고 모든 generated PHP model이 manifest_hash를 가진다. 연결은 각 요청을 그 hash의 model로 계획한다. 읽히지 않은 manifest의 요청과 hash가 선언과 다른 generated text는 Go, PHP, TypeScript, Rust에서 실행 전에 SCHEMA_HASH_MISMATCH이며, cache된 engine이나 plan이 있어도 그렇다(Go engineFor와 Rust Db::plan은 cache를 먼저 보았고 Rust는 SCHEMA_INVALID를 보고했다). generated code가 자기 set을 등록하므로 register()는 제공하지 않고, docs/protocol.md가 N3.2를 다시 쓴다; (3) branch의 transaction 종료 code를 main의 오류 형식으로 유지한다: ROLLBACK transaction failed (<cause>) and rollback failed (<error>)는 모든 client에서 두 오류를 유지하고 재시도하지 않으며, server가 connection을 닫았을 때도 보고한다(Go는 driver.ErrBadConn을 끝난 rollback으로 보지 않는다. TestWithContextCancelsInsideTransaction은 MySQL과 PostgreSQL에서 context.Canceled를 유지한 ROLLBACK을 기대한다); (4) docs/protocol.md가 clock 규칙을 한 번 쓴다: 마이크로초의 UTC client clock, Now(precision)의 MySQL CURRENT_TIMESTAMP(p)와 NOW(6), 두 번째 slot의 소수 여섯 자리를 붙인 SQLite 상대 형식, 생략한 default now column에 SQLite가 bind하는 client clock(SQLite database clock은 millisecond라 이것 없이 clock_microseconds가 Go, PHP, TypeScript, Rust의 SQLite에서 실패했다), column의 소수 자리를 가진 모든 now slot(Go TestSoftDeleteClockSlotPrecision); (5) dbspec$plans.applied_at은 YYYY-MM-DDTHH:MM:SS.ffffffZ이고 TypeScript apply clock은 epoch 이후 microsecond이며 docs/plans.md가 이를 쓴다. Test: main의 clock, refusal(trigger_refused, check_refused), rollback(rollback_failed, savepoint_rollback_failed) case는 contracts/fixtures의 clock, clock_mark, refusal, rollback .dbs로 네 client에서 MySQL, PostgreSQL, SQLite로 실행한다. several_schemas는 남고, register_schema는 installed_elsewhere로, edited_manifest와 register_edited_manifest는 edited_manifest로 바뀌며, unregistered_schema는 PHP와 TypeScript에 남는다. ledger case migration_ledger_microseconds, migration_ledger_whole_seconds, migration_ledger_earlier는 clock 2026-10-01T00:00:00.123456789Z로 make dbspec-apply-check, dbspec-apply-php-check, dbspec-apply-ts-check, dbspec-apply-rust-check, dbspec-apply-pairs-check의 applied_at 확인으로 대체한다. history column은 text이고 초 단위의 이전 history가 없기 때문이다. SQLite conformance 기대값 write_cycle과 now_defaults는 네 출력이 일치해 기록했다. main checkout의 TEST_ENV로 "Merge branch 'main' into chore/integrate-T8.8"에서 checklist-check, docs-rules-check, docs-check(48 page, 550 internal target. checklist의 raw <nil>, <value> 인용 넷을 code span으로 바꿨다), 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(각 database에서 25 vector × 4 language 일치), client-db-check, dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check, dbspec-ddl-check, 네 dbspec-introspect check, dbspec-plan-check와 그 PHP, TypeScript, Rust 변형, dbspec-apply-check와 그 PHP, TypeScript, Rust 변형, dbspec-apply-pairs-check, example-check, timing-check, go vet ./..., go test -p 1 ./..., git-check가 통과한다. feature-check는 T10에서처럼 T15의 "missing coverage kind" 15개로만 실패한다.[o]
T8.8.2main을 feat/dbspec-T8에 다시 통합한다 (T8.8.1 후속). Cause: T8.8.1 뒤 main에 N16(N16)가 생겼다: 각 client의 test entry point가 rollback fault를 설정하고(Go는 build tag ormtest의 FailNextRollback, Rust는 feature test-faults의 orm::testing, TypeScript는 Node condition orm-test의 @polyspec/orm-typescript/testing, PHP는 Orm\Testing\Faults), callback이 실패한 다음 transaction의 rollback은 ROLLBACK 안의 catalog code FAULT로 보고된다. 이 commit은 T8.8.1이 하나의 ROLLBACK 형태로 바꾼 Go, PHP, TypeScript의 transaction 끝, dbspec fixture로 실행하는 TypeScript와 PHP rollback test, Rust와 client database gate를 고친다. 완료 기준: main이 feat/dbspec-T8에 merge되고, 각 client는 하나뿐인 transaction 종료 오류 형태(transaction failed (<cause>) and rollback failed (<error>))로 fault를 보고하며, rollback_fault case가 contracts/fixtures/rollback으로 네 client에서 MySQL, PostgreSQL, SQLite로 실행되고, 네 symbol 기록이 fault symbol을 담으며, 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와 모든 dbspec check가 merge된 tree에서 통과한다.—Evidence: merge는 path 41개를 해결했다. Go, PHP, TypeScript의 transaction 끝은 rollbackFailed로 fault를 보고하고, N16에서 가져온 comment는 한국어다. fault case는 MySQL, PostgreSQL, SQLite에서 통과한다: Go TestRollbackFault* test 4개, PHP database마다 case 3개, TypeScript --conditions=orm-test로 database마다 case 3개, Rust --features test-faults --test rollback test 10개. merge된 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와 dbspec-apply-check, dbspec-apply-pairs-check, dbspec-compare-check를 포함한 모든 dbspec check가 통과한다. dbspec-introspect-compare-check는 다른 실행개발 machine을 쓰는 동안 5 s budget에서 두 번 실패했다(PostgreSQL PHP 8.0-9.0 s, TypeScript 5.5-6.8 s, Rust 6.8 s, MySQL PHP 7.6 s, 1분 load는 96까지 올랐다). client 하나와 database 하나씩, CPU idle이 60% 이상이고 disk 처리량이 10 MB/s 미만일 때 시작해 조합마다 5번 잰 2000 table introspection은 Go 0.73-0.95 s, PHP 1.93-2.56 s, TypeScript 0.86-1.22 s(idle 21.7%에서 시작한 한 번은 2.58 s), Rust 0.86-1.20 s였다. process CPU time은 Go 0.43-0.82 s, PHP 1.66-2.04 s였다. 같은 idle 상태에서 시작한 make dbspec-introspect-compare-check는 모든 조합이 0.86-2.64 s로 통과했다.[o]
T8.8.3main을 feat/dbspec-T8에 다시 통합한다 (T8.8.2 후속). Cause: T8.8.2 뒤 main에 N17(N17)가 생겼다: clients/php/composer.json은 PDO driver 확장을 요구하지 않고 pdo_mysql, pdo_pgsql, pdo_sqlite를 제안하며, scripts/php-without-mysql.sh와 make php-without-mysql-check가 pdo_mysql 없는 공식 PHP image php:8.5.10-cli-trixie에서 실행하는 case clients/php/tests/sqlite_without_mysql.php는 branch가 없앤 Config(schemaPath:)와 schema/schema.json으로 schema를 설치한다. 완료 기준: main이 feat/dbspec-T8에 merge되고, case는 Dbspec::readFile로 읽은 schema/bench.dbs를 utils()->schema()->install()로 설치한 뒤 공식 PHP image의 SQLite에서 generated Polyspec\Orm\Tests\Model\Service로 row를 만들고 읽으며, make check와 .PHONY 목록이 php-without-mysql-check를 담고, N17에서 가져온 comment는 한국어이며, 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와 모든 dbspec check가 merge된 tree에서 통과한다.—Evidence: merge는 CHANGELOG.md, CHANGELOG.ko.md, Makefile, clients/php/README.md를 해결했고, composer.json은 main과 같다. Red: merge된 tree에서 main의 case는 container 안에서 38번째 줄의 Unknown named parameter $schemaPath로 실패했다(make php-without-mysql-check exit 255). Green: make php-without-mysql-check가 세 번 통과하고(각 2-3 s, case 48.3 ms), pdo_mysql을 load하는 host PHP로 실행한 같은 case는 this PHP loads pdo_mysql로 실패한다. merge된 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)와 dbspec-apply-check(453 s), dbspec-apply-pairs-check(98 s), dbspec-compare-check(38 s)를 포함한 모든 dbspec check가 통과한다. dbspec-introspect-compare-check는 다른 실행개발 machine을 쓰는 동안(CPU idle 0-40%, 1분 load 50) 5 s budget에서 한 번 실패했다: MySQL에서 PHP가 7.6 s 걸렸고 Go는 3.4 s, TypeScript는 3.0 s, Rust는 3.5 s였다. N17은 introspection code를 바꾸지 않으며, 이 check는 부모 merge T8.9(322 s)과 T15(357 s)에서 통과했다. contracts/features.json은 이 case를 dsn_connection에 verification make php-without-mysql-check와 함께 둔다. node scripts/features/check.mjs가 "php test file belongs to no feature"로 거부했기 때문이며, 그 뒤 feature check는 feature contract 17개로 통과한다.[o]
T8.8.4main을 feat/dbspec-T8에 다시 통합한다 (T8.8.3 후속). 원인: T8.8.3 뒤 main에 N18(N18), N19(N19), N19.1(N19.1)가 생겼다. N18은 ssl-mode=VERIFY_IDENTITY와 절대 경로 ssl-ca로 MySQL에 TLS 연결하고, PHP와 TypeScript client가 scheme마다 정한 집합 밖의 DSN parameter를 거부하게 하며, T8.2.6.4가 없앤 PHP SchemaImport와 TypeScript tools/db.ts의 tool 연결도 바꿨다. N19와 N19.1은 옛 schema 경로에 utils().schema().addColumns(manifestJson)을 더한다: manifest 사실을 붙인 live table의 Mermaid import(internal/ormgen/add_columns.go, PHP SchemaColumns, TypeScript schema_columns.ts, Rust orm_schema::catalog과 orm_schema::live), JSON fixture contracts/fixtures/add_columns_*, type, default, CHECK, index 이름 맞춤이다. 이 branch는 그 경로를 모두 없앴으므로 지운 것으로 해결하는 merge는 Go 쪽을 포함해 기능을 잃는다. 수용 기준: main이 merge되고, 각 기능이 dbspec 설계 위에서 Go, PHP, TypeScript, Rust로 MySQL, PostgreSQL, SQLite test와 함께 동작하며, version은 0.0.2이고, 옛 schema 형식은 돌아오지 않으며, mapping과 대응이 없는 부분을 여기에 기록한다.—근거: Mapping. N18: Go parseDSN/mysqlTLS와 openSQL(clients/go/orm/dsn.go, db.go), PHP Orm::parseDsn, mysqlTls, mysqlTlsOptions와 DSN_PARAMETERS(parseDsn은 CA 경로를 여섯째 원소로 돌려준다), TypeScript parseDsn, mysqlTls, mysqlSsl과 DSN_PARAMETERS(driver.ts), Rust는 그대로다(sqlx가 ssl-mode와 ssl-ca를 읽는다). test는 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와 make test-servers-tls는 그대로 받았다. PHP SchemaImport와 TypeScript tools/db.ts 변경은 대응이 없다: 이 branch의 어떤 tool도 DSN으로 연결을 열지 않고, dbspec introspect와 apply 함수는 호출자가 연 연결을 받는다. N19: utils().schema().addColumns(schema)(Go AddColumns, Rust add_columns)는 manifest JSON 대신 generated schema 값을 받아 연결의 database를 introspect하고, document set의 기존 table을 그 schema text의 compareSchemas로 비교하며, 모든 차이가 null이거나 default가 있는 column의 add_column이면 그 table들에서 set까지의 plan step을 실행한다. 새 순수 함수가 각 client dbspec module에서 그 step을 쓴다: Go dbspec.AddColumnSteps(engine/dbspec/add_columns.go), PHP Dbspec::addColumnSteps, TypeScript addColumnSteps(src/dbspec/add_columns.ts, client가 자기 pool로 introspect하도록 introspectCatalog와 함께), Rust orm_schema::dbspec::add_column_steps. plan step은 바뀐 table의 audit trigger를 바꾸고(dbspec audit trigger는 모든 column을 복사하므로 이력 table도 그 column을 얻는다), 각 column의 renderer CHECK를 더하며, SQLite에서는 table을 다시 만든다. 다른 모든 차이는 기존 column 앞에 더한 column의 reorder_columns와 set table의 읽지 못한 객체를 포함해 어떤 statement보다 먼저 각 <kind> <table>[.<name>]을 담은 SCHEMA_DIFFERS다. docs/spec/errors.yaml은 그 code를 dbspec 표현으로 두고 docs/spec/schema.md §5가 호출을 적는다. table이 자기 key를 참조하는 foreign key의 import 수정은 dbspec introspection이 대응하며, 세 database에서 addcol_tag의 fk_addcol_tag_parent를 복원한다. test는 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의 case add_columns, add_columns_differs, add_columns_transaction, add_columns_edited_manifest이고, dbspec fixture contracts/fixtures/add_columns/*.dbs(v1, v2, 다른 set log, 그리고 required, removed, changed, nullable, default, index, unique, reorder)를 쓴다. Go는 row와 audit 대상 update를 version 1과 version 2 model로 client를 거쳐 쓰고, 다른 client는 SQL로 쓴다. N19.1: 대응이 필요 없다: dbspec에는 bytes type 하나뿐이고 char(n)이 없으며, 이름은 63 byte 이하이고 줄이지 않으며, introspection이 canonical type, default, check, index 이름을 복원한다. fixture는 bytes column과 63 byte index ix_addcol_item_enabled_created_at_at_the_sixty_three_byte_limit를 가지며 case는 PostgreSQL에서 통과한다. 대응이 없는 부분: SQLite는 MySQL처럼 transaction 밖에서 column을 더하고 안에서는 CONFIG를 돌려준다. column을 더하는 plan step은 foreign key를 끈 채 table을 다시 만드는데 SQLite는 transaction 안에서 foreign_keys를 바꾸지 않기 때문이다. 연결 하나에서 BEGIN IMMEDIATE transaction 하나로 step을 실행하고, PRAGMA foreign_key_check가 row를 돌려주지 않아야 하며, foreign key를 다시 켠다(case는 같은 연결이 뒤에 없는 item의 tag를 거부하는지 확인한다). addColumns 뒤의 install은 set에 늘어난 table을 만들지 않는다. 여기서 install은 set을 통째로만 만들고 일부만 설치된 set에는 CONFIG를 돌려주기 때문이며, case는 그 CONFIG를 요구한다. schema로 한정한 table 이름(main의 CAPABILITY_UNSUPPORTED)은 dbspec에서 생길 수 없다. 더한 column은 manifest 순서가 아니라 table 이름 순, 그다음 column 순서다. catalog 읽기를 orm_schema::catalog으로 옮긴 Rust 변경은 runtime과 tool이 함께 쓰는 orm::dbspec::introspect가 대응한다. N19의 TypeScript DriverPool.session은 MySQL install의 unprepared를 대신하고, SQLite pool은 foreign key 변경을 위한 session을 가진다. N16과 N17: Go 부분이 이 branch에 있다: clients/go/orm/fault_ormtest.go, rollback_fault_test.go, rollback_fault_entry_test.go, tx.go의 fault 확인(N16)이며, N17에는 Go 부분이 없다. 둘 다 internal/ormgen에 있던 것은 없다. Red: Go TestAddColumns*는 아무것도 더하지 않는 stub에서 AddColumns = [], <nil>과 SCHEMA_DIFFERS 없음으로 12개 모두 실패했다. PHP는 branch utilities에서 Call to undefined method Orm\SchemaUtils::addColumns()로 12개 모두, TypeScript는 stub에서(addColumns = [], no such column: note, Unknown column 'note', column "note" does not exist, required: no error) 12개 모두, Rust는 stub에서 4개 모두 실패했다. N18은 branch의 parsing에서 PHP와 TypeScript case가 거부해야 할 DSN 17개를 모두 받아들이고 빈 Ssl_version을 읽고 잘못된 server 둘을 모두 받아들였으며, Go case는 sslCA와 sslHost 없이 compile되지 않았다. Green: add-columns case가 네 client에서 MySQL, PostgreSQL, SQLite로 통과하고(Go test 12개 1.8 s, PHP case 12개 1.4 s, TypeScript case 12개 1.3 s, Rust test 4개 0.7 s), TLS case가 네 client에서 통과한다(TLSv1.3, 잘못된 server 둘 거부). node scripts/features/check.mjs --run --feature schema_install과 --feature dsn_connection이 새 verification 명령 install-add-columns-*, dsn-mysql-tls-*와 함께 통과하고, interface-check는 다시 기록한 symbol 목록과 GeneratedSchema를 받는 SchemaUtils.addColumns contract로 통과한다. Check: merge한 tree에서 make check의 모든 step이 통과한다. 다른 worktree가 build하는 동안 남은 disk가 1 GB로 줄어 공유 server가 쓸 수 있게 전체 실행을 두 번 멈추고 실행 사이에 Rust target을 지웠다: 한 실행은 처음 29 step을 통과하고(client-db-check 163 s, client-pooler-check 134 s, conformance-check 92 s, rust-check 66 s, ts-check 28 s) 통과한 dbspec-introspect-rust-check 뒤에 멈췄으며, make check CHECK_TARGETS=<나머지 15개>가 dbspec-introspect-compare-check(49 s), dbspec plan과 apply check, dbspec-apply-pairs-check(32 s), ts-model-check, feature-check(448 s), go-test-check를 통과했다. 멈춘 두 실행의 database는 지웠다.[o]
T8.8.5설치한 set을 올릴 때 새 version이 더한 table을 만든다 (T8.8.4 후속). 원인: main에서는 module을 addColumns와 이어지는 install로 올렸고, install이 module에 새로 생긴 table을 만들었다(N19). 이 branch에서는 install이 set을 통째로만 만들고 addColumns는 column만 더하므로 table을 더한 module version을 적용할 수 없었다. 인수 조건: 설치한 set을 더해서만 올리는 호출이 database에 없는 set의 table을 같은 plan step으로 index, foreign key, check, audit과 immutable trigger와 함께 모두 만들고, 빠진 null이거나 default가 있는 column을 더하며, 다른 모든 차이는 어떤 statement보다 먼저 SCHEMA_DIFFERS로 거절한다. Go, PHP, TypeScript, Rust에서 MySQL, PostgreSQL, SQLite로 확인한다.—증거: 호출 이름이 하는 일을 말하도록 alias 없이 모든 곳에서 바꾸었다: utils().schema().addTablesAndColumns(schema)(Go AddTablesAndColumns, Rust add_tables_and_columns)와 순수 step 함수 Go dbspec.AddTablesAndColumnsSteps(engine/dbspec/add_tables_and_columns.go), PHP Dbspec::addTablesAndColumnsSteps, TypeScript addTablesAndColumnsSteps(src/dbspec/add_tables_and_columns.ts), Rust orm_schema::dbspec::add_tables_and_columns_steps. 각 함수는 database에 있는 set의 table을 set 전체와 비교하고, create_table과 null이거나 default가 있는 column의 add_column만 받아들이며, 그 table에서(하나도 없으면 empty에서) set까지의 plan 하나를 쓰므로 만드는 table은 적용한 plan과 같은 step을 갖는다. 만든 table은 table, 더한 column은 table.column으로 table 이름 순으로 돌려준다. fixture는 contracts/fixtures/add_tables_and_columns로 옮겼다: v2는 audit table addcol_extra(index, addcol_item으로의 foreign key, check)와 그 이력 table을 더하고, required는 default 없는 non-null column 옆에 같은 table을 더한다. 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, clients/rust/orm/tests/add_tables_and_columns.rs는 결과의 두 table, addcol_extra에 넣은 row가 action insert로 이력에 남는 것, 거절된 없는 item의 insert, 거절된 빈 label, 거절된 delete, 아무것도 더하지 않는 반복 호출, 그 뒤 아무것도 바꾸지 않는 version 2의 install, 그리고 SCHEMA_DIFFERS fixture 여덟 개 뒤에 다시 나오는 전체 결과를 요구하므로 거절된 required version은 table을 만들지 않았다. docs/spec/schema.md §5(+ko), docs/usage, docs/interfaces, docs/errors.yaml과 다시 생성한 Go, PHP, Rust code, PHP와 Rust README, contracts/features.json(verification install-add-tables-and-columns-*), 출력 AddedTablesAndColumns인 contracts/interfaces.json의 SchemaUtils.addTablesAndColumns 항목, 다시 기록한 symbol inventory가 이를 적는다. Red: fixture와 test를 바꾸고 옛 구현으로 Go는 12개 중 12개(addcol_extra addcol_extra_history 없는 AddColumns = [addcol_item.note ...]), PHP 12개 중 12개, TypeScript 12개 중 12개(같은 결과와 no such table: addcol_extra, Table '...addcol_extra' doesn't exist, relation "addcol_extra" does not exist), Rust 4개 중 4개가 실패했다. Green: Go test 12개(2.4 s), PHP case 12개, TypeScript case 12개, Rust test 4개(0.7 s)가 MySQL, PostgreSQL, SQLite에서 통과한다. node scripts/features/check.mjs --run --feature schema_install, interface-check, go-fmt-check, rust-fmt-check가 통과한다.[o]
T9PHP client의 dbspec 표면 전체를 C로 쓴 PHP 확장에, 같은 인터페이스의 독립된 다섯 번째 구현으로 만든다(T8.2.5 선행). 원인: 이 항목이 Dbspec.parse, Dbspec.emit, DbspecDiagnostic만 적었으므로 T9.1~T9.5는 Rust dbspec 위의 ext-php-rs 확장으로 메서드 여섯 개와 opaque Document만 만들었다. 사양은 PHP client Orm\Dbspec\Dbspec(clients/php/src/Dbspec/Dbspec.php)이고, 확장은 그 public 메서드 전부와 결과, 값 class 전부를 같은 이름과 property 순서로 ordered-json php-extension처럼 C로 제공해야 한다. 완료 조건: Orm\Dbspec\Native\Dbspec이 parse, readFile, readBytes, emit, render, manifest, introspect, externalDifferences, parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps, apply, recover, rollback, finalize, exportMermaid, importMermaid를 제공하고, Native\Document가 PHP Document의 public 가변 property graph를 가지며, interface contract가 네 언어와 확장의 모든 rule을 선언하고, make dbspec-compare-check가 모든 input에서 확장을 비교하고, apply와 introspect 교차 검사가 확장을 실행하고, 확장 test가 PHP plan, Mermaid, apply 정리 test를 따르며, Rust 확장과 그 cargo 단계가 없고, 하위 항목(T9.6부터)의 마지막 commit에서 CI가 통과한다.——[o]
T9.1Rust dbspec 위에 PHP 확장 orm_dbspec을 만들고 공유 vector와 PHP client로 확인한다(T9 후속).—근거: clients/php-extension은 자기 Cargo.lock을 가진 별도 Cargo workspace인 ext-php-rs 0.15.15 cdylib이며, orm_schema::dbspec 위의 readFile, readBytes, parse, emit, manifest, render를 가진 Orm\Dbspec\Native\Dbspec, 불투명한 Document, 그리고 결과 class Diagnostic, ReadResult, ParseResult, Manifest, ManifestResult, RenderResult를 선언 순서의 typed readonly property와 private 생성자를 가진 final readonly class로 등록한다. ext-php-rs의 property는 type이 없고 쓸 수 있으며 getter property의 순서가 proc macro의 HashMap을 따르므로 결과 class는 Zend API로 선언한다. PHP code는 자동 전환 없이 그 namespace로 확장을 고른다. UTF-8이 아닌 text는 PHP client와 같은 encoding diagnostic이 된다. make dbspec-php-extension-check는 clippy를 실행하고 library를 build해 복사한 뒤 clients/php-extension/tests/declarations_test.php(load한 확장의 Reflection이 40개 선언에서 stubs/orm_dbspec.stub.php와 같다. emit을 ?string으로 적은 stub은 실패했다)와 dbspec_test.php(10 case: parse case 118개, 두 번 읽은 file 4개, manifest 5개, rendering 51개가 vector와 PHP client와 같고, encoding, 인자, 객체 규칙. 줄 바꿈 없는 CR의 순서를 뒤집으면 bytes case가 실패했다)를 두 번씩 실행한다. CI는 setup step php-extension-tools에서 libclang을 설치하고(feature-check의 need libclang), make install-rust가 그 Cargo.lock의 crate를 받는다. repo-check, full-run-check, feature-unit-check, version-check, rust-fmt-check, interface-check가 통과한다.[o]
T9.2PHP 확장을 make dbspec-compare-check의 다섯 번째 runner로 실행한다(T9.1 후속).—근거: tests/dbspec/compare/php-interface.php는 vector reader와, Dbspec class 하나로 공유 case, stress 문서, files, hashes, statement vector를 출력하는 dbspec_write_interface를 가진다. php.php는 그것을 Orm\Dbspec\Dbspec으로 불러 나누기 전과 같은 3378줄을 출력했고, php-extension.php는 dbspec-compare-check/prepare가 build해 복사한 debug build를 php -d extension=으로 load해 Orm\Dbspec\Native\Dbspec으로 부른다. runners.mjs는 runner마다 읽는 input(reads)과 확장 runner의 until: 'plans/'를 선언하고, compare.mjs는 그런 출력을 첫 Go 출력의 첫 plan case 앞까지와 비교하므로 빠지거나 더해지거나 다른 case는 실패하고, 그 줄이 없는 reference는 오류다. until을 무시하면 check.test.mjs의 새 case 4개 가운데 3개가 실패했고 지금 10개가 통과한다. runners.test.mjs와 inputs.test.mjs는 mutation과 읽을 수 없는 input을 그것을 읽는 runner에만 준다. 확장 runner는 없는 cases file, stress 문서 자리의 directory, signature 없는 stress 문서, id 없는 ddl case를 그 경로와 함께 거부했다. 확장의 출력은 plans/ 앞까지 PHP 출력과 같다(1125줄). repo-check, full-run-check, feature-unit-check가 통과한다.[o]
T9.3PHP 확장을 interface와 symbol contract에 선언한다(T9.2 후속).—근거: contracts/interfaces.json은 extensions에 extension php-extension(PHP 문법, stub root clients/php-extension/stubs, snapshot contracts/symbols/php-extension.json과 그 hash, Dbspec rule 여섯 개와 owner DbspecDiagnostic, DbspecManifest)을 선언하고, 그 rule과 owner가 그 adapter를 가진다. contracts/validate.go는 extension adapter를 공통 입력과 결과의 php-extension 열과 대조하고, extension이 적지 않은 rule의 adapter, 적은 rule의 빠진 adapter, 없는 rule을 거부한다(TestExtensionContractRejectsDrift: 확인을 건너뛰면 mutation 5개 가운데 4개가 받아들여졌다). interface 검사는 interfaces/php-extension에서 stub을 PHP 추출기로 읽어 rule, owner, 금지 symbol, snapshot과 그 hash를 비교한다(symbol 39개, rule 6개, owner 2개). TestExtensionDeclarations는 emit이 ?string을 돌려주는 stub, externalText 없는 Manifest, extension의 Model.gets adapter, 바뀐 snapshot을 거부한다. --language php-extension은 그것만 확인한다. go test ./contracts ./tests/interfaces/check, make interface-check, repo-check, feature-unit-check, docs-rules-check가 통과한다.[o]
T9.4PHP 확장을 문서로 남기고 PHP client에서 제안한다(T9.3 후속).—근거: docs/dbspec.md와 한국어 짝 문서는 "PHP 확장" 절을 가진다: Orm\Dbspec\Native\Dbspec과 그 결과 class, 자동 전환 없이 namespace로 고르는 것, encoding과 인자 규칙, 범위인 Dbspec component, php-config의 PHP를 위한 Cargo build와 php -d extension= load, 선언인 stub과 두 검사. 검증 절은 make dbspec-compare-check가 비교하는 구현에 확장을 넣는다. clients/php/composer.json은 ext-orm_dbspec를 제안하고, composer validate는 lock file이 최신이라고 보고한다. docs-rules-check, docs-check, docs-verify-idempotent, repo-check, version-check, feature-unit-check가 통과한다.[o]
T9.5input test의 apply runner와 stress harness에 그 자신의 input을 준다(T9.2 후속). 원인: G5.83의 CI 실행은 tests/dbspec/inputs.test.mjs에서 apply runner에 runRunner takes the 5 inputs cases, stress, ddl, plans, mermaid; given 4, Rust stress harness에 given 1로 features/schema_definition/schema-compare를 실패했다. T9.2는 runRunner가 compare runner의 input을 reads로 고르고 다른 개수를 거부하게 했지만, 같은 test가 apply runner(인자 네 개)와 stress harness(한 개)를 runRunner로 실행한다. 그 실행에서 PHP 확장을 포함한 다섯 compare runner는 일치했다.—근거: runRunner는 reads를 선언한 runner에만 그것으로 input을 고르고 그런 runner의 다른 개수를 고치는 방법과 함께 거부하며, reads가 없는 runner에는 input을 그대로 준다. reads 없는 runner는 인자 네 개를, compare runner는 선언한 두 개를 받았고, input 하나를 받은 compare runner는 거부되었다. check.test.mjs의 case 10개가 통과한다.[o]
T9.6Dbspec의 plan, Mermaid, apply, introspect 메서드를 interface rule로 선언한다(T9 후속). 원인: contracts/interfaces.json에는 readFile, readBytes, parse, emit, manifest, render의 rule만 있어서 parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps, externalDifferences, apply, recover, rollback, finalize, introspect, exportMermaid, importMermaid는 PHP symbol snapshot에만 있었고, 어떤 검사도 네 언어의 signature를 비교하지 않았다.—근거: Dbspec component가 16개 메서드를 나열하고, contract가 각각에 Go, PHP, Rust, TypeScript adapter와 state를 가진 Dbspec.<method> rule과, 언어마다 field를 가진 owner DbspecPlan, DbspecPlanStep, DbspecEffect, DbspecNullCheck, DbspecChange, DbspecDifference, DbspecUnsupported, DbspecTableRename, DbspecColumnRename, DbspecColumnName, DbspecApplyEvent, DbspecApplyError를 가진다. contracts/validate.go가 새 공통 입력과 결과를 각 언어와 php-extension에 대응시키고, docs/spec/interfaces-model.md(+ko)를 다시 생성했다. go test ./contracts가 통과하고 make interface-check가 네 언어와 확장(rule 여섯 개)에서 통과한다.[o]
T9.7이름이 다른 문서를 거치는 use cycle에서 PHP parser가 끝없이 재귀하는 것과, audit 대상 table의 숫자만으로 된 column 이름에서 TypeError가 나는 것을 막는다(T9.6 후속). 원인: 변형한 공유 vector로 PHP client와 T9의 C 확장을 비교하는 실행에서 PHP client가 두 번 멈췄다. header가 집합의 이름과 다른 문서를 쓰고 그 문서가 자기 집합 이름을 쓰면 memory가 다할 때까지 재귀했고(Parser::usedDocument는 header 이름만으로 cycle을 찾았다), 10이라는 column을 가진 audit 대상 table에서는 checkAuditHistories가 int key 10을 auditLists의 문자열 closure에 넘겼다. Go, TypeScript, Rust parser는 두 문서를 diagnostic으로 보고했다.—근거: tests/dbspec/cases.json에 invalid case use-cycle-through-another-header-name(3:5의 use error 하나)과 audit-digit-column-names(7:3과 18:3의 name.format, 24:16의 setting error 둘)를 더했다. 고치기 전 clients/php/tests/dbspec_test.php는 첫 case에서 Allowed memory size ... exhausted로 실패했다. Parser::usedDocument는 읽는 중인 문서를 표시하고 그동안 다시 쓰이면 use cycle을 보고하며, checkAuditHistories는 column 이름을 문자열로 읽는다. dbspec_test.php가 invalid case 100개를 통과하고, go test ./engine/dbspec, node --test clients/typescript/tests/dbspec.mjs(test 141개), cargo test -p orm-schema --test dbspec가 새 case를 통과하며, php interface 검사, make repo-check, gofmt가 통과한다.[o]
T9.8Rust PHP 확장을 PHP client의 문서 model, parse, emit, readFile, readBytes, manifest, render를 C로 독립 구현한 것으로 바꾼다(T9.7 후속). 원인: T9.1의 ext-php-rs 확장은 Rust dbspec을 실행하고 PHP에 opaque Document를 주었으므로 PHP code에서 문서를 만들거나 고칠 수 없었고, class가 PHP client와 달랐으며, build에 Rust와 libclang이 필요했다.—근거: clients/php-extension은 C다(src/*.c, config.m4, build path가 src인 PIE용 composer.json). 호출마다 arena를 쓰는 C model, clients/php/src/Dbspec에서 옮긴 parser, check 식 reader, literal 규칙, emitter, 문서 집합 검사, manifest, renderer가 있고, convert.c가 public 가변 Orm\Dbspec\Native 문서 graph를 C model로, 다시 그 graph로 바꾼다. stubs/orm_dbspec.stub.php는 PHP client dbspec 표면의 모든 class(생성자, 메서드, 상수를 가진 문서, 결과, 값 class)와 메서드 여섯 개의 Dbspec을 선언하고, scripts/arginfo.php가 그것으로 gen_stub.php를 실행해 src/orm_dbspec_arginfo.h를 쓰며 배열 상수는 src/orm_dbspec.c에 남긴다. scripts/build.sh는 임시 directory에서 phpize로 build하고 컴파일러 경고를 거부하며 rename으로 게시한다. make install-php-extension-tools는 libclang 대신 gen_stub.php와 그 PHP-Parser를 .runtime/bin/gen-stub에 두고, Rust source, Cargo 파일, clippy와 rustfmt 단계, cargo fetch는 없어졌다. clients/php-extension/tests/dbspec_test.php는 이제 message를 포함한 diagnostic, emission, 파일, manifest, statement, 예외, PHP code로 만든 문서를 PHP client와 비교하고(case 11개 통과), declarations_test.php는 load한 확장의 선언 299개가 stub과 같음을 확인하고 PIE package를 확인한다. make dbspec-php-extension-check가 통과하고, T9.7 뒤 변형 문서 40000개와 생성한 check와 default 줄 15000개의 차등 fuzz가 PHP client와 차이를 찾지 못했으며, 반복 호출은 memory를 남기지 않는다. make interface-check(다시 기록한 php-extension snapshot), make repo-check, go test ./contracts ./tests/interfaces/check, owners, downloads, full-run, compare check node test, make docs-check, gofmt가 통과한다.[o]
T9.9PHP client의 plan 메서드를 C 확장에 구현한다(T9.8 후속). 원인: T9.8 뒤 Orm\Dbspec\Native\Dbspec에는 externalDifferences, parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps가 없었다.—근거: src/plan.c(plan text, chain, diff와 객체 비교, schema 비교, 집합 비교)와 src/steps.c(SQLite 다시 만들기를 포함한 세 dialect의 step)가 PlanText, PlanChain, PlanDiff, PlanObjects, SchemaComparison, PlanSteps와 Dbspec.php의 집합 메서드를 옮기고, convert.c가 plan, step, change, difference, unsupported 객체를 바꾸며 chain은 받은 Plan 객체를 돌려준다. stub이 메서드 아홉 개를 선언한다. tests/dbspec/compare/php-interface.php가 이제 Dbspec class를 인자로 받는 plan과 Mermaid 출력부를 가지고, php.php는 그것을 Orm\Dbspec\Dbspec으로 부르며, 확장 runner는 plan vector를 읽는다(runners.mjs의 reads가 cases, stress, ddl, plans이고 until이 mermaid/). 그 출력은 첫 Mermaid case 앞까지 PHP client의 출력과 같다(2918줄). clients/php-extension/tests/dbspec_plan_test.php는 clients/php/tests/dbspec_plan_test.php와 본문이 같고(case 51개 통과), declarations_test.php가 두 본문이 같게 남는지 확인하며, make dbspec-php-extension-check가 그것을 두 번 실행한다. contracts/interfaces.json은 rule 아홉 개와 owner DbspecPlan, DbspecPlanStep, DbspecEffect, DbspecNullCheck, DbspecChange, DbspecDifference, DbspecUnsupported, DbspecTableRename, DbspecColumnRename, DbspecColumnName에 php-extension adapter를 주고 snapshot을 다시 기록했다. 차등 fuzz는 변형한 plan 17000개(parse, emit, diff, 세 dialect의 step, chain, 비교, installed differences, 더하는 table과 column)와 rename, drop, 넓히기, 새 table을 가진 무작위 source, target schema 쌍 18000개(diff와 step, step이 있는 것 16444개)에서 PHP client와 차이를 찾지 못했고, 반복 호출은 memory를 남기지 않는다. make interface-check(확장의 rule 15개, owner 12개), make repo-check, make docs-check, owners와 compare check node test, gofmt가 통과한다.[o]
T9.10C 확장에 exportMermaid와 importMermaid를 구현하고 모든 input에서 확장을 비교한다(T9.9 후속). 원인: T9.9 뒤 확장에는 Mermaid 메서드가 없어서 compare runner가 until로 Mermaid vector 앞에서 멈췄다.—근거: src/mermaid.c가 erDiagram을 쓰고, PHP client 정규식의 줄 형식(entity 시작, attribute, relationship, label, column suffix, Go TrimSpace 공백, 아는 type, %q 인용)을 읽는 scanner로 erDiagram을 읽으며, src/catalog.c는 PHP client의 Catalog와 CatalogTable인 중립 중간 model로 문서 text를 쓰고 parse하고 거부된 객체를 뺀다(introspection도 쓴다). stub이 두 메서드를 선언한다. 확장 runner는 이제 다섯 input을 모두 읽고 그 출력은 PHP client runner의 출력과 같으며(3385줄), 일찍 멈추는 runner가 없으므로 tests/dbspec/compare/compare.mjs와 check.mjs의 until 비교와 check.test.mjs의 case 네 개를 지웠고, runners.test.mjs와 inputs.test.mjs는 더 이상 확장이 vector를 건너뛴다고 하지 않는다. clients/php-extension/tests/dbspec_mermaid_test.php는 PHP client Mermaid test와 본문이 같고(case 13개 통과), declarations_test.php가 두 본문을 확인하며, make dbspec-php-extension-check가 그것을 두 번 실행한다. interface contract에서 exportMermaid와 importMermaid가 php-extension adapter를 가진다(rule 17개). 변형한 erDiagram 16000개(4000개마다 import한 문서 876개)와 모든 공유 case의 export에서 차등 fuzz가 PHP client와 차이를 찾지 못했고, 반복 호출은 memory를 남기지 않으며, make interface-check, make repo-check, make docs-check, owners와 compare check node test(case 6개), gofmt가 통과한다.[o]
T9.11C 확장에 introspect를 구현하고 introspection 교차 검사에서 실행한다(T9.10 후속). 원인: T9.10 뒤 Orm\Dbspec\Native\Dbspec에는 introspect가 없어 확장이 database를 읽지 못했다.—근거: src/introspect.c는 PHP client의 MysqlCatalog, PostgresCatalog, SqliteCatalog의 catalog query를 받은 PDO의 query, fetchAll, errorCode, errorInfo를 Zend API로 불러 보내고(pdo_dbh_t 내부를 쓰지 않는다), PDOException을 그것을 이전 예외로 둔 RuntimeException "Catalog query failed: ..."로 감싸며, 모든 error mode에서 실패한 statement를 보고하고, 모양이 다른 catalog 값을 CatalogRows의 UnexpectedValueException message로 거부하고, check 식을 CheckDecoder처럼 읽고, immutable과 audit trigger를 CatalogTriggers처럼 알아보며, 문서를 src/catalog.c로 만든다. stub은 introspect(\PDO $connection, string $dialect, string $name): IntrospectResult를 선언하고 module은 pdo를 요구한다. clients/php-extension/tests/dbspec_test.php에 php-extension/introspect를 더한다: 메모리 SQLite database에 적용한 ddl.json case 5개와 tests/dbspec/introspect.json의 SQLite case 4개가 PHP client의 문서와 미지원 객체, 그리고 vector의 것을 내고, 세 error mode의 실패하는 query, 다른 type의 값, PDO 하위 class가 던진 예외, 알 수 없는 dialect, PDO가 아닌 connection에서 PHP client와 같은 예외와 이전 예외가 나온다. tests/dbspec/introspect/php-extension.php는 TestIntrospectCompare의 다섯 번째 runner이고(ORM_DBSPEC_EXTENSION, make dbspec-introspect-compare-check가 build한다), clients/php-extension/tests/dbspec_introspect_test.php는 PHP client introspection test의 본문을 따르며 새 make dbspec-introspect-php-extension-check가 MySQL, PostgreSQL, SQLite에서 실행한다. declarations_test.php는 따르는 본문 세 개를 확인하고, Dbspec.introspect는 php-extension adapter를 가진다(rule 18개). SQLite database(공유 문서와 변형한 statement)와 변형한 row, 값 type, query 실패를 가진 MySQL, PostgreSQL, SQLite 가짜 catalog로 introspection 24000번을 비교한 differential fuzz는 문서, 미지원 객체, 예외, 보낸 query에서 PHP client와 다른 곳이 없었고, 2000번 반복한 호출은 memory를 남기지 않는다. server suite는 CI에서 실행되고, local에서 make interface-check, make repo-check, make docs-check, 확장의 declarations와 dbspec test, owners와 check node test, gofmt가 통과한다.[o]
T9.12모든 client에서 기록된 step이 plan 밖인 applied row의 rollback을 멈춘다(T9.11 후속). 원인: SQLite에서 command 사이에 history row를 바꾸며 apply, recover, rollback, finalize를 PHP client와 C 확장으로 비교한 differential run에서, applied나 done row의 rollback이 recover와 중단된 rollback이 하는 범위 확인 없이 step을 읽는 것을 찾았다. step이 4개인 plan의 row를 step 99로 두면 PHP client는 없는 step을 읽었고(warning 뒤 LogicException "step 98 has no effect to read"), Go와 Rust는 index 범위 밖으로 panic했으며, TypeScript는 TypeError: Cannot read properties of undefined (reading 'rollback')를 던졌다.—근거: engine/dbspec/apply.go, clients/php/src/Dbspec/PlanApply.php, clients/typescript/src/dbspec/apply.ts, clients/rust/orm/src/dbspec/apply.rs는 이제 그런 row를 drift 확인 전에 chain error "the recorded step 99 is outside the plan's 4 steps"로 보고하고, docs/spec/plans.md(+ko)가 규칙을 적는다. 각 client의 SQLite 정리 test에 case apply/rollback-step-outside(create-from-empty를 적용하고 row의 step을 99로 바꾼 뒤 rollback)를 더했다. 수정 전 Go는 index out of range [98] with length 4, PHP는 그 LogicException, TypeScript는 그 TypeError, Rust는 index out of bounds: the len is 4 but the index is 98로 실패했고, 수정 뒤 go test ./engine/dbspec(apply test 세 개), clients/php/tests/dbspec_apply_cleanup_test.php(case 5개), make dbspec-ts-check(test 277개), make dbspec-rust-check/apply-cleanup, gofmt, make docs-check가 통과한다.[o]
T9.13C 확장에 apply, recover, rollback, finalize를 구현하고 public class를 PHP client와 비교한다(T9.12 후속). 원인: T9.12 뒤 확장에는 PHP client Dbspec의 마지막 메서드인 apply 명령 네 개가 없었다.—근거: src/apply.c는 PlanApply를 따른다. MySQL, PostgreSQL, SQLite의 session lock, 설정과 그 복원, history table, position, settled 상태, 확장 자신의 introspection과 manifest로 하는 verify, resolve, forward, finalize, null check가 있는 rollback, $events closure로 보내는 event, $now closure를 DateTimeImmutable로 쓴 applied_at, 그리고 모든 정리 단계를 실행하고 첫 error 하나 또는 정리 error를 순서대로 담은 ApplyCleanupError를 던지는 finish(안쪽 ApplyCleanupError는 편다)가 있다. 받은 PDO의 getAttribute, prepare, exec와 statement 메서드를 불러 쓰므로 PDO 하위 class는 PHP client의 호출을 보고, ERRMODE_EXCEPTION이 아닌 connection은 PlanApply.php의 InvalidArgumentException이다. stub은 네 메서드와 @internal PlanApply(src/orm_dbspec.c가 등록하는 중첩 상수 EFFECT_QUERIES와 effectOn)를 선언하고, contract는 Dbspec.apply, recover, rollback, finalize에 php-extension adapter를 주므로 확장은 Dbspec rule 22개를 모두 가진다. Test: clients/php-extension/tests/dbspec_apply_cleanup_test.php와 dbspec_apply_test.php는 PHP client test의 본문을 따르고(declarations_test.php가 첫 const부터 본문 다섯 개를 확인한다), make dbspec-php-extension-check가 정리 test를 두 번 실행하며, 새 make dbspec-apply-php-extension-check가 세 database에서 apply scenario를 실행하고, tests/dbspec/apply/php-extension.php는 TestApplyChainAcrossClients의 다섯 번째 client다(순서쌍 180개). dbspec_test.php에 php-extension/apply를 더한다: SQLite file의 script 108개(chain을 plan마다 적용, 둘째 plan의 event마다 멈춘 뒤 recover나 rollback, drift, 바꾼 history row, finalize)가 PHP client의 event, code가 있는 예외, 정리 error와 이전 예외, history row, schema를 내고, error mode, 알 수 없는 dialect, 갈라진 chain, text를 돌려주는 clock도 같다. declarations_test.php에 php-extension/client를 더한다: PHP client의 public dbspec class 43개의 public 선언 297개가 property 순서까지 확장의 것과 같고, 확장의 나머지 class는 stub에서 @internal이다. 이를 위해 PHP client의 helper class CheckDecodeFailure, Expression, ExpressionFailure, Literal, ParseStop, SchemaComparison을 @internal로 적는다. SQLite file에서 무작위 명령 script 3600개(무작위 event에서 멈춤, 남는 table, 바꾼 history row, row 삽입)를 비교한 differential fuzz는 PHP client와 다른 곳이 없었다. server suite는 CI에서 실행되고, local에서 make dbspec-php-extension-check, make interface-check, make repo-check, make docs-check, gofmt가 통과한다.[o]
T10package의 Cargo manifest가 하나인 ordered-json revision에 의존한다. 원인: clients/rust의 모든 cargo 명령이 "skipping duplicate package ordered-json"을 경고한다. clients/rust/orm/Cargo.toml의 ordered-json revision과 ordered-json main 모두 Cargo.toml(library rust/src/lib.rs)과 rust/Cargo.toml에서 같은 package를 정의하고, cargo는 앞의 것을 쓰고 뒤의 것을 건너뛴다. 완료 조건: ordered-json이 package의 manifest를 하나만 두고, clients/rust/orm/Cargo.toml이 그 revision을 고정하며, clients/rust의 cargo test가 duplicate package 경고를 출력하지 않는다.—Evidence: ordered-json이 root Cargo.toml을 지워 rust/Cargo.toml이 package의 유일한 manifest가 되고, 그 저장소 test test_each_cargo_package_has_one_manifest는 manifest가 둘이면 실패한다. Go, PHP, TypeScript, Rust client가 모두 그 ordered-json을 쓴다. cargo test -p orm --lib는 "skipping duplicate package" 줄을 출력하지 않고, rust-check, client-unit-check, ts-check, conformance-check, 네 dbspec check가 통과한다.[o]
T11추적되는 Go source가 gofmt 형식이 아니면 check를 실패시키고, Go와 Rust 형식 check를 통과시킨다. 원인: make check는 rust-fmt-check를 실행하지만 Go 형식 check는 없고, rust-fmt-check는 Rust workspace 전체의 rustfmt 차이 82개로 실패하며(기본 toolchain과 1.98.1 toolchain의 rustfmt 1.9.0이 같은 결과), 추적되는 Go 파일 6개(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)가 gofmt 형식이 아니다. 그중에는 struct field 정렬이 어긋나고 if 본문이 한 줄인 "feat: preserve exact decimal model values"의 code가 있다.—근거: make go-fmt-check는 gofmt가 바꿀 추적 Go 파일을 나열하고 실패한다. 형식 정리 전에는 파일 6개로 실패했고 gofmt -w 뒤에는 통과하며, make check가 rust-fmt-check 옆에서 실행한다. cargo fmt --all로 Rust workspace를 정리했고 make rust-fmt-check가 통과한다. 변경 뒤 go build ./..., engine, clients/go, bench의 go vet, cargo check --workspace --all-targets --all-features가 통과한다. rustfmt가 orm-build catalog/mod.rs의 여러 줄 parameter 목록 두 개(read_only_query, read_only_grid_query)에 끝 쉼표를 더해 기록된 token signature가 달라졌다. contracts/symbols/rust.json에 다시 기록했고 네 언어 interface-check가 통과한다.[o]
T12commit된 TypeScript model이 generator 출력과 다르면 check를 실패시키고, model을 호출하는 source만 scan한다. 원인: clients/typescript의 npm run build는 npm run models를 실행하고, 그 --scan tests는 모든 test 파일을 읽으므로 tests/physical-document-render.mjs의 Playwright 호출 browser.newPage()가 Author과 User model에 newPage(value: unknown)를 더한다. commit된 models.ts에는 그것이 없고, 둘을 비교하는 target도 없다.—Evidence: clients/typescript/tests/models-scan.mjs는 models script가 44개 source를 scan했지만 generated model을 import하는 source는 8개뿐이고 나머지 36개에 tests/physical-document-render.mjs가 있어서 실패했다. 이제 script는 그 8개만 지정하고 test는 통과한다. 그 scan으로 같은 test는 differs: src/models/models.ts로 실패했고, 다시 생성해 12개의 newPage(value: unknown) 선언을 지운 뒤 통과한다. make check에 속한 make ts-model-check는 client를 build하고 두 test를 실행한다: scan은 model module이나 class를 import하는 source와 같고, script 인자에 --check를 더한 orm-gen gen은 출력 없이 0으로 끝난다. contracts/symbols/typescript.json에서 12개의 newPage symbol이 빠지고 go run ./tests/interfaces/check가 통과한다. TEST_ENV로 make ts-check가 통과한다.[o]
T13commit 제목을 AGENTS.md의 commit rule로 검사한다. Cause: make git-check는 type build, perf, merge와 72자를 허용하는 type: description을 요구했고, AGENTS.md는 type feat, fix, docs, style, refactor, test, chore와 50자 제목의 type(scope): Subject (#id)를 요구한다. 그래서 rule을 따른 모든 commit과 type이 없는 이전 commit에서 check가 실패했다.—Evidence: scripts/git/check.test.mjs는 subjectErrors가 없어 실패했고, type(scope): Subject (#id)를 받아들이고 type, scope, id가 없는 제목, type perf와 type 없는 형식, 소문자 제목, 끝 마침표, 50자를 넘는 제목을 거부하는 3개 test로 통과한다. contracts/rules.json은 AGENTS.md의 type, 50자 한도, baseline main을 가진다. merge commit은 건너뛴다. make git-check가 통과한다.[o]
T14환경에 묶인 Go test를 소유 target에서만 실행한다. Cause: make client-db-check는 go test ./clients/go/...를 실행해 TestDecimalPhysical*와 TestCoverage*도 실행했다. 이 test는 decimal-physical-check와 feature-check가 주는 DSN이 필요하므로 client-db-check가 "DECIMAL_MYSQL_DSN is required"와 "selected database and DSN are required"로 실패했다.—Evidence: TestDecimalPhysical*는 build tag decimalphysical, TestCoverage*는 tag featurecoverage를 가진다. scripts/decimal-physical-check.mjs와 scripts/features/coverage.mjs가 tag를 넘긴다. TEST_ENV로 go test -count=1 ./clients/go/...가 모든 package에서 통과한다. make decimal-db-setup 후 make decimal-physical-check는 연속 세 번 모두 24 run(4 client, 3 database, 2 run)을 통과한다. node --test scripts/features/coverage.test.mjs는 10 test를 통과한다. make feature-check는 T15 때문에 test 실행 전에 멈춘다.[o]
T14.1Rust DSN coverage test를 feature-check에서만 실행한다 (T14 후속). Cause: client-db-check와 client-pooler-check의 cargo test --workspace가 ORM_FEATURE_DATABASE와 ORM_FEATURE_DSN이 필요한 coverage_dsn_connection을 실행해 "ORM_FEATURE_DATABASE is required"로 실패했다.—Evidence: coverage_dsn_connection은 소유자를 이유에 적은 #[ignore]다. cargo test -p orm --test coverage_dsn은 1 ignored로 통과한다. scripts/features/coverage.mjs가 이제 쓰는 형식인 ORM_FEATURE_DATABASE=sqlite, bench SQLite DSN, -- --exact --include-ignored로는 test 1개가 통과한다. node --test scripts/features/coverage.test.mjs는 10 test를 통과한다.[o]
T14.2Rust decimal과 generated-model coverage test를 소유 target에서만 실행한다 (T14.1 후속). Cause: coverage_dsn_connection이 먼저 실패하지 않게 되자 cargo test --workspace가 decimal-physical-check와 feature-check의 변수가 필요한 decimal_mysql, decimal_postgres, decimal_sqlite, coverage_generated_model_connection을 실행해 "DECIMAL_SQLITE_DSN is required"로 실패했다.—Evidence: decimal test 3개와 coverage_generated_model_connection은 소유자를 이유에 적은 #[ignore]다. scripts/decimal-physical-check.mjs가 --include-ignored를 넘긴다. TEST_ENV로 cargo test --workspace가 1개와 3개 ignored로 exit 0이다. make decimal-physical-check는 Rust run을 포함해 통과한다.[o]
T15contracts/features.json의 모든 feature에 coverage kind와 owner test를 선언한다. Cause: make feature-check가 17개 feature 중 16개에서 "missing coverage kind"로 실패한다. DSN connection feature만 coverage를 선언했고 나머지를 추적하는 항목이 없었다.—근거: contracts/features.json의 모든 feature가 coverage.kind(database 11개, independent 6개), 공유 case ID, clients/<language> 아래 지원 client별 owner test를 선언한다. scripts/features/coverage.mjs는 152개 owner와 사용 부분 요구를 각각 두 번 실행하고 결과와 database digest가 같은지 확인한다. database case는 시드된 bench database의 MySQL, PostgreSQL, SQLite에서 실행되며 composite_account, composite_membership과 스스로 설치하고 지우는 audit table에만 쓴다. TEST_ENV로 make feature-check가 통과한다: "feature coverage: 17 contracts executed", 검증 명령 71개 모두, "features: 17 feature contracts passed". 공유 fixture contracts/fixtures/planner.json, schema_definition.json, authenticated_encryption.json은 independent case에 네 client 공통 기대값을 준다. manifest 오류는 원인에서 고쳤다: decimal_model.json은 fixture 형식(feature, operation, expected)을 따르고 model_writes에 속하며, 네 reader는 옛 형식에서 실패하고 새 형식에서 통과한다. 제거된 test의 path를 지우고 매핑되지 않은 test file마다 feature를 정했다. dsn_connection과 schema_install 설명은 현재 동작을 말하며 docs/features(.ko).md는 contracts/features.json에서 생성한다. 새 feature catalog_connection(Rust partial)이 orm-build catalog test를 소유한다. 한 검증 명령은 .bin link 대신 node …/typescript/bin/tsc를 실행한다. owner test는 clients/<language> 아래에 있어야 하므로 bench/go를 clients/go/bench로 옮겼다. 네 conformance 실행기는 반복할 수 있는 vector 선택을 받는다. 새 case가 찾아 Red 다음 Green으로 고친 결함: checker가 두 번째 Go 실행을 test cache에서 읽었다(새 checker test가 두 번 중 1번을 셌고, 이제 go test -count=1이 두 번 실행한다). SQLite가 1811로 보고하는 RESTRICT foreign key 위반이 DRIVER였고 네 client와 docs/errors.yaml에서 FOREIGN_KEY가 되며, trigger RAISE의 1811은 DRIVER로 남는다. TypeScript, PHP, Rust에서 CHECK 위반이 CONSTRAINT가 아니었다. PHP가 bind 한도를 넘는 IN list의 나눌 수 없는 plan을 plan cache에 두었다(count 뒤 유지된 memory가 87 MB에서 5 MB로 줄었다). Rust catalog가 MySQL _bin VARCHAR를 binary로 읽어 insert 검사가 ROW_WRITE_MISMATCH로 실패했다. T8.8.1 merge 뒤 PHP schema install case가 hash 없이 manifest text를 읽었다. checklist의 원문 인용 <value>와 <nil>은 code span이 되었다. VitePress가 이를 만나 멈추고 docs-check가 실패했기 때문이다. 발견해 기록한 항목: T18~T22. T8.6.8, T8.8.2와 merge한 tree에서 make feature-check가 한 번에 통과한다: "feature coverage: 17 contracts executed", verification 명령 72개, "features: 17 feature contracts passed"가 1453 s에 끝나고, 그동안 clients/rust/target은 11.8 GB까지 커진다. merge한 tree에서 드러난 공유 database 결함 두 개를 고쳤다. Rust rollback_fault case(clients/rust/orm/tests/rollback.rs)는 끝에 rollback_probe를 지운다. test database에 남은 table이 Rust integration binary의 schema().empty() case를 실패시켰기 때문이다("mysql: schema().empty() on a database without tables"). clients/rust/tests/src/decimal_physical.rs의 schema set database 이름은 process id와 counter를 포함한다. 병렬 MySQL case 두 개가 같은 timestamp 이름을 만들어 하나가 MySQL error 1007 "database exists"로 실패했기 때문이다. 그 뒤 decimal_physical의 case 9개가 세 번 연속 통과했다.[o]
T16send-savepoint SQLite file을 TEST_ENV 옆에서 연다. Cause: Makefile이 이 file을 checkout 아래 .runtime/servers/send-savepoint.sqlite로 지정했는데 worktree에는 그 directory가 없어서, client-db-check와 client-pooler-check에서 sqlite_send_savepoint가 "unable to open database file"로 실패했다.—Evidence: 이전 경로로는 이 worktree에서 cargo test -p orm --lib sqlite_send_savepoint가 "unable to open database file"로 실패했다. 이제 SEND_SQLITE_DSN은 $(dir $(abspath $(TEST_ENV)))send-savepoint.sqlite이며 make -n client-db-check는 이를 TEST_ENV의 servers directory로 펼친다. 그곳에서 send_savepoint test 3개가 통과한다.[o]
T17DSN이 없으면 Rust client benchmark를 실패시킨다. Cause: clients/rust/tests/src/client_bench.rs는 ORM_BENCH_MYSQL_DSN이 없으면 내장 local DSN으로 바꾼다. 어떤 spec도 정하지 않은 fallback이다.—Evidence: clients/rust/tests/tests/client_bench_dsn.rs는 빌드된 client_bench를 ORM_BENCH_MYSQL_DSN 없이, 그리고 빈 값으로 실행한다. 변경 전에는 두 test가 모두 실패했다. 변수가 없으면 bench가 내장 socket에 연결해 0으로 끝났고, 빈 값이면 변수 이름 없이 DSN scheme에서 panic했다. 이제 client_bench는 두 경우 모두 "ORM_BENCH_MYSQL_DSN is required"를 출력하고 1로 끝나며, 2개 test는 cargo test -p orm-tests --test client_bench_dsn과 TEST_ENV를 쓴 cargo test --workspace에서 통과한다. TEST_ENV로 bench는 시드된 bench database에서 100회를 실행하고 0으로 끝났다. client_bench를 실행하는 make target은 없다. make perf-check는 Go와 PHP gate만 실행하고, DSN test는 client-db-check의 cargo test --workspace에서 실행되며 이 target은 T17.1에서 멈춘다. make rust-check와 make rust-fmt-check는 통과한다.[o]
T17.1client-db-check에서 orm-schema 시간 한도를 통과시킨다. Cause: load average 약 70에서 client-db-check의 cargo test --workspace가 orm-schema target 두 개에서 실패했다. debug build의 physical_json::tests::preflight_bounds는 node-limit에서 15.15 s, physical_graph_vectors는 45.9 s와 51.9 s를 측정했고, 한도는 각각 15 s이다.—Evidence: 제한이 잘못된 양을 쟀다. docs/schema.md는 Rust debug build와 graph test 15초 제한을 정하고 test는 wall-clock 시간을 이것과 비교했다. 이 공유 machine(11 core, 이 작업 동안 load average 16-92)에서 wall-clock 시간은 다른 process가 processor를 쓰는 시간도 담는다. load average 30 근처에서 단독으로 실행한 physical_graph_vectors는 wall-clock 17.31초와 26.48초로 두 번 실패했지만 process는 CPU(user와 system) 10.60초와 11.86초를 썼고, preflight_bounds는 wall-clock 19.23초에 CPU 6.11초를 썼다. Go TestPhysicalGraphVectors는 go test ./clients/go/... 안에서 wall-clock 64.46초와 81.71초로 실패했고 단독으로는 wall-clock 2.92초, CPU 4.13초였다. 제한은 15초 그대로이고 이제 CPU 시간을 제한한다. Rust CaseClock(clients/rust/orm-schema/tests/common/case_clock.rs, libc dev-dependency로 clock_gettime(CLOCK_THREAD_CPUTIME_ID))은 physical graph, document, JSON limit test 8개와 1초 record case 제한에서 case를 실행한 thread를 재고, Go caseClock(clients/go/orm/case_clock_test.go, getrusage)과 PHP processCpuNs(clients/php/tests/process_cpu.php)는 test를 하나씩 실행하는 test process를 재며, 모든 case가 두 시간을 출력한다. 변경 후 physical_graph_vectors는 cpu=10.25초 wall=13.43초, node-limit은 cpu=2.05초, Go graph test는 cpu=5.19초를 보고했고, PHP test 3개가 통과하며, TEST_ENV로 load average 16-31에서 ORM_CLIENT_DB_LANGS=rust make client-db-check(통과한 test result 78개)와 ORM_CLIENT_DB_LANGS=go make client-db-check가 통과한다. docs/schema.md가 고친 기준을 적는다. 나머지 wall-clock 제한은 T17.6이다.[o]
T17.2ORM_BENCH_MYSQL_DSN이 없으면 example program을 실패시킨다. Cause: examples/complex와 examples/thin-slice의 Go, PHP, Rust program은 없는 ORM_BENCH_MYSQL_DSN을 내장 local socket DSN으로 바꾸고, 두 README는 그 DSN을 기본값으로 적는다.—Evidence: examples/complex와 examples/thin-slice의 Go, PHP, Rust program은 변수가 없거나 비어 있으면 연결하지 않고 "ORM_BENCH_MYSQL_DSN is required; it names the seeded bench database"를 출력하며 status 1로 끝나고, 두 README가 이를 적는다. Go TestExamplesRequireBenchDSN(examples/dsn_test.go)은 Go program을 build해 PHP program과 함께 변수를 뺀 환경과 빈 값으로 실행한다. Rust examples_fail_when_dsn_is_unset과 examples_fail_when_dsn_is_empty(clients/rust/tests/tests/example_dsn.rs)는 build한 complex와 demo를 실행한다. 변경 전에는 Go subtest 8개가 모두 실패했다. Go program은 내장 socket에 연결해 0으로 끝났고, PHP program은 T8.2.6.3.4가 PHP Config에서 없앤 schemaPath를 아직 넘겨 "Unknown named parameter $schemaPath"와 함께 255로 끝났다. 그것과 schema 인자를 없앤 뒤에는 PHP program도 내장 socket에 연결해 0으로 끝났다. Rust test 2개도 실패했다("left: Some(0), right: Some(1)"). 변경 후 subtest 8개와 Rust test 2개가 통과하고, TEST_ENV로 여섯 program이 0으로 끝난다. thin-slice 출력은 byte까지 같고 complex 출력은 같은 JSON 값이다. README가 diff로 비교하는데도 complex 출력은 key 순서와 indent가 다르며, 이것이 T17.4다.[o]
T17.3Rust native benchmark의 database를 선언된 DSN에서 읽는다. Cause: bench/rust/src/native.rs와 bench/rust/src/driver_compare.rs는 선언된 DSN 대신 고정 socket /tmp/mysql.sock과 database orm_bench에 연결한다.—Evidence: bench/rust/src/native.rs와 bench/rust/src/driver_compare.rs는 Go bench와 client_bench가 쓰는 DSN인 ORM_BENCH_MYSQL_DSN에서 database를 읽는다. sqlx는 MySqlConnectOptions::from_str로, mysql_async는 Opts::from_url로 해석하고, 해석할 수 없는 DSN은 변수 이름과 함께 status 1로 끝나며, 없거나 빈 DSN은 연결하지 않고 "ORM_BENCH_MYSQL_DSN is required; it names the seeded bench database"와 함께 status 1로 끝난다. benchmarks_fail_when_dsn_is_unset과 benchmarks_fail_when_dsn_is_empty(bench/rust/tests/dsn.rs)는 build한 native와 driver_compare를 실행한다. 변경 전에는 native가 /tmp/mysql.sock에 연결해 status 101로 멈춰 둘 다 실패했고("left: Some(101), right: Some(1)"), 변경 후 둘 다 통과한다. make rust-driver-check가 이제 이 test를 실행한다. TEST_ENV로 두 program은 선언된 database에 연결했고 첫 row에서 "Rust type u64 (as SQL type BIGINT UNSIGNED) is not compatible with SQL type BIGINT"와 함께 status 101로 멈췄으며, 이것이 T17.5다.[o]
T17.4complex 예제 출력을 byte 단위로 같게 만든다. Cause: examples/complex/README.md는 Go, PHP, Rust 출력을 diff로 비교하지만, 출력은 key 순서와 들여쓰기가 다른 같은 JSON 값이어서 문서의 비교가 실패한다.—Evidence: make example-check는 Rust complex와 demo 프로그램을 build하고 TestExampleOutputsAreIdentical(examples/outputs_test.go, build tag examples)를 실행한다. 이 test는 examples/complex와 examples/thin-slice의 Go, PHP, Rust 프로그램을 시드된 bench database에서 실행하고 stdout이 byte 단위로 같기를 요구한다. 변경 전 complex subtest가 실패했다: Go는 최상위 member를 정렬하고 두 칸 들여쓰기로 2241 byte, PHP는 선언 순서와 네 칸 들여쓰기로 3121 byte, Rust는 json!이 각 row를 정렬된 map으로 바꾸고 serde_json이 정수 값 f64에 소수부를 붙여 모든 object의 member를 정렬하고 "read_sum": 456000.0으로 2243 byte를 썼다. thin-slice는 각 200 byte로 통과했다. 이제 세 complex 프로그램은 member가 하나의 선언 순서(Go struct, PHP array, client의 row JSON을 유지하는 Rust Serialize struct)를 따르는 compact JSON을 쓰고, read_sum과 like_avg를 Go와 PHP처럼 소수부를 강제하지 않는 가장 짧은 십진 표기로 쓴다. Rust는 serialization error를 unwrap하지 않고 main에서 반환한다. 변경 후 두 subtest가 각 언어 1183 byte와 200 byte로 통과하고, README가 형식과 검사를 설명한다. make example-check는 make check에 포함된다.[o]
T17.5Rust native benchmark에서 bench schema 형을 decode한다. Cause: bench/rust/src/native.rs와 bench/rust/src/driver_compare.rs는 seq와 다른 key를 u64로, boolean을 u8로 decode하지만 schema/bench.dbs는 signed i64 key를 선언하므로, 두 프로그램 모두 시드된 bench database의 첫 row에서 멈춘다. make rust-driver-check는 이들을 compile만 한다.—Evidence: native_reads_the_seeded_database와 driver_compare_reads_the_seeded_database(bench/rust/tests/seeded.rs)는 build된 프로그램을 ORM_BENCH_MYSQL_DSN에서 30회와 10회 반복으로 실행하고 status 0과 모든 workload의 통계 줄을 요구한다. 변경 전 둘 다 column 0에서 status 101로 실패했다: "Rust type u64 (as SQL type BIGINT UNSIGNED) is not compatible with SQL type BIGINT". 이제 두 프로그램은 선언된 형을 decode한다: i64 key, i32 count, bool, sqlx chrono feature를 통한 NaiveDateTime의 datetime(6), String의 varchar. native는 모든 column decode error를 빈 값으로 바꾸지 않고 전파하며, relation 단계는 SQL에 없는 두 값을 더 bind하지 않고 placeholder가 있는 key만 bind한다. 두 프로그램은 3(native) 또는 10(driver_compare) 이상의 정수가 아닌 반복 인자를 기본값으로 바꾸지 않고 거부한다. 변경 후 make rust-driver-check가 TEST_ENV로 DSN test 두 개와 seeded test 두 개를 하나씩 실행하고 4개 모두 통과한다. docs/perf.md가 검사 내용을 설명한다.[o]
T17.6남은 test 기한을 CPU 시간으로 제한한다. Cause: 남은 test가 case가 끝난 뒤 확인하는 기한(예: orm-schema의 1-10 s dbspec과 1-3 s physical 한도, 그리고 Go와 PHP의 대응 한도)은 여전히 wall-clock 시간을 비교하는데, 공유 machine에서 wall-clock 시간은 다른 process가 processor를 쓰는 시간도 담고, TypeScript graph test는 15초 node:test timeout으로 wall-clock 시간을 제한한다.—Evidence: make timing-check는 tests/timing/preempted.test.mjs를 실행한다. 이 test는 아래 각 test를 자기 process group에서 실행하며 10 ms 실행마다 그 group을 SIGSTOP으로 90 ms 멈춰, wall-clock 시간의 10분의 1만 받게 한다. 변경 전 네 stress budget이 여기서 실패했다: Go median parse 523 ms가 100 ms를, Rust 518 ms가 300 ms를, PHP 3110 ms가 400 ms를, TypeScript 1232.5 ms가 250 ms를 넘었다. Rust orm-schema vector 기한과 다른 PHP dbspec test 여섯 개는 통과했다. 이제 test가 자기 계산에 두는 모든 한도는 CPU 시간을 제한하고 모든 case가 CPU와 wall-clock 시간을 출력한다: 새 workspace crate clients/rust/case-clock(CaseClock, clock_gettime(CLOCK_THREAD_CPUTIME_ID))이 orm-schema vector test 일곱 개, dbspec_stress example, orm-build의 accessor와 page test 두 개에서 case thread를 잰다. Go caseClock(engine/dbspec/case_clock_test.go)은 stress test가 고정한 OS thread를 잰다. process CPU 시간은 garbage collector worker의 시간도 담기 때문이다(process CPU median 135 ms와 120 ms, thread CPU 71-80 ms). clients/php/tests/case_clock.php(caseClockStart, caseClockElapsed)는 dbspec test 일곱 개에서 한 thread로 도는 PHP process를 잰다. TypeScript stress test는 process.threadCpuUsage를 잰다(process CPU median 228 ms, thread CPU 135 ms). 변경 후 preempted test 11개가 모두 CPU 시간 median 76.7 ms(Go), 49.1 ms(Rust), 391.0 ms(PHP), 153.1 ms(TypeScript)로 통과하고, make dbspec-go-check, make dbspec-php-check, make dbspec-ts-check, make dbspec-rust-check가 통과한다. 다른 기한은 wall-clock 시간을 유지한다: 멈춘 case를 끊는 timer(Go runTimed, runWithDeadline, exec.CommandContext, Rust tokio::time::timeout과 run_probe, 15000 ms dbspec vector timeout을 포함한 node:test timeout)는 CPU를 쓰지 않는 멈춤을 재고, database나 다른 process가 하는 일의 한도(Rust 300-600 s apply와 introspection suite, PHP apply, plan-apply, introspection, runtime test, introspection 비교 budget)는 test process 밖에서 쓴 시간을 재며, cancel과 lock 대기 assertion은 시간 동작 자체를 확인한다. T8.2.6.4가 clients/go/orm에 남긴 쓰이지 않는 Go caseClock을 제거했고, AGENTS.md가 규칙을, docs/dbspec.md가 CPU budget과 검사를 설명한다.[o]
T17.7native benchmark의 쓰기와 bind를 bench schema에 맞춘다. Cause: clients/go/bench/native_test.go의 relation4와 listN은 SQL에 placeholder가 없는 bench-salt 값 두 개를 bind하므로 go test ./clients/go/bench -bench Relation4가 "sql: expected 1 arguments, got 3"으로 실패하고, bench/rust/src/native.rs의 native insert는 Go 기준이 HostEncode로 encode하는 AES column aes_hex_email에 평문 ins@example.com을 쓴다.—근거: Go TestNativeStatementsEqualClient(clients/go/bench/statements_test.go)는 네이티브 PK와 100행 statement를 생성 클라이언트가 실행하는 statement와 비교하고, TestNativeWorkloadsRead는 모든 네이티브 읽기 workload를 벤치 데이터베이스에서 실행한다. 변경 전 두 test는 실패했다. 네이티브 statement는 raw hex AES column과 ip를 선택했고 클라이언트는 UNHEX column, price, 숨은 aes_key_version을 선택했으며, listN은 "sql: expected 1 arguments, got 3"으로 실패했다. 이제 네이티브 statement가 클라이언트와 같고, 네이티브 scan은 행의 key version과 keyring 하나로 AES column을 decode하며, relation4와 listN은 key만 bind한다. 두 test가 통과하고 make perf-check가 hot-path 검사 전에 실행한다. list100 검사가 잰 것은 이 차이였다. 같은 부하에서 2026-09-27 commit "docs: record client overhead measurements"은 1.13~1.18, HEAD는 1.22~1.32였고 클라이언트 할당은 같았다(호출당 4440과 4438). 고친 기준 코드로 다섯 번 실행한 값은 한도 1.25에 대해 1.10~1.15, PK는 1.35에 대해 1.08~1.13이다. Rust insert_email_is_an_aes_hex_envelope(bench/rust/src/native.rs)는 값이 hex가 아니어서 실패했고, 이제 native가 Go 기준 코드의 HostEncode처럼 bench-salt의 AES ciphertext를 hex로 써서 통과한다. docs/perf.md가 같은 SQL 검사를 설명한다.[o]
T18composite foreign key의 모든 component로 relation을 읽는다. Cause: relation request는 key pair 하나만 담아서 어느 client도 composite_account에서 composite_membership을 tenant_id와 account_id로 함께 읽지 못한다. T15의 composite_key_rows case는 한 component로 join하고 다른 component로 거르지만, contracts/features.json의 composite_keys는 모든 key component를 유지하는 relation을 주장한다.—Evidence: relation request는 left와 right 대신 key 성분마다 {left, right} 한 쌍을 key 순서대로 담는 keys를 가진다(docs/spec/protocol.md 1.2와 한국어 짝, contracts/interfaces.json의 IRRelation과 새 IRKeyPair). Go, PHP, Rust, TypeScript validator는 빈 keys, column이 없는 쌍, 한쪽에 두 번 나오는 column을 IR_INVALID로, 모르는 column을 COLUMN_UNKNOWN으로 거절한다. planner는 모든 쌍을 bind하므로 relation step은 (tenant_id, account_id) IN ((...))를 읽는다. generated code의 match<L>With<R>()는 호출 순서대로 쌍 하나를 더하고(Rust Core::add_match가 쌍을 덮어쓰던 set_match를 대신한다), 자기 연결을 가진 자식의 relation도 모든 성분으로 key를 만들고 거른다(성분이 둘 이상이면 tuple 조건). Go, PHP, Rust, TypeScript의 composite_key_rows case는 tenant 990004(account 1과 2, membership member와 owner)와 tenant 990006(account 2, membership guest)을 쓰고, tenant 990004 account의 membership을 matchTenantIdWithTenantId().matchAccountIdWithAccountId()로 같은 statement에서, 그리고 자기 연결을 가진 자식으로 읽어 정확히 990004/1:990004/1/member와 990004/2:990004/2/owner를 기대하며, 쓴 row를 모두 지운다. Red: 변경 전에는 네 client 모두 MySQL, PostgreSQL, SQLite에서 남는 row 990004/2:990006/2/guest로 실패했다. 두 번째 match가 첫 번째를 바꿔 relation이 account_id만으로 이어졌기 때문이다. Green(각각 시드된 bench database의 MySQL, PostgreSQL, SQLite): Go TestCoverageCompositeKeyRows 3/3, PHP와 TypeScript composite_key_rows 각 3/3, Rust coverage_composite_keys::coverage_composite_key_rows 3/3, 네 client의 model_query_rows relation case 12/12. 바뀐 code의 owner check는 모두 통과한다: go test ./engine/...(7 package, 새 TestCompositeRelationKeys 포함), Go clients/go/orm의 relation, key, shape test 14개, Go TestJoinsAndRelations 3 database; TypeScript tsc --noEmit, tests/engine.mjs(새 composite relation case), tests/generate-scan.mjs(2 case), tests/models-scan.mjs(2 test), tests/model.mjs 3 database; PHP tests/engine_test.php(새 composite relation case), tests/relation_keys.php, tests/model_test.php(11 test × 3 database); Rust engine::tests(4 test, 새 composite_relation_keys), orm-build generate(17 test), integration joins_and_relations와 writes 3 database, orm, orm-build, orm-tests의 clippy -D warnings; 기록한 symbol과 hash로 interface-check, docs-rules-check, checklist-check, git-check, go-fmt-check, rust-fmt-check. 다시 생성한 Go와 TypeScript bench model은 MatchTenantIdWithTenantId와 matchAccountIdWithAccountId를 가진다.[o]
T19생성된 Go relation getter에서 실패한 type assertion을 보고한다. Cause: clients/go/model/composite_account.go의 GetMemberships 같은 generator 출력은 v, _ := x.m.Related(...).(...)로 읽어 맞지 않는 relation 값을 보고하지 않고 버린다.Acceptance: 생성된 Go relation getter는 (<result>, error)를 돌려준다. 저장된 relation 값, row에 related row가 없으면 error 없는 nil result, 저장된 값의 type이 다르면 relation result 이름과 요청한 type을 담은 INTERNAL error다. 생성된 Go model을 모두 다시 생성하고 어느 것도 v, _ :=로 relation을 읽지 않는다. 실패한 downcast_ref나 다른 relation 종류에 None을 돌려주는 Rust relation getter도 orm::Result<Option<..>>로 같은 규칙을 따른다. tracked Go와 Rust unit case가 맞지 않는 relation 값을 저장하고 INTERNAL을 받으며, Go와 Rust generator test가 생성된 getter를 확인한다. PHP와 TypeScript는 저장된 relation 값을 type 변환 없이 돌려주므로 버릴 수 없다.근거: 변경 전 Red: go test ./generator -run '^TestGoRelationGetterReportsMismatch$'는 생성된 getter가 값 하나를 돌려줘 실패했다("not enough return values, have (*model.BrandModel) want (*model.BrandModel, error)"). clients/go/orm의 TestRelatedAsReportsMismatch는 compile되지 않았다("undefined: RelatedAs"). orm-build test relation_getters_report_a_mismatched_relation_value는 generated source lacks pub fn get_owner(&self) -> orm::Result<Option<&super::user::User>>로 실패했다. orm unit test relation_results_report_a_mismatched_value는 related_one이 Option<&M>을 돌려줘 compile되지 않았다. Go generator는 return orm.RelatedAs[<result>](../x.m, "<name>")를 쓰고, orm.RelatedAs는 result가 없거나 related row가 없는 row에 zero value를, 다른 type에 INTERNAL relation result <name> holds <stored type>, not <requested type>를 돌려준다. related_one과 related_many는 Result<Option<..>>를 돌려주고, 다른 model, row로 읽은 collection, collection으로 읽은 row를 result 이름과 요청한 type을 담은 INTERNAL로 보고한다. clients/go/model을 다시 생성했고(author, composite_account, service, user. clients/go/decimalmodel은 relation이 없어 그대로다), 어느 Go source도 v, _ :=로 relation을 읽지 않는다. Go와 Rust test와 conformance runner의 호출부는 error를 전파하고, docs/interfaces(.ko).md가 규칙을 적고, contracts/symbols/go.json과 rust.json이 새 signature를, contracts/interfaces.json이 그 hash를 기록한다. 새 clients/go/orm/related_test.go는 contracts/features.json의 model_queries feature에 속한다. 실행한 owner check는 모두 통과했다: Red case 네 개, go test ./generator(generator test 전체, 70.6 s), go test ./contracts ./tests/interfaces/check, SQLite와 이번 실행에서 만들고 끝나고 지운 MySQL과 PostgreSQL database에서 clients/go/model의 TestJoinsAndRelations, bench database 세 개에서 featurecoverage case TestCoverageModelQueryRows와 TestCoverageCompositeKeyRows, Rust orm-build generate test(18개 통과), SQLite와 만들고 지운 MySQL과 PostgreSQL database에서 Rust integration case joins_and_relations, bench database 세 개에서 Rust coverage case coverage_model_query_rows와 coverage_composite_key_rows, 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, git-check.[o]
T20Rust client_bench의 잘못된 반복 횟수 인자를 거부한다. Cause: clients/rust/tests/src/client_bench.rs는 숫자가 아닌 인자를 3000으로 바꾼다(.parse().ok().unwrap_or(3000)). 어느 명세도 이 기본값을 정하지 않는다.Acceptance: client_bench는 iterations 인자를 요구한다. 인자가 없거나 1 이상의 정수가 아니면 연결하기 전에 status 1로 끝나고 iterations 인자와 받은 값을 담은 error를 출력한다. 숫자가 아닌 값은 거부하지만 인자가 없으면 3000번과 1000번을 반복하는 bench/rust의 native와 driver_compare도 같은 방식으로 인자를 요구한다. tracked process case가 세 program을 인자 없이, 숫자가 아닌 인자로, 너무 작은 인자로 실행한다. Go bench와 PHP 성능 test는 반복 횟수 인자를 받지 않고 TypeScript에는 bench program이 없으므로 이런 기본값이 없다.근거: 변경 전 Red: clients/rust/tests/tests/client_bench_args.rs의 두 case가 실패했다. 인자 없는 client_bench와 "many"를 받은 client_bench가 DSN 검사까지 진행했기 때문이다("ORM_BENCH_MYSQL_DSN is required ..."). bench/rust/tests/dsn.rs에서 benchmarks_require_the_iterations_argument는 인자 없는 native가 DSN 검사에 닿아 실패했고, benchmarks_reject_an_invalid_iterations_argument는 인자 이름이 없는 옛 message "iterations must be an integer of at least 3, got "many""에서 실패했다. 세 program은 각각 iterations(minimum) 함수 하나로 첫 인자를 읽고 기본 횟수가 없다. 인자가 없으면 usage: <program> <iterations>; the iterations argument is required를, 최소값(client_bench 1, native 3, driver_compare 10) 이상의 정수가 아니면 the iterations argument must be an integer of at least <minimum>, got <value>를 출력하고, 둘 다 연결하기 전에 status 1로 끝난다. docs/perf(.ko).md가 이를 적고, 새 clients/rust/tests/tests/client_bench_args.rs는 contracts/features.json의 performance_gate feature에 속한다. 실행한 owner check는 모두 통과했다: cargo test -p orm-tests --test client_bench_args --test client_bench_dsn(4개 통과), bench/rust의 cargo test --test dsn(4개 통과)과 시드된 MySQL bench database의 cargo test --test seeded(2개 통과), bench database에서 client_bench 10이 n=10인 통계 줄 세 개를 출력했다, orm-tests와 bench/rust의 cargo clippy -D warnings, rust-fmt-check, docs-rules-check, checklist-check, git-check.[o]
T21Rust catalog grid에서 MySQL과 PostgreSQL의 날짜와 시간 cell을 decode한다. Cause: GridCell에 시간 값이 없어 datetime column이 있는 table의 table_page가 MySQL과 PostgreSQL에서 "Unsupported catalog cell type"으로 실패한다. 그래서 T15의 catalog_read case는 user의 page를 읽는다.Acceptance: GridCell에 dbspec text 형식 YYYY-MM-DD, HH:MM:SS, 소수 자릿수를 가진 YYYY-MM-DD HH:MM:SS를 담는 Date, Time, DateTime cell이 있다. MySQL DATE, TIME, DATETIME과 PostgreSQL date, time, time zone 없는 timestamp가 이 cell로 decode된다. 모든 connection이 zone 없는 값을 UTC로 읽으므로 time zone type(MySQL TIMESTAMP, PostgreSQL timestamptz, timetz)은 지원하지 않는 채로 남는다. 기술된 table의 read(table page, row lookup, insert read)는 column이 선언한 precision p만큼 정확히 소수 자릿수를 쓰고(p 없는 MySQL datetime은 0, p 없는 PostgreSQL timestamp는 6), column 선언이 없는 read-only grid query는 여섯 자리를 쓴다. 00:00:00부터 23:59:59.999999 밖의 시각은 다른 값으로 바뀌지 않고 실패한다. temporal primary key cell은 row identity bind로 명시적으로 실패한다. SQLite에는 temporal storage class가 없으므로 decimal처럼 cell이 저장된 값의 storage class를 유지하고, dbspec column에서는 CHECK된 text를 그대로 읽는다. tracked live-db test가 세 database에서 date, time(0), time(3), datetime(0), datetime(6) column이 있는 table의 page를 읽고 같은 값을 grid query로 읽는다. catalog_read case는 MySQL, PostgreSQL, SQLite에서 datetime(6) column이 있는 author의 page를 읽는다. catalog connection은 Rust client에만 있으므로(feature catalog_connection) 다른 client에는 이 결함이 없다.근거: decoder 전, GridCell에 cell 세 개를 더한 상태의 Red: 새 live-db test clients/rust/orm-build/tests/grid_temporal.rs가 MySQL과 PostgreSQL에서 "table page: Err("error occurred while decoding: Unsupported catalog cell type")"와 temporal key page의 같은 오류로 실패했고 SQLite는 통과했다. author의 page를 읽도록 바꾼 coverage_catalog_read는 MySQL과 PostgreSQL bench database에서 "author page: "error occurred while decoding: Unsupported catalog cell type""로 실패했고 SQLite에서 통과했다. MySQL decoder는 sqlx로 DATE, DATETIME, TIME을 읽고 음수나 24시간인 TIME을 거부한다. sqlx chrono decoder가 24:00:00을 00:00:00으로 바꾸므로 PostgreSQL decoder는 binary date, time, timestamp 값을 직접 읽는다. clients/rust/orm-build/src/catalog/temporal.rs는 table page, row lookup, insert read 세 개에서 decode한 여섯 자리 소수를 선언된 p로 줄이고, 버리는 자리가 0이 아니면 GRID_TEMPORAL_PRECISION으로 실패한다. test의 grid query는 page가 03:04:05와 2026-01-02 03:04:05를 읽는 곳에서 03:04:05.000000과 2026-01-02 03:04:05.000000을 읽는다. temporal cell이 있는 row는 key update를 받는다. CAST('25:00:00' AS TIME), CAST('24:00:00' AS time), PostgreSQL infinity 값, MySQL TIMESTAMP column, PostgreSQL timestamptz는 실패한다. DATE primary key의 update는 "temporal row identity is not supported"로 실패한다. catalog가 temporal column에 쓰는 값은 여전히 text로 bind되고 다시 읽은 temporal cell과 같지 않으므로, 이 write는 성공하지 않고 ROW_WRITE_MISMATCH로 실패한다. docs/interfaces(.ko).md와 docs/schema(.ko).md가 cell을 적고, 새 test는 contracts/features.json의 catalog_connection feature에 속하며, contracts/symbols/rust.json과 그 hash가 새 함수를 기록한다. 실행한 owner check는 모두 통과했다: SQLite, MySQL, PostgreSQL에서 grid_temporal(각 실행은 만든 table을 지웠다), bench database 세 개에서 coverage_catalog_read와 coverage_catalog_row_mutation, cargo test -p orm-build --features live-db 전체(unit test와 doc test를 포함한 test target 21개), precision을 줄이는 unit test 두 개, live-db orm-build와 orm-tests의 cargo clippy -D warnings, make interface-check와 go test ./contracts ./tests/interfaces/check, node scripts/features/build.mjs --check, rust-fmt-check, docs-rules-check, checklist-check, git-check. node scripts/features/check.mjs는 T8.8.3 merge의 clients/php/tests/sqlite_without_mysql.php만 feature 없는 test file로 보고한다.[o]
T21.1기술된 table의 SQLite 날짜·시간 cell을 temporal grid cell로 준다(T21 후속). Cause: SQLite에서는 기술된 table의 date, time, datetime column이 Text로 읽히는데, 같은 dbspec column이 MySQL과 PostgreSQL에서는 Date, Time, DateTime이 되어 한 neutral type의 catalog grid가 database마다 다르다. 완료 기준: 기술된 table을 읽을 때(table page, row lookup, insert 결과) 같은 column과 값이 세 database에서 같은 temporal cell이 된다. column 선언이 없는 read-only grid query는 SQLite에서 저장된 text를 그대로 둔다.—근거: 변경 전 Red: SQLite page에서 temporal cell을 기대하도록 바꾼 grid_temporal이 SQLite에서 "sqlite: table page: Ok(TablePage … Text(…))"로, 그리고 page가 text로 받아들인 형식 밖의 값들로 실패했고, SQLite bench database의 coverage_catalog_read가 "sqlite: first author page"(left Text("2026-06-01 00:00:00.000000"), right DateTime(…))로 실패했다. clients/rust/orm-build/src/catalog/temporal.rs는 DATE, TIME, DATETIME으로 선언된 SQLite column의 Text cell이 dbspec 형식(0001-01-01부터 9999-12-31까지의 달력 날짜, 0~6자리 소수의 하루 안 시각)이면 Date, Time, DateTime으로 바꾸고, NULL은 그대로 두며, 그 밖의 값은 GRID_TEMPORAL_VALUE: <column> holds a value that is not a dbspec <kind>로 실패시킨다. SQLite renderer CHECK가 소수 자릿수를 p로 고정하므로 cell은 같은 column과 값의 MySQL, PostgreSQL cell과 같다. read-only grid query는 SQLite에서 여전히 Text를 준다. SQLite DATE key도 다른 database처럼 row identity bind에서 실패한다. docs/interfaces(.ko).md가 이를 적고, contracts/symbols/rust.json과 그 hash가 새 함수를 기록한다. 실행한 owner check는 모두 통과했다: SQLite, MySQL, PostgreSQL에서 grid_temporal(SQLite page에서 2026-01-02T03:04:05Z, 20260102, 2026-02-30 03:04:05, 2026-01-02 24:00:00, 소수 일곱 자리를 거부하고, 만든 table을 지웠다), orm-build unit test 12개(dbspec 형식 case 포함), bench database 세 개에서 coverage_catalog_read와 coverage_catalog_row_mutation, cargo test -p orm-build --features live-db 전체, live-db orm-build와 orm-tests의 cargo clippy -D warnings, make interface-check와 go test ./contracts ./tests/interfaces/check, rust-fmt-check, docs-rules-check, checklist-check, git-check.[o]
T21.2catalog grid로 날짜·시간 cell을 쓴다(T21 후속). Cause: catalog 쓰기가 temporal 값을 text로 bind해서, 다시 읽은 temporal cell이 쓴 cell과 달라 모든 database에서 쓰기가 ROW_WRITE_MISMATCH로 실패한다. 완료 기준: date, time, datetime column의 catalog insert와 update가 MySQL, PostgreSQL, SQLite에서 성공하고, 다시 읽은 cell이 column이 선언한 정밀도에서 쓴 cell과 같다.—근거: 변경 전 Red: 값을 text로 bind하는 grid_temporal의 새 write case에서 insert가 SQLite와 MySQL에서 "ROW_WRITE_MISMATCH: stored inserted values differ"로, PostgreSQL에서 "column "d" is of type date but expression is of type text"로 실패했고, DATE key의 update가 세 database에서 "ROW_UPDATE_INVALID: temporal row identity is not supported"로 실패했다. P와 ParamType에 Date, Time, DateTime이 생겼다. text는 dbspec 형식이어야 하며 아니면 실행 전에 TOOL_BIND_INVALID다. MySQL과 PostgreSQL은 chrono date, time, timestamp 값을 bind하고 PostgreSQL은 그 parameter type을 선언하며, SQLite는 text를 bind한다. temporal grid cell은 이 bind와 서로 바뀌므로 temporal row identity도 bind된다. case는 date, time(3), datetime(6) column에 2026-01-02, 03:04:05.120, 2026-01-02 03:04:05.123456을 넣고 2027-12-31, 23:59:59.999, 0001-01-01 00:00:00.000001로 고치며, insert, update, page에서 쓴 cell을 그대로 다시 읽고, 2026-01-02T03:04:05를 거부하고, DATE key의 row를 고친다. typed NULL case가 세 종류를 다룬다. docs/interfaces(.ko).md가 bind를 적고, contracts/symbols/rust.json과 그 hash가 이를 기록한다. 실행한 owner check는 모두 통과했다: SQLite, MySQL, PostgreSQL에서 grid_temporal(만든 table을 지웠다), typed_binds를 포함한 cargo test -p orm-build --features live-db 전체, bench database 세 개에서 coverage_catalog_read와 coverage_catalog_row_mutation, live-db orm-build와 orm-tests의 cargo clippy -D warnings, make interface-check와 go test ./contracts ./tests/interfaces/check, rust-fmt-check, docs-rules-check, checklist-check, git-check.[o]
T22root의 schema:check npm script를 제거한다. Cause: package.json이 T8.2.6.4가 제거한 node scripts/schema/check.mjs를 실행한다.Acceptance: root package.json에 schema:check script가 없고, history file 밖의 어떤 tracked file도 그 이름을 쓰지 않는다. make repo-check는 root npm script가 tracked file도 tracked file의 directory도 아닌 repository path(/가 있는 단어)를 쓰면 script와 path를 담아 실패한다. tracked node test가 없는 path, tracked file, tracked directory, /가 없는 단어를 다루고 make repo-check가 이 test를 실행한다.근거: 제거 전 Red: scripts/repo/check.mjs에 새 path 검사를 넣자 make repo-check가 "package.json script schema:check names scripts/schema/check.mjs, which is not a tracked file or directory"와 "repository check: 1 script path problems remain"으로 실패했다. package.json에서 schema:check 줄을 지웠고, CHANGELOG와 checklist 밖의 어떤 tracked file도 그 script나 scripts/schema/check.mjs를 쓰지 않는다. scripts/repo/scripts.mjs의 scriptPathErrors는 root npm script에서 /가 있는 모든 단어를 repository path로 보고, 앞의 ./를 뗀 뒤 tracked file이나 tracked file을 가진 directory를 받는다. scripts/repo/check.test.mjs가 네 경우를 다루고 Makefile target이 검사 전에 이를 실행한다. 실행한 owner check는 모두 통과했다: make repo-check(node test, 그다음 "repository check: 890 tracked paths inspected"), docs-rules-check, checklist-check, git-check.[o]
T24모든 check가 case마다 실행 중에 결과를 보고하고 case마다 시간 한도를 갖게 한다(AGENTS testing 규칙). Cause: make feature-check는 약 25분 돌고 coverage.mjs가 끝날 때까지 아무것도 출력하지 않는다. dbspec-introspect-compare-check는 5-11분 돌고 database마다 시작과 끝만 출력한다. Makefile의 physical Go check는 전체 실행 한도로만 묶여 있다(dbspec-apply-check는 -timeout 70m, conformance runner check는 40m과 35m, dialect facts check는 0). 완료 기준: 모든 check가 실행 중에 case마다 시작, 결과(성공, 또는 이유와 함께 실패), 경과 시간을 출력한다. 모든 case가 자기 시간 한도를 가지며 전체 실행 한도가 그것을 대신하지 않는다. 1분보다 오래 도는 case는 단계와 경과 시간을 출력한다. test가 각 runner의 출력 형식을 검사한다.—근거: T24.1-T24.5가 모든 Makefile target과 runner script를 살폈다. Go test(check, dbspec physical check, dialect-facts, conformance check, example, fuzz, dbspec-go, client-db와 perf script, feature 명령)는 GO_TEST(go test -v -timeout 0)와 internal/testcase case를 쓴다. Node test와 runner(checklist, repository, git, docs-rules, docs-build, docs-check, docs-static, docs-verify-idempotent, feature-check, feature-docs, dbspec-ts와 TypeScript physical check, dbspec-compare, ts-check, ts-min-check, ts-model, timing, group-rows, unselected-column, decimal physical check)는 tests/testcase.mjs를 쓴다. PHP test(client-unit, dbspec-php와 PHP physical check, client-db, perf, php-without-mysql, conformance-result, tests/codec/check.php)는 tests/testcase.php를 쓰고, tests/conformance/run_result_tests.py는 지웠다. Rust test와 program(dbspec-rust, Rust physical check, rust-send-savepoint, rust-driver, client-db의 integration program)은 orm-testcase를 쓴다. interface와 conformance CLI는 testcase.Run을 쓴다. build, format, lint, package 명령은 tests/run-case.mjs를 쓴다. check가 아니어서 바꾸지 않은 것: 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, stress와 vector 생성기. make testcase-check가 Go, JavaScript, PHP, Rust, 명령 harness의 형식을, coverage.test.mjs가 coverage.mjs의 형식을 확인한다. 전체 suite는 checklist가 끝난 뒤 한 번 실행한다.[o]
T25빈 database가 필요한 모든 case에 자기 database를 준다. Cause: schemaEmpty, conditions, joinsAndRelations case와 설치 전에 schema().empty()를 확인하는 다른 case가 ORM_TEST_MYSQL_DSN의 고정된 공유 database와 ORM_TEST_POSTGRES_DSN의 public schema에서 돌아서, 다른 실행이나 중단된 실행이 남긴 table 하나로 실패한다. 2026-10-03 전체 실행에서 client는 옳았는데도 feature 명령 다섯 개(model_queries PHP와 TypeScript, parameter_chunking Rust, PHP, TypeScript)와 ts-check가 a database without tables로 실패했다. 완료 기준: 빈 database를 확인하거나 schema를 설치하는 모든 case가 자기 이름의 database(MySQL)나 schema(PostgreSQL), 또는 자기 SQLite file을 만들고 끝날 때, 실패한 때에도 지운다. 어떤 case도 공유 database가 비어 있다고 가정하지 않는다. test가 공유 database에 table을 남겨 두고도 case가 통과하며 자기 것을 남기지 않음을 보인다.—근거: T25.1(Go, internal/testdb), T25.2(PHP, clients/php/tests/case_database.php), T25.3(TypeScript, clients/typescript/tests/case-database.mjs), T25.4(Rust, clients/rust/case-database)는 빈 database를 확인하거나 schema를 설치하는 각 case에 ORM_TEST_MYSQL_DSN이나 ORM_TEST_POSTGRES_DSN을 admin 연결로 써서 만든 database orm_case_<pid>_<n>이나 SQLite file orm-case-<pid>-<n>.sqlite를 주고, case가 끝날 때 실패한 뒤에도 지우며, 둘 다 STEP 줄로 보고한다. PostgreSQL은 이 항목이 말한 schema 대신 database를 만든다. schema().empty()는 public이 아닌 모든 schema를 내용으로 세므로, 공유 database 안의 case schema로는 빈 database 확인을 통과할 수 없기 때문이다. 조사(네 client의 test와 feature, coverage runner에서 empty(), install(, ORM_TEST_*_DSN, DROP TABLE 검색): 공유 database가 비었는지 확인한 case는 TypeScript schemaEmpty와 install()(model.mjs에서 설치하는 모든 case), PHP schema empty, Rust schema_empty였다. 공유 database에 설치한 case는 T25.1-T25.4에 적었다. feature 명령 model_queries와 parameter_chunking은 model_test.php, model.mjs, Rust integration program을 실행하는 검증 명령에서 실패했다. coverage case(coverage_*)는 feature-check가 주고 실행마다 전후 상태를 읽는 seed된 bench DSN에서 실행되므로 그대로 둔다. dbspec physical test와 schema-set test는 이미 자기 database를 썼다(schema-set test는 이제 helper를 쓴다). orm_test에만 닿는 PgBouncer 항목 orm_test_single을 거치는 statement timeout case는 그곳에 자기 이름의 table로 설치하고 그 table만 지운다. make case-database-check(scripts/case-database-check.mjs, make check의 일부)는 TypeScript client와 Rust integration program을 build하고, 공유 MySQL database와 PostgreSQL public schema에 row 하나를 가진 table orm_leftover_<pid>를 만들고, tests/conformance/check의 공유 상태 digest, PostgreSQL schema, orm_case_ database, orm-case- file을 읽은 뒤 Go TestSchemaEmpty와 ./clients/go/model, PHP model_test.php, TypeScript model.mjs, Rust integration program을 실행한다. 각각이 통과하고 앞서 읽은 값이 모두 그대로여야 하며, 자기 table은 finally에서 지운다. 그 table과 다른 실행이 남긴 table routed, unowned가 공유 database에 있는 채로 통과했다: Go TestSchemaEmpty 1과 ./clients/go/model 18 case, PHP 66, TypeScript 57, Rust 30이 FAIL 없이 통과했고, case들은 database를 각각 2, 18, 35, 33, 20개 만들고 지웠다. 공유 상태는 mysql 238574e7…와 postgres 636d2bfd…로 그대로였고, orm_case_ database와 orm-case- file은 남지 않았으며, table은 지워졌다.[o]
T24.1모든 Go test와 Go check runner가 case마다 자기 기한을 갖고 보고하게 한다(T24의 일부). Cause: Go test는 -v 없이 package 결과만 출력했고, physical check는 제각각인 start/result 줄을 쓰거나 아무것도 쓰지 않았으며, 한도는 test binary 전체의 -timeout 값뿐이었다. 완료 기준: 모든 Go test case가 실행 중에 시작과 기한, 1분을 넘을 수 있으면 단계, 결과와 경과 시간을 출력한다. 모든 case가 자기 기한을 가진다. Makefile, script, feature 명령의 어느 것도 binary 전체의 한도로 그것을 대신하지 않는다. test가 출력 형식을 검사한다.—근거: 새 package internal/testcase가 한 형식을 준다: RUN <case> deadline=<d>, STEP <case> elapsed=<d>: <step>, PASS <case> elapsed=<d>, FAIL <case> elapsed=<d>: <reason>(test 실패의 이유는 그 위에 기록된 error다), SKIP <case> elapsed=<d>, subtest가 기한을 가지는 test의 RUN <group> group. Start(t, deadline)는 기한에 끝나는 context와, 기한 5 s(Grace) 뒤 FAIL … deadline <d> exceeded와 모든 goroutine stack을 출력하고 binary를 끝내는 watchdog을 준다. Run(name, deadline, work)는 test가 아닌 program의 case를 보고하고, StepWriter는 하위 process 출력의 빈 줄이 아닌 줄을 STEP 줄로 바꾼다. 기한 등급은 근거를 적는다: Compute 1m(memory와 repository file만. 그런 case 가운데 가장 큰 TestStressDocument가 2.4 s), Database 2m, Process 5m(cache가 빈 go run/go build). 다른 Go test file 120개의 모든 top-level Test와 Fuzz 함수가 case를 시작한다. 하는 일에 맞는 등급이나 자기 기한을 쓴다: TestDialectFacts, TestDDLVectors, TestSchemaDocumentsApply, TestIntrospectRoundTrip, TestIntrospectUnsupported, TestPlanApply, TestApplyChain, TestSQLiteFileNameWithQuery의 probe case는 1m(probe 30 s와 정리 30 s)이고 시작과 관찰한 note를 단계로 출력한다. TestApplyStressPlan은 database마다 21m이고 적용한 step 2000개마다 단계를 출력한다. TestIntrospectCompare는 database마다 15m이고 statement 1000개마다와 runner마다 단계를 출력한다. TestApplyChainAcrossClients는 client 순서쌍마다 7m이고 runner 동작마다 단계를 출력한다. conformance physical test는 build case(40m과 30m)와 언어와 database마다 32m case를 가지며 첫 실행, 반복 실행, 비교를 단계로 출력하고, state check는 database마다 6m이다. TestExampleOutputsAreIdentical은 예제마다 5m, hot path gate는 5m이다. TestCPULoad는 부모 process에서만 case를 시작한다. 부하를 만드는 하위 process는 부모가 끝낼 때까지 돌기 때문이다. go run ./tests/conformance/check run은 conformance/<db>/build, /<language>, /compare case를 보고하고 명령과 그 출력 줄을 단계로 출력한다. go run ./tests/interfaces/check는 interfaces/manifest, interfaces/rust-build, interfaces/<language>, interfaces/results를 보고하고 실패 이유를 FAIL 줄에 담는다. Makefile은 모든 Go test를 -timeout 10m, 70m, 30m, 40m, 35m, 3m, 0 대신 GO_TEST = go test -v -timeout 0으로 실행하고, scripts/client-db-test.sh, scripts/perf-test.sh, contracts/features.json의 Go 명령도 -v -timeout 0을 쓴다(-timeout 45s와 90s는 없어졌다). make testcase-check는 TestReportForm을 실행한다. 이 test는 fixture test를 하위 go test -v로 실행해, 단계가 있는 통과 case, error 뒤에 실패한 case, 건너뛴 case, case 두 개를 묶은 group, 단계와 빈 줄을 뺀 하위 출력 줄과 함께 통과하는 Run, 이유와 함께 실패하는 Run, context 기한에 끝나는 Run, 기한을 넘겨 멈춘 Run, 기한을 넘겨 FAIL과 goroutine stack을 출력하고 PASS가 없는 case의 줄과 순서를 확인한다. 실행한 owner check는 모두 통과했다: make testcase-check. ./engine/..., ./contracts, ./tests/interfaces/..., ./tests/conformance/..., ./tests/dialects, ./generator의 go test -v -timeout 0(generator case 24개, 가장 긴 것 11.0 s). TestSQLiteFileNameWithQuery. TEST_ENV에서 TestDialectFacts, TestApplyChain, TestPlanApply의 SQLite probe. SQLite의 TestApplyStressPlan(STEP TestApplyStressPlan/sqlite elapsed=2.249s: 2000 of 12000 steps applied … PASS TestApplyStressPlan/sqlite elapsed=1m15.03s). SQLite의 TestIntrospectCompare(statement 단계 13개와 runner 단계 4개, PASS TestIntrospectCompare/sqlite elapsed=1m17.936s). SQLite의 TestPhysicalCounterCleanup. TestClockMicrosecondsSQLite. go run ./tests/conformance/check run -driver sqlite 두 번(cache가 빈 build에서 PASS conformance/sqlite/build elapsed=3m7.386s, 이어서 각 언어와 비교). go run ./tests/interfaces/check -language go --self-test. physical, featurecoverage, ormtest, examples, decimalphysical tag의 go vet. go-fmt-check, docs-rules-check, checklist-check, git-check.[o]
T24.2모든 JavaScript test와 Node check runner가 case마다 자기 기한을 갖고 보고하게 한다(T24의 일부). Cause: make feature-check는 coverage.mjs가 끝날 때까지 아무것도 출력하지 않았다. TypeScript test와 Node runner는 제각각인 start/result/ok 줄이나 요약만 출력했고, 일반 test script는 기한 없이 첫 실패에서 멈췄다. 완료 기준: 모든 Node test file과 Node runner에 T24.1과 같은 기준을 적용하고, test가 출력 형식을 검사한다.—근거: tests/testcase.mjs가 Go 형식(RUN <case> deadline=<d>, STEP, PASS, FAIL … : <reason>, Go time.Duration처럼 쓴 시간)을 준다. runCase(AbortSignal이 기한에 끝나고 case는 5 s 뒤 실패한다), caseTest(case를 실행하는 node:test test), cases()(script의 차례 case와 종료 코드), sections()(끊을 수 없는 코드 구역: 기한 뒤 FAIL과 종료, 구역 안에서 process가 끝나면 FAIL), runGroup(runner가 자기 case를 보고하는 명령), stepLines(하위 출력 줄을 단계로)다. 등급은 COMPUTE 1m, DATABASE 2m, PROCESS 5m이다. coverage.mjs는 native 실행마다 <feature>/<role>/<language>/<database> run <n>을 보고하며, 기한은 그 실행의 process 한도(state reader와 native 명령마다 10m)의 합이고, state digest와 명령마다 단계를, cargo와 go의 stderr 줄을 단계로 DSN을 가린 채 출력한다. features/check.mjs는 검증 명령마다 group으로 보고하고 그 출력 줄을 단계로 출력하며 --feature <id>를 받는다. 모든 node:test file이 이전 timeout이나 COMPUTE로 caseTest를 쓴다(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, features test. tests/conformance/result_typescript.test.mjs. tests/dbspec/compare의 check와 runners test. tests/dbspec/inputs.test.mjs. 멈춘 하위 process의 줄을 단계로 출력하는 tests/timing/preempted.test.mjs). 일반 script aggregate_numeric, decimal_model, group_rows, dsn, engine, generate, generate-scan, codec-vector, styled-value, clock, driver-error, rollback, schema-set, sqlite-concurrency, model은 구역마다 case로 실행하고 모든 case 뒤에 종료 코드를 정한다. group-rows, unselected-column, decimal physical runner, tests/dbspec/compare/check.mjs, checklist, git, repository, docs-rules, feature-docs, docs build, docs 멱등성, docs static check가 자기 case를 보고한다. coverage.mjs나 physical runner가 출력을 읽는 owner file(coverage_*.mjs, group_rows_db.mjs, decimal_model_db.mjs)은 정확한 CASE <id> PASS 출력을 지키고 그 runner가 보고한다. docs build가 T19와 T20 기록의 raw <program>과 <name> placeholder에서 멈췄으므로 그것들을 code span으로 바꿨다. make testcase-check는 tests/testcase.test.mjs도 실행한다. 이 test는 하위 process에서 단계와 함께 통과하는 runCase, 이유와 함께 실패하는 것, signal로 끝나는 것, 기한을 넘겨 멈춘 것, 하위 줄과 빈 줄을 뺀 runGroup, 통과, 실패, case 안에서 끝남, 기한을 넘겨 멈춰 종료 코드 1인 sections, node --test 아래의 caseTest의 줄과 순서를 확인한다. coverage.test.mjs는 통과한 실행 두 번과 실패한 실행 하나의 줄 아홉 개를 확인한다. 실행한 owner check는 모두 통과했다: make testcase-check. 위의 node:test file(test 265개). TypeScript physical test의 SQLite case(83개). TypeScript runner의 compare와 input test(34개). 멈추게 한 Go와 TypeScript timing case. dsn, engine, generate, generate-scan, codec-vector, styled-value, aggregate_numeric, decimal_model, group_rows, sqlite-concurrency. clock, driver-error, rollback, schema-set(SQLite, MySQL, PostgreSQL에서 case 33개). model.mjs(case 57개, 가장 긴 것 1.724 s). node scripts/features/coverage.mjs --feature planner(실행 8번). node scripts/features/check.mjs --run --feature planner. unselected-column(9개)과 group-rows(15개) physical check. docs build, 멱등성(두 build가 file 179개에서 같다), static check. checklist, git, repository, docs-rules check.[o]
T24.3모든 PHP test case가 자기 기한을 갖고 보고하게 하고 Python result runner를 없앤다(T24의 일부). Cause: PHP test는 RUN/PASS 줄을 여러 형태(cpuMs=, elapsedMs=, ok , 요약)로 출력했고, 대부분 FAIL 줄 없이 첫 실패에서 멈췄으며, render, manifest, introspect, runtime case는 기한이 없었고, runtime_model과 runtime_db는 case 대신 script 전체를 묶었다(30 s, 120 s). tests/conformance/run_result_tests.py는 자기 형식을 출력했다. 완료 기준: 모든 PHP test script에 T24.1과 같은 기준을 적용하고, test가 출력 형식을 검사한다.—근거: tests/testcase.php가 Go 형식을 준다. testcase_run(closure case), TestCases(script의 case와 종료 코드), testcase_begin/testcase_step/testcase_end(코드 구역. script를 끝낸 잡히지 않은 error는 shutdown handler가 Uncaught <class>: 접두와 file을 뺀 message로 FAIL 줄을 출력한다), 등급 TESTCASE_COMPUTE 60 s, TESTCASE_DATABASE 120 s, TESTCASE_PROCESS 300 s다. pcntl이 있으면 기한의 SIGALRM이 case를 끊고(system call을 다시 시작하지 않는다) 5 s 뒤 FAIL을 출력하고 process를 종료한다. pcntl이 없을 때(php-without-mysql-check의 공식 PHP image)와 alarm의 초 단위보다 짧은 기한에서는 기한을 넘긴 case가 끝날 때 실패한다. 전체 error는 stderr로, message는 FAIL 줄로 간다. case_clock.php의 cpuCaseBegin/cpuCaseEnd는 각 CPU 시간 한도를 검사로 지키고, 멈춘 case는 그 한도의 열 배 wall-clock 시간 뒤 종료한다. timing-check가 process에 CPU의 10분의 1만 주기 때문이다. 바꾼 test: dsn, relation_keys, hostcodec, engine, runtime_model(script 전체의 30 s 한도는 없어졌다), orm_gen, aggregate_numeric, decimal_model, group_rows, unselected_column, styled_value, aes_json, runtime_db(script 전체의 120 s 한도는 없어졌다), sqlite_concurrency, sqlite_without_mysql, perf_gate(측정한 조회마다 5분, 비율은 단계), model(구역 13개 각각), clock, driver_error, rollback, schema_set, 모든 dbspec test(CPU 합계를 cpu-total case로 둔 vectors와 rules, manifest, render, mermaid, plan, stress, apply cleanup, introspect, introspect stress, plan apply, apply). tests/conformance/result_php.php도 case다. coverage owner file과 group_rows_db, decimal_model_db, unselected_column_db owner는 정확한 CASE <id> PASS 출력을 지킨다. make conformance-result-check는 TypeScript build, PHP result test, TypeScript result test, tests/conformance/runner_go의 go test -v -timeout 0을 직접 실행하고, run_result_tests.py는 지웠다. make testcase-check는 tests/testcase_test.php도 실행한다. 이 test는 하위 process에서 단계가 있는 통과 case, 실패 case, 1 s 기한을 넘겨 PHP 코드에서 멈춘 case와 sleep에서 막힌 case, grace가 끝날 때까지 끊김을 무시하는 case, 통과, 실패한 검사 보고, 잡히지 않은 error로 끝남, 멈춤의 구역, pcntl을 끈 늦은 case와 구역의 줄과 순서를 확인한다. 실행한 owner check는 모두 통과했다: php tests/testcase_test.php. make client-unit-check(case 28개). make dbspec-php-check(script마다 두 번 실행해 PASS 줄 502개). TEST_ENV에서 clock, driver_error, rollback, schema_set(case 33개), aes_json(3개), runtime_db(9개), model(66개), sqlite_concurrency(3개), dbspec_plan_apply(36개), dbspec_apply(120개), dbspec_introspect(57개), dbspec_introspect_stress. bench database의 perf_gate(비율 1.22와 1.17). PHP dbspec script 일곱 개의 멈추게 한 timing case. make conformance-result-check.[o]
T24.4모든 Rust test case가 자기 기한을 갖고 보고하게 한다(T24의 일부). Cause: cargo test는 binary마다 결과만 출력했다. test는 제각각인 RUN, PASS, start, pass, passed, running, finished, result 줄을 println!으로 출력했는데 libtest는 --nocapture 없이 그것을 숨기고, test마다 기한이 없었다. 완료 기준: 모든 Rust test와 Rust check program에 T24.1과 같은 기준을 적용하고, test가 출력 형식을 검사한다.—근거: workspace crate clients/rust/testcase(orm-testcase. 모든 crate와 bench/rust의 dev-dependency, orm-tests의 dependency)가 Go 형식을 stderr에 준다. 직접 쓰므로 libtest의 capture가 숨기지 않고, stdout의 test <name> ... ok 줄은 parser가 그대로 읽는다. case!(deadline)은 libtest test와 같은 이름의 case를 시작하고, start(name, deadline)은 안쪽 case를, Case::step, step(thread의 가장 안쪽 case. helper 함수가 쓴다), Case::fail(panic 없는 FAIL)을 준다. panic한 case는 panic message를 FAIL 이유로 출력하고, watchdog thread는 기한에 GRACE(5 s)를 더한 때 FAIL을 출력하고 101로 끝내므로 case 안의 tokio::time::timeout이 먼저 실패한다. 등급은 COMPUTE 60 s, DATABASE 120 s, PROCESS 300 s이고, CPU 시간 한도를 검사하는 test에는 wall_for_cpu(limit) = 한도의 열 배를 쓴다(timing-check가 process에 CPU의 10분의 1만 준다). 모든 #[test]와 #[tokio::test] 함수(file 91개의 228개. crate 안 unit test와 bench/rust를 포함한다)가 case를 시작한다. orm-schema dbspec test는 test와 각 vector, rule, Mermaid, plan case에 CPU 한도의 wall_for_cpu를 쓴다. physical dbspec test의 probe case, 정리 case, SQLite path vector는 안쪽 case다. 제각각이던 줄은 단계가 되거나, case 자신의 RUN과 PASS 줄과 같은 말이면 지웠다. integration program은 <case>/<database> case(각 2분)를, dbspec_stress example은 parse와 emit 단계를 가진 case 하나를 보고한다. make testcase-check는 cargo test -p orm-testcase도 실행한다. 이 crate의 report_form은 fixture로 test binary를 다시 실행해, 단계와 helper 단계가 있는 통과 case, message 첫 줄을 이유로 가진 panic case, 통과한 case 안의 실패한 안쪽 case, async case, 기한을 넘겨 FAIL을 출력하고 101로 끝나며 PASS가 없는 case의 줄과 순서를 확인한다. 실행한 owner check는 모두 통과했다: cargo test -p orm-testcase. cargo test -p orm-schema -p orm-case-clock(PASS 줄 240개). TEST_ENV에서 orm test zone, sqlite_file_name, dbspec_apply_cleanup, clock(PASS 줄 37개). database마다 단계를 출력한 orm-build --features live-db의 grid_cells와 tool_columns. 시드된 bench database에서 bench/rust cargo test -- --test-threads=1(test 6개). 세 database에서 release integration program의 --case conditions. dbspec_stress example. -D warnings의 cargo clippy --workspace --all-targets, -p orm-build --features live-db, -p orm --features test-faults. rust-fmt-check. T24.1에서 commit한 internal/testcase/testcase_test.go(gofmt하지 않은 식 하나)에서 실패했고 이 항목에서 gofmt한 뒤 통과한 go-fmt-check.[o]
T24.5check의 build, format, lint, package와 남은 runner 명령을 case로 보고한다(T24의 일부). Cause: 자기 case를 보고하지 않는 명령(tsc와 npm build, Rust release build와 clippy, go test -c, gofmt, cargo fmt, go generate, package, rust-150, php-without-mysql script)은 시작, 결과 줄, 기한 없이 도구 출력만 냈고, codec 교차 검사와 feature manifest 검사는 요약만 출력했다. 완료 기준: check 안의 그런 명령마다 시작과 기한, 경과 시간을 담은 출력 줄 단계, 결과를 출력하고 기한에 멈추며, test가 형식을 검사한다.—근거: tests/run-case.mjs는 명령 하나를 case로 실행한다(node tests/run-case.mjs <name> <n>s|<n>m [--cwd <dir>] -- <program> [args]): 기한과 함께 RUN, 빈 줄이 아닌 stdout과 stderr 줄마다 STEP, PASS, 또는 종료 상태를 담은 FAIL을 출력하고, 기한에 명령의 process group을 끝내며 deadline <d> exceeded로 실패한다. Makefile은 RUN_CASE, BUILD_DEADLINE(30m: release build, clippy, go test -c는 빈 target에서 모든 의존성을 compile하며 여기서 2.5-4분 걸린다), TOOL_DEADLINE(5m: tsc, gofmt, cargo fmt, go generate, npm, container 시작 한 번), TSC_BUILD를 정의하고, tsc build 여덟 개, dbspec-apply-pairs, dbspec-introspect-compare, dbspec-compare, dbspec-rust(stress example은 한 번 build하고 직접 두 번 실행한다), example, timing check의 Rust example build, rust-send-savepoint-check의 clippy, feature-check, timing-check, typescript-build의 npm run typescript:build, ts-check의 npm run typescript:check, go-model-check, go-fmt-check, rust-fmt-check, rust-check의 명령 네 개, rust-150-check, package-check, php-without-mysql-check를 감싼다. scripts/client-db-test.sh는 integration build를 감싼다. scripts/php-without-mysql.sh는 자기 RUN과 PASS 줄을 더 출력하지 않고, 공식 image 안의 PHP case가 pcntl 없이 스스로 보고한다. tests/codec/check.php는 vector, error, 다른 언어 output case를 보고하고, scripts/features/check.mjs는 manifest 검사를 features/contracts로 보고하며, CI workflow는 go test -v -timeout 0을 실행한다. tests/testcase.test.mjs는 run-case.mjs에 대해 stdout과 stderr 줄을 단계로 가진 통과 명령, 3으로 끝나는 명령, 1 s 기한을 넘겨 끝낸 명령, usage error를 확인한다. 실행한 owner check는 모두 통과했다: make testcase-check. make go-fmt-check, rust-fmt-check, go-model-check(19.6 s), typescript-build, package-check(1m7.853s, cargo, composer, go 줄이 단계), php-without-mysql-check(container의 RUN sqlite_without_mysql deadline=2m0s와 PASS 줄이 단계). php tests/codec/check.php. node scripts/features/check.mjs.[o]
T24.6다음 단위로 반올림되는 시간을 그 단위로 쓴다(T24.5 후속). Cause: make decimal-physical-check에서 관찰: PASS decimal/postgres/go/2 elapsed=1000ms. JavaScript, PHP, Rust harness는 반올림하기 전 값으로 단위를 골라 999.6 ms를 1000ms로, 999.6 µs를 1000µs로 썼고, Go는 1s와 1ms로 쓴다. 완료 기준: 모든 harness가 다음 단위로 반올림되는 값을 Go 형식으로 쓰고, 각각 tracked case를 가진다.—근거: 변경 전 Red: 새 duration case가 "Expected values to be strictly equal"(JavaScript), "duration 0.0009996: 1000µs, want 1ms"(PHP), left: "1000µs"(Rust)로 실패했다. tests/testcase.mjs, tests/testcase.php, clients/rust/testcase는 반올림한 값으로 단위를 고르고, 세 harness 모두 0.9996 ms는 1ms, 999.6 ms는 1s다. 실행한 owner check는 모두 통과했다: 세 형식 test(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, git-check.[o]
T25.1schema를 설치하거나 빈 database를 확인하는 모든 Go database case에 자기 database를 준다(T25의 일부). Cause: clients/go/model의 databases()와 clients/go/orm case TestAESJSONColumn, TestWithContextCancels, TestWithContextCancelsTransaction, TestWithContextCancelsInsideTransaction, TestMySQLInstallInsideTransaction, TestJsonTextOrderPreservation, TestSQLKeywordNames, TestStyledFixturePhysicalCells, TestConnectionsUseUTC, TestStatementTimeout, TestStatementTimeoutThroughAPooler는 공유 database에서 자기 table을 지우고 그곳에 설치했고, TestPrimaryAndReplica는 공유 database에 썼다. orm package helper newDatabase(clients/go/decimalmodel에 복사되어 있었다)를 쓰는 case만 자기 database를 가졌다. 완료 기준: Go case에 T25와 같은 기준을 적용한다.—근거: internal/testdb의 New(t, driver)는 ORM_TEST_MYSQL_DSN이나 ORM_TEST_POSTGRES_DSN을 admin 연결로 써서 orm_case_<pid>_<n>을, SQLite는 임시 directory에 file orm-case-<pid>-<n>.sqlite를 만들고, path만 바꾼 DSN을 돌려주며, t.Cleanup에서 실패한 뒤에도 지운다(DROP DATABASE, PostgreSQL은 WITH (FORCE)). 지우지 못하면 database 이름과 함께 case가 실패한다. 새 testcase.Of(t)가 찾은 case(t나 가장 가까운 바깥 test에 Start한 case. TestReportForm이 그 줄을 검사한다)의 STEP 줄로 database <name> created와 database <name> dropped를 보고하고, Retarget은 replica의 같은 이름 database DSN을 만든다. PostgreSQL도 schema가 아니라 database를 만든다. Empty()는 public이 아닌 모든 schema를 내용으로 세기 때문이다. 위 case, 이전 newDatabase의 모든 case(TestSchemaEmpty, TestInstallAppliesRenderedStatements, clock, audit, refusal, rollback, runtime value case, ormtest TestRollbackFault)와 decimalmodel TestSchemaSet이 이것을 쓰고, 공유 table을 지우는 code는 없어졌다. TestStatementTimeoutThroughAPooler는 PgBouncer 항목 orm_test_single이 묶인 database에만 닿으므로 그곳에 자기 이름(testdb.Name())의 table로 zone document를 설치하고, 실패한 뒤에도 그 table만 지운다. 공유 DSN에 남은 case는 schema를 설치하지도 빈 database를 확인하지도 않는다. TestCancelledTransactionClearsSessionState, TestTransactionReportsFailedLocalReset, TestTransactionReportsFailedLockRelease, TestPoolSize, TestPoolIdleSizeAndLifetime은 session 상태, named lock, user variable을 쓰고, featurecoverage case는 feature-check가 주고 전후에 읽는 bench DSN에 남는다. 공유 database가 비었는지 확인한 Go case는 없었으므로 남은 table로 실패한 Go case도 없었다. 다른 실행이 남긴 table routed와 unowned가 두 공유 database에 있는 채로 바뀐 test의 go test -v -count=1이 통과했다: ./clients/go/orm 36 case, ./clients/go/model 9, ./clients/go/decimalmodel 9, -tags ormtest TestRollbackFault 4. ORM_TEST_MYSQL_DSN과 ORM_TEST_POSTGRES_DSN을 ProxySQL과 PgBouncer DSN으로 바꾸어도 같은 수가 통과했고, 그 뒤 orm_case_ database와 table, orm-case- file은 남지 않았다. make testcase-check와 go-fmt-check가 통과했다.[o]
T25.2schema를 설치하거나 빈 database를 확인하는 모든 PHP database case에 자기 database를 준다(T25의 일부). Cause: model_test.php(schema empty, database()와 그것을 쓰는 모든 case, replica case), aes_json_test.php, clock_test.php, driver_error_test.php, rollback_test.php, runtime_db_test.php는 공유 database에서 자기 table을 지우고 그곳에 설치했고, schema empty는 그 database에 table이 하나도 없기를 요구했다. 완료 기준: PHP case에 T25와 같은 기준을 적용한다.—근거: clients/php/tests/case_database.php의 case_database($driver, $step)은 ORM_TEST_MYSQL_DSN이나 ORM_TEST_POSTGRES_DSN을 admin 연결로 써서 orm_case_<pid>_<n>을, SQLite는 임시 directory에 file orm-case-<pid>-<n>.sqlite를 만들고, DSN(path만 바꾼다), replica의 같은 database를 위한 related(), drop()(DROP DATABASE, PostgreSQL은 WITH (FORCE). 지우지 못하면 database 이름을 담은 error를 던진다)을 가진 CaseDatabase를 돌려준다. with_case_database()는 body가 실패해도 body 뒤에 지우고, 둘 다 database <name> created와 dropped를 testcase_step으로 보고한다. PostgreSQL도 schema가 아니라 database를 만든다. empty()는 public이 아닌 모든 schema를 내용으로 세기 때문이다. model_test.php는 구역의 모든 case database를 modelEnd()에서 지운다. schema empty, statement timeout, install inside a transaction, connection time zone('', +00:00, UTC), $tests의 모든 case, primary and replica(replica 연결은 replica에 database가 생긴 뒤 같은 database를 연다)와 다른 다섯 script의 모든 case가 이것을 쓰고, schema_set_test.php는 자기 복사본 대신 helper를 쓴다. 공유 table을 지우는 code는 없어졌다. statement timeout through a pooler/postgres는 PgBouncer 항목 orm_test_single이 묶인 database에만 닿으므로 그곳에 table 하나짜리 runtime document를 자기 이름의 table로 설치하고 finally에서 그 table만 지운다. 공유 DSN에 남은 case는 schema를 설치하지도 빈 database를 확인하지도 않는다: pool size, 거부되는 time zone, failed local reset/mysql, failed lock release/mysql. coverage_*.php script는 feature-check가 주고 전후에 읽는 bench DSN에 남는다. Red: 바꾸기 전에 ORM_TEST_MYSQL_DSN과 ORM_TEST_POSTGRES_DSN이 table t25_leftover 하나를 가진 database orm_t25_red(옛 case가 공유 table을 지우지 않도록 이 실행을 위해 만들었다)를 가리키게 하자 model_test.php는 schema empty/mysql에서 "a database without tables", schema empty/postgres에서 "a database without tables"와 "the empty schema dropped"로 실패했다. 다른 실행이 남긴 table routed와 unowned가 두 공유 database에 있는 채로 model_test.php 66 case, aes_json_test 3, runtime_db_test 9, schema_set_test 9, clock_test 9, driver_error_test 6, rollback_test 9가 통과했다. ProxySQL과 PgBouncer를 거쳐도 같았는데, rollback_test의 rollback_failed/mysql과 savepoint_rollback_failed/mysql은 바꾸지 않은 script도 ProxySQL을 거치면 실패한다(T26). 그 뒤 orm_case_ database와 table, orm-case- file은 남지 않았다. 바뀐 file의 php -l이 통과했다.[o]
T25.3schema를 설치하거나 빈 database를 확인하는 모든 TypeScript database case에 자기 database를 준다(T25의 일부). Cause: model.mjs(schemaEmpty, 그리고 설치하는 모든 case가 쓰며 설치 전에 schema().empty()를 확인하는 install()), clock.mjs, dbspec_runtime_db.mjs, driver-error.mjs, rollback.mjs는 공유 database에서 자기 table을 지우고 그곳에 설치했으므로, 다른 실행이 남긴 table이 a database without tables와 schema().empty() before install을 실패시켰다. 완료 기준: TypeScript case에 T25와 같은 기준을 적용한다.—근거: clients/typescript/tests/case-database.mjs의 createCaseDatabase(dialect, step)은 ORM_TEST_MYSQL_DSN이나 ORM_TEST_POSTGRES_DSN을 admin 연결로 써서 orm_case_<pid>_<n>을, SQLite는 임시 directory에 file orm-case-<pid>-<n>.sqlite를 만들고, 이름, DSN(path만 바꾼다), drop()(DROP DATABASE, PostgreSQL은 WITH (FORCE). 지우지 못하면 database 이름을 담은 error를 던진다)을 돌려준다. withCaseDatabase()는 finally에서 지우고 body와 지우기가 함께 실패하면 두 이유를 모두 남기며, 둘 다 실행 중인 case의 STEP 줄로 database <name> created와 dropped를 보고한다. relatedDsn()은 replica의 같은 이름 database DSN을 만든다. PostgreSQL도 schema가 아니라 database를 만든다. empty()는 public이 아닌 모든 schema를 내용으로 세기 때문이다. model.mjs의 schemaEmpty, conditions, joinsAndRelations, columnsAndSubqueries, writes, styledStates, transactions, aesRotation, bindLimitSplitting, statementTimeout, cancellation, aesJsonColumn, installInsideTransaction, connectionsUseUtc('', UTC, +00:00), sqlite/readOnly, primaryAndReplica(replica 연결은 replica에 database가 생긴 뒤 같은 database를 연다)는 case database에서 실행되고, 다른 네 script와 schema-set.mjs의 모든 case가 helper를 쓴다. 공유 table을 지우는 code는 없어졌다. postgres/statementTimeoutThroughAPooler는 PgBouncer 항목 orm_test_single이 묶인 database에만 닿으므로 그곳에 table 하나짜리 runtime document를 자기 이름의 table로 설치하고 finally에서 그 table만 지운다. 공유 DSN에 남은 case는 schema를 설치하지도 빈 database를 확인하지도 않는다: poolSize, failedLocalReset, failedLockRelease. coverage_*.mjs script는 feature-check가 주고 전후에 읽는 bench DSN에 남는다. Red: 바꾸기 전에 ORM_TEST_MYSQL_DSN과 ORM_TEST_POSTGRES_DSN이 table t25_leftover 하나를 가진 database orm_t25_red(옛 case가 공유 table을 지우지 않도록 이 실행을 위해 만들었다)를 가리키게 하자 model.mjs는 25 case를 통과하고 32 case에서 실패했다: mysql/schemaEmpty와 postgres/schemaEmpty가 "a database without tables"(PostgreSQL은 "the empty schema dropped"도)로, conditions와 joinsAndRelations를 포함해 MySQL과 PostgreSQL에서 설치하는 모든 case가 "schema().empty() before install"로 실패했다. 다른 실행이 남긴 table routed와 unowned가 두 공유 database에 있는 채로 npm run typescript:build 뒤 model.mjs 57 case, node --test dbspec_runtime_db.mjs 24, schema-set.mjs 9, clock.mjs 9, driver-error.mjs 6, node --conditions=orm-test rollback.mjs 9가 통과했다. ProxySQL과 PgBouncer를 거쳐도 같았는데, rollback.mjs의 rollback_failed/mysql과 savepoint_rollback_failed/mysql은 바꾸지 않은 script도 ProxySQL을 거치면 실패한다(T26). 그 뒤 orm_case_ database와 table, orm-case- file은 남지 않았다. npm run typescript:check가 통과했다.[o]
T25.4schema를 설치하거나 빈 database를 확인하는 모든 Rust database case에 자기 database를 준다(T25의 일부). Cause: integration program(schema_empty와 설치하는 모든 case의 Env::without_tables, databases)과 orm test aes_json, audit, clock, refusal, rollback(test-faults 포함), value_types, zone은 공유 database에서 자기 table을 지우고 그곳에 설치했으므로, 다른 실행이 남긴 table이 schema_empty를 실패시켰다. 완료 기준: Rust case에 T25와 같은 기준을 적용한다.—근거: workspace crate clients/rust/case-database(orm-case-database. orm의 dev-dependency, orm-tests의 dependency)의 CaseDatabase::create(driver)는 ORM_TEST_MYSQL_DSN이나 ORM_TEST_POSTGRES_DSN을 admin 연결로 써서 orm_case_<pid>_<n>을 만들거나 SQLite file orm-case-<pid>-<n>.sqlite를 임시 directory에 잡고, dsn()(path만 바꾼다), replica의 같은 database를 위한 related_dsn(), drop()(DROP DATABASE, PostgreSQL은 WITH (FORCE). 지우지 못하면 database 이름과 함께 panic한다)을 준다. case가 panic하거나 기한을 넘기면 Drop guard가 자기 thread와 runtime에서 database를 지우고, 둘 다 orm_testcase::step으로 database <name> created와 dropped를 보고한다. PostgreSQL도 schema가 아니라 database를 만든다. empty()는 public이 아닌 모든 schema를 내용으로 세기 때문이다. integration의 Target::empty, installed, finish가 schema_empty, primary_and_replica(replica에 database가 생긴 뒤 replica의 같은 database에 연결한다), conditions, joins_and_relations, columns_and_subqueries, writes, transactions, aes_rotation, json_values, bind_limit_splitting의 case database를 갖고, 위 orm test의 모든 case가 이것을 쓴다. table 목록과 공유 table을 지우는 code는 없어졌다. statement_timeout_through_a_pooler는 PgBouncer 항목 orm_test_single이 묶인 database에만 닿으므로 그곳에 자기 이름의 table(CaseTable)로 zone document를 설치하고, Drop guard에서도 그 table만 지운다. 공유 DSN에 남은 case는 schema를 설치하지도 빈 database를 확인하지도 않는다: zone pool_size_bound와 postgres_float_round_trip, value_decode, 그리고 자기 probe table이나 temporary table을 만들고 지우는 src/tx.rs와 src/tx_send_tests.rs의 orm unit test. coverage_* test는 feature-check가 주고 전후에 읽는 bench DSN에 남는다. Red: 바꾸기 전에 ORM_TEST_MYSQL_DSN과 ORM_TEST_POSTGRES_DSN이 table t25_leftover 하나를 가진 database orm_t25_red(옛 case가 공유 table을 지우지 않도록 이 실행을 위해 만들었다)를 가리키게 하자 integration schema/bench.dbs --case schema_empty는 schema_empty/sqlite를 통과하고 schema_empty/mysql에서 "mysql: schema().empty() on a database without tables"로 실패했다. 다른 실행이 남긴 table routed와 unowned가 두 공유 database에 있는 채로 cargo test --locked -p orm --test의 aes_json 1, audit 1, clock 9, refusal 6, rollback 7, zone 11 case, --features test-faults rollback 10, value_types 1, release integration program 30 case(세 database의 schema_empty)와 cargo test -p orm-tests --bin integration이 통과했다. ProxySQL과 PgBouncer를 거쳐도 rollback 실패 case 넷(T26)을 빼고 같았고, 그 뒤 orm_case_ database와 table, orm-case- file은 남지 않았다. rust-fmt-check와 cargo clippy -p orm-case-database -p orm -p orm-tests --all-targets -- -D warnings가 통과했다.[o]
T26rollback 실패 case가 pooler를 거쳐도 통과하게 한다. Cause: ProxySQL을 거치면 clients/php/tests/rollback_test.php와 clients/typescript/tests/rollback.mjs의 rollback_failed/mysql, savepoint_rollback_failed/mysql case와 clients/rust/orm/tests/rollback.rs의 MySQL rollback 실패 case가 검사하는 error 없이 끝난다. case가 ProxySQL로 보내는 KILL이 transaction을 가진 server session을 끝내지 않기 때문이다. PgBouncer를 거치면 Rust case 자신의 session 연결이 unsupported startup parameter: extra_float_digits로 실패한다. 그래서 Go case는 통과하지만 make client-pooler-check는 실패한다. 완료 기준: 모든 client의 rollback 실패 case가 make client-pooler-check에서 MySQL과 PostgreSQL에서 검사하는 실패와 함께 통과한다.—근거: ProxySQL은 text protocol의 KILL <id>를 자기 client session에 대한 명령으로 받는다. ORM_TEST_PROXYSQL_DSN으로 확인하자 transaction 중인 backend thread에 대한 KILL <id>는 OK를 받았지만 session은 살아 있었고, prepared KILL ?는 server에 닿아 session을 종료했다. Go case(raw.Exec("KILL ?", id))가 통과한 까닭이다. Rust case는 session 연결을 sqlx::PgPool::connect로 열었는데, 그 기본값은 startup parameter extra_float_digits를 보내고 PgBouncer는 ignore_startup_parameters에 없는 그 parameter를 거부한다. client 자신은 보내지 않는다(orm::db의 extra_float_digits(None). PostgreSQL 12부터 기본값으로 float8을 정확히 출력한다). 그러므로 기본 설정의 PgBouncer 뒤 배포에는 client 쪽이 옳고, pooler 설정은 바꾸지 않는다. 변경: make target은 WITH_TEST_ENV에서 TEST_ENV의 server DSN을 ORM_TEST_MYSQL_SERVER_DSN과 ORM_TEST_POSTGRES_SERVER_DSN으로 export하고, client-pooler-check는 이 둘을 바꾸지 않는다. Go, PHP, TypeScript, Rust의 rollback 실패 case는 그 server에서 case database를 열고 rollback_probe의 lock을 가진 session을 KILL이나 pg_terminate_backend로 종료한다(case database 이름이 pooler 뒤의 transaction을 가려내는 표지이고, PostgreSQL은 pg_locks를 현재 database로도 거른다). 변수가 없으면 그 이름과 함께 실패한다. Rust session 연결은 client 연결(Db::connect)이다. case가 검사하는 ROLLBACK 오류는 그대로다. Red(바꾸기 전, ORM_TEST_MYSQL_DSN과 ORM_TEST_POSTGRES_DSN을 pooler DSN으로 두고 ProxySQL과 PgBouncer를 거친 실행): PHP rollback_test.php는 rollback_failed/mysql과 savepoint_rollback_failed/mysql에서 "transaction raises nothing", TypeScript rollback.mjs는 같은 두 case에서 "transaction raises undefined"로 실패했다. Rust는 test-faults가 있든 없든 rollback_failed_mysql과 savepoint_rollback_failed_mysql에서 "transaction: ()", rollback_failed_postgres와 savepoint_rollback_failed_postgres에서 "unsupported startup parameter: extra_float_digits"로 실패했다. Green(직접 연결, 그리고 ProxySQL과 PgBouncer를 거쳐): PHP rollback_test.php 9 case, TypeScript node --conditions=orm-test rollback.mjs 9, Rust cargo test -p orm --test rollback 7과 --features test-faults 10, Go TestRollbackFailed*, TestSavepointRollbackFailed*, -tags ormtest TestRollbackFault* 10이 통과했다. ORM_TEST_MYSQL_SERVER_DSN이 없으면 Rust MySQL case는 "ORM_TEST_MYSQL_SERVER_DSN is required"로 실패한다. orm_case_ database, rollback_probe table, orm-case- file은 남지 않았다. go-fmt-check, rust-fmt-check, cargo clippy -p orm --features test-faults --test rollback -- -D warnings, php -l이 통과했다.[o]
T27몇십 분씩 도는 check를 없앤다(T24 후속, AGENTS testing 규칙). Cause: T24는 check가 case마다 보고하게 했지만 실행 시간은 그대로 두었다. make feature-check는 약 40분 걸린다(2026-10-03 전체 실행은 12분 동안 case 107개를 마쳤고, 첫 Rust coverage case는 Rust coverage 명령마다 cargo를 실행해 compile하므로 2 m 40 s 걸렸다). 30분짜리 case 한도도 남아 있다(conformance check의 rustBuildDeadline, coverage case 한도). 완료 기준: 개발 machine에서 모든 check가 몇 분 안에 끝나고 check마다 잰 총 시간을 기록한다. build는 case 안이 아니라 check마다 case 전에 한 번 한다. 반복 실행은 만든 binary를 다시 쓴다. 모든 case 한도는 그 case가 하는 일에서 나오고 몇십 분인 것은 없다. 총 시간은 Evidence에 기록한다.—Evidence: T27.1-T27.6이 완료됐다. T29.1에서 빈 Rust target으로 시작한 make check 한 번이 scripts/check/run.mjs로 step 43개를 돌렸고 모두 906 s(15분 6초)에 통과했다. 가장 긴 step은 feature-check 3분 59초, client-pooler-check 1분 54초, conformance-check 1분 18초, client-db-check 1분 8초였고, 나머지 step은 모두 1분 안에 끝났다(T27 전에는 같은 check가 약 100분 걸렸고 feature-check가 34분이었다).[o]
T27.1feature coverage binary를 한 번 build하고 기능 명령을 한 번씩 실행한다(T27의 일부). Cause: coverage 실행 304번은 저마다 state reader를 go run으로 두 번 시작하고, Go test는 go test(compile과 link)로, Rust는 symbol마다 crate의 모든 test binary를 도는 cargo test로 실행했다(첫 Rust 실행은 compile에 2 m 40 s). 한도는 process마다 10분(실행마다 30분)이었다. state digest는 bench row 100000개를 JSON으로 encode했다(읽기마다 1.2-1.6 s). 검증 명령은 TypeScript client를 18번 다시 build하고 같은 명령 13개를 두 번 실행했다. T25 helper clients/php/tests/case_database.php와 clients/typescript/tests/case-database.mjs가 어느 기능에도 속하지 않아 manifest 검사가 실패했다.—Evidence: coverage.mjs는 state reader, test2json, Go package마다 test binary 하나, Rust crate마다 test binary를 한 번씩 build하고, build마다 8분 한도의 case로 보고한다. 모든 실행은 그 binary를 실행한다. Go test는 test2json으로, Rust entry의 symbol은 선언된 file을 compile한 test binary(dep-info로 찾는다)마다 process 하나에서 하나씩 차례로 실행하고(결과는 stdout에서 읽는다), 그 file을 compile한 binary가 없으면 실패한다. process마다 한도는 DATABASE 등급 2분이다. database는 서로 함께 진행하는 줄(lane)로 실행하므로 state 비교는 자기 database의 실행만 본다. state digest는 type을 붙인 값을 hash한다(읽기마다 0.45-0.9 s). 같은 검증 명령은 한 번 실행하고, 뒤 기능은 그 결과를 보고한다. 이전: coverage 1103 s와 검증 943 s(34분, 실패). 이후: 빈 Rust target에서 1020 s(17분), 통과. coverage.test.mjs의 12 case가 통과한다.[o]
T27.2conformance runner를 세 database에 한 번만 build한다(T27의 일부). Cause: make conformance-check는 database마다 checker를 실행했고, 실행마다 Rust와 TypeScript runner를 build하고 Go runner를 go run으로 여섯 번 시작했다. build 한도는 30분, runner 한도는 10분이었고, 전역 lock /tmp/orm-conformance.lock 하나가 다른 실행을 실패시켰다.—Evidence: run 한 번이 -driver/-dsn 쌍을 받아 Rust runner(8분 한도), TypeScript client(2분), Go runner binary(3분)를 한 번 build하고, runner마다 1분 한도로 실행한다(측정 1-3 s). state 읽기는 1분(측정 1 s 안), 언어 case는 8분이다. lock은 bench database DSN마다 잡는다. Rust runner는 test 의존성을 함께 쓰는 debug build다. 이전 197 s, 이후 make check 안에서 176 s.[o]
T27.3make check를 target마다, 자기 database에서, Rust toolchain과 target 하나로 실행한다(T27의 일부). Cause: make check는 target마다 보고하지 않고 첫 실패에서 멈췄다. target들은 공유된 seed orm_bench와 고정된 orm_decimal_case(손으로 만든 .runtime/decimal-env)를 썼다. case-database-check는 다른 실행의 test가 도는 중에도 server 전체의 orm_case_ 목록과 공유 orm_test database를 비교했다. Rust tx test는 orm_test에 고정 table orm_once_probe와 orm_timeout_probe를 지우고 만들었다. Rust는 1.98.0과 1.98.1을 함께 썼고, bench/rust와 rust-150은 자기 target을 썼으며, target은 incremental 결과와 전체 debug 정보를 남겼다(target의 절반쯤 돈 뒤 clients/rust/target 10.4 GiB와 bench/rust/target 1.4 GiB, client-db-check 하나가 약 10 GB를 더함). build 한도는 30분, 2000 table stress 한도는 15분과 20분이었다.—Evidence: scripts/check/run.mjs는 실행마다 orm_check_<pid>_<random>_bench와 _decimal을 만들어 seed하고(scripts/check/databases.sh), TEST_ENV와 DECIMAL_ENV로 넘긴다. target마다 남은 disk와 함께 묶음으로 실행하고, 실패한 뒤에도 계속해 모든 결과를 출력하며, 끝에 database를 지운다. feature-check가 이미 실행하는 target은 다시 실행하지 않는다. case-database-check는 끝난 process가 남긴 것만 남은 것으로 본다(공유 database는 PgBouncer orm_test_single 뒤의 database 그대로이므로, 그 digest는 같은 server에서 다른 실행이 test를 실행하지 않는다고 가정한다). tx probe는 case database를 쓴다. 모든 cargo 명령이 toolchain 하나(RUSTUP_TOOLCHAIN)와 CARGO_TARGET_DIR 하나를 incremental 결과 없이, 줄 번호 debug 정보만으로 쓴다: 실행 전체 뒤 target 하나가 3.9 GiB이고 bench/rust/target은 없다. build 한도는 8분이다(가장 긴 clean build의 두 배). stress 한도는 측정한 MySQL 작업(8m10s, 6-7.7분)에서 12분과 10분이고, DDL durability 설정은 이 시간을 14%만 바꾼다. Rust dbspec 한도는 5분이다. 이전: target 합계 약 100분(5985 s, feature-check 실패, client-pooler-check 실패). 이후: 42 step의 make check 한 번이 1897 s 걸렸고 모든 target을 보고했다(database 생성 77 s, client-db-check 449 s, 다시 실행한 feature-check 570 s, conformance-check 176 s, go-test-check 79 s, package-check 63 s, testcase-check 43 s, 나머지 target은 모두 50 s 아래로 예를 들어 rust-check 215 s 대신 31 s, example-check 165 s 대신 29 s, dbspec-apply-check 646 s 대신 37 s, dbspec-introspect-compare-check 456 s 대신 19 s). feature-check와 case-database-check(83 s)를 다시 실행한 값으로 target 합계는 약 100분 대신 약 2130 s(36분)다. client-pooler-check는 전처럼 실패한다(T29). 첫 실행은 결함 두 개를 찾았고, 고친 뒤 다시 실행해 통과했다: 한 entry의 Rust coverage symbol이 한 process에서 함께 실행되었고, case-database-check가 PgBouncer case에 pooler가 닿지 않는 database를 주었다.[o]
T27.42000 table case와 시간 budget case를 make bench로 옮긴다(T27의 일부). Cause: 2000 table stress 문서 때문에 dbspec-apply-check(646 s, MySQL 8m10s)와 dbspec-introspect-compare-check(456 s)가 가장 긴 check였고, 네 client의 stress parse budget이 dbspec check 안에서 돌았으며, 기능 runner는 test build와 따로 compile하는 release build였다. 이것들은 성능 측정이다.—Evidence: make bench는 dbspec-stress-bench(Go, PHP, TypeScript, Rust stress parse budget 각 두 번과 PHP introspection stress, Go TestStressDocument는 build tag bench), dbspec-apply-stress-bench, dbspec-introspect-compare-bench(2000 table, release Rust runner), dbspec-compare-bench(2000 table), timing-check를 같은 assertion으로 실행한다. make check는 dbspec-compare-check와 dbspec-introspect-compare-check를 같은 모양의 20 table 문서(node tests/dbspec/stress.mjs 20, 기본 출력은 그대로)로 실행하고, dbspec, integration, conformance, example runner를 debug로 build한다. dbspec-stress-bench(58 s), dbspec-compare-bench(48 s), timing-check(73 s)로 실행한 make bench가 통과한다. dbspec-apply-stress-bench와 dbspec-introspect-compare-bench는 변경 뒤에 실행하지 않았다.[o]
T27.5feature-check와 client-db-check를 몇 분 안으로 줄인다(T27의 일부). Cause: T27.1-T27.4 뒤 feature-check는 make check 안에서 570 s 걸린다: coverage 190 s(Rust test build 60-90 s와 네 줄의 실행), 검증 380 s이고, 그중 catalog-rust 92 s(orm-build live-db test build와 실행, client-db-check가 실행하는 것과 같은 명령), generation-go 55 s, schema-compare 30 s, queries-php 20 s가 크다. client-db-check는 449 s 걸린다: Go 29 s, PHP와 TypeScript 약 1분, 그다음 Rust의 workspace test build 97 s와 실행 약 2분, test-faults build와 실행 49 s, live-db build 57 s와 실행 약 50 s다.—Evidence: 모든 cargo test 명령은 --workspace --features $ORM_RUST_TEST_FEATURES(orm/test-faults, orm-build/live-db)를 써서 coverage build, client-db-check, dbspec Rust check, 기능 명령이 test build 하나를 함께 쓰고, client-db-check는 build 셋 대신 Rust test 명령 하나를 실행한다. go-test-check는 client-db-check, feature-check, testcase-check가 실행하는 package를 빼고 실행한다. client-db-check는 네 client를 병렬 줄로 실행한다(ORM_CLIENT_DB_LANES=parallel, client-pooler-check는 pooler 한도 때문에 차례로 실행한다). feature-check는 검증 명령을 ORM_FEATURE_LANES개(기본 4) 줄로 실행하고, contracts/features.json에서 exclusive인 명령(추적되는 file을 다시 만들고 git diff를 실행하는 generation-go)은 그 앞에서 혼자 실행한다. TypeScript primary와 replica case는 mysqlConnection(dsn, undefined)가 DSN의 기본 database를 쓰므로 replica가 case database를 만들기 전에 그 database를 열었다(Unknown database). 이제 database 없는 연결은 null로 요청하고, 기다림은 SELECT DATABASE()가 NULL인지 확인하며(전: 기다림 연결이 orm_case_58817_49를 열었다), replica 연결이 실패하면 기다림의 primary 연결을 닫는다(전: 닿지 않는 replica로 model.mjs가 60 s 안에 끝나지 않았다. 후: 10 s에 1로 끝난다). commit T27.5는 client-db-check가 이 case에서 실패하고 feature-check가 generation-go에서 실패한 채 만들어졌다(generation-go의 git diff가 commit하지 않은 Go replica test를 보았다). 이 tree에서 측정: client-db-check 86 s(T27.5 전 449 s), feature-check 251 s(T27.5 전 570 s, 검증을 차례로 실행하면 457 s). 알려진 한계: case-database-check는 공유 test database, 그 PostgreSQL schema, server 전체의 case database를 비교하므로 같은 server에서 같은 때 test를 실행하는 다른 실행이 그것을 바꿀 수 있다. 공유 database는 PgBouncer orm_test_single이 정하고, case database 이름에는 test process id가 들어가는데 Go는 go test가 test binary를 손자 process로 시작하므로 검사가 그 id를 알 수 없다.[o]
T27.6멈춘 JavaScript와 PHP case를 기한에 종료한다(T27의 일부). Cause: tests/testcase.mjs의 runCase와 caseTest는 기한에 GRACE를 더한 시간 뒤 FAIL을 출력하고 돌아갔으므로, timer나 연결을 쥔 채 멈춘 case가 process를 살려 두었다(Go와 Rust harness는 process를 종료한다. harness test의 멈춘 case는 handle을 쥐지 않았다). tests/testcase.php의 testcase_run과 testcase_begin은 pcntl이 없으면(공식 PHP image) 멈춘 case를 끝낼 수 없었다. 기한을 무시하는 case의 PHP fixture는 기한 error가 loop 반복 사이에 오면 0으로 끝났다.—Evidence: runCase는 기한에 GRACE를 더한 시간 뒤 FAIL <case> elapsed=<경과>: deadline <기한> exceeded and the case did not stop을 출력하고 process를 종료 코드 1로 종료한다. pcntl이 없으면 case와 구역마다 watchdog process를 시작하고, watchdog은 기한에 TESTCASE_GRACE를 더한 시간까지 case가 끝나지 않으면 같은 FAIL 줄을 출력하고 test process를 SIGTERM으로 끝내며(posix_kill, 없으면 shell의 kill), stdin이 닫히면(case나 process가 끝나면) 끝난다. testcase.test.mjs는 interval timer를 쥔 채 멈춘 case를 실행하고(전: fixture가 30 s 안에 끝나지 않았다. 후: FAIL 줄과 함께 1로 끝난다), testcase_test.php는 pcntl 없이 멈춘 case와 멈춘 구역을 실행하며(전: fixture가 30 s 안에 끝나지 않았다. 후: 둘 다 FAIL 줄과 함께 끝난다) 무시하는 loop를 try 안에 둔다(6번 중 6번 통과). make testcase-check가 통과한다.[o]
T28version 0.0.2를 release한다(T9를 뺀 이 branch의 다른 열린 item 모두와 통과한 전체 make check 한 번이 필요하다).Acceptance: 네 client, CLI, contracts의 모든 package manifest와 version 상수가 0.0.2다(Go module tag나 version 상수, PHP composer, TypeScript package.json과 lockfile, 모든 workspace crate의 Rust Cargo.toml과 Cargo.lock). CHANGELOG 쌍은 Unreleased를 0.0.2 section으로 옮기고, 그중 하나라도 다르면 check가 실패한다.Evidence: root VERSION 파일이 0.0.2를 적는다. orm Rust manifest 9개(package version과 orm, orm-schema에 대한 =0.0.2 path dependency), Cargo.lock 3개의 orm crate 항목(cargo metadata로 offline에서 다시 만듦), PHP composer 파일과 composer.lock의 content hash, TypeScript package와 lockfile root, contracts/features.json의 contract_version, README, SECURITY, AGENTS, plan 쌍과 PHP README가 0.0.2를 적는다. ordered-json은 자기 0.0.1을 유지한다. scripts/build-artifacts.sh와 scripts/package-check.sh는 VERSION을 읽고, scripts/features/check.mjs는 contract_version을 그것과 비교한다. make version-check(make check에 포함)는 scripts/version/check.test.mjs(test 3개: scripts/version/check.mjs가 생기기 전에는 load에 실패했다. repository가 일치하고, 바뀐 package.json version과 사라진 contract_version이 파일과 함께 보고된다)와 scripts/version/check.mjs를 실행하며, check는 처음에 낡은 lockfile 항목 12개를 보고했고 이어서 25 declarations agree on 0.0.2를 출력했다. make package-check가 @polyspec/orm-typescript@0.0.2, composer validate, v0.0.2의 Go module로 통과한다. CHANGELOG 쌍은 미발행 항목을 ## 0.0.2 아래로 옮긴다.[o]
T29Rust tx unit test가 ProxySQL을 거쳐도 통과하게 한다. Cause: make client-pooler-check는 cargo test --workspace에서 failed_local_reset_is_reported(끊긴 connection의 reset이 Ok를 돌려준다), every_failed_cleanup_step_is_reported(정리 실패 둘 가운데 하나만 보고), dropped_transaction_closes_its_connection(ProxySQL connection은 TLS가 아니다)으로 실패한다. 이 test들은 server session을 끝내고 server connection을 ORM_TEST_MYSQL_DSN으로 살피는데, 이 check는 그 DSN을 ProxySQL로 바꾼다.Acceptance: server 동작을 확인하는 test는 server DSN으로 server에 닿고, make client-pooler-check가 통과한다.Evidence: failed_local_reset_is_reported와 every_failed_cleanup_step_is_reported는 ORM_TEST_MYSQL_SERVER_DSN으로, dropped_transaction_closes_its_connection은 ORM_TEST_MYSQL_SERVER_DSN과 ORM_TEST_POSTGRES_SERVER_DSN으로 연결하고, 셋 다 make client-pooler-check에서 통과한다(전: 위의 세 실패). 그 뒤 check는 dbspec_apply에서 실패했고(PgBouncer를 거친 postgres.apply.lock: apply under another session's lock: Ok(()), want locked) T29.1이 그것을 다룬다. T29.1과 함께 make client-pooler-check가 120 s에 통과한다.[o]
T29.1connection이 server session 하나를 지키지 않으면 dbspec apply, recover, rollback, finalize를 멈춘다(T29 다음). Cause: 명령의 lock은 transaction 밖에서 하나씩 실행되는 statement를 덮어야 하는데, session advisory lock은 transaction pooling을 견디지 못한다. PgBouncer(pool_mode = transaction)를 거치면 apply는 lock 없이, error 없이 실행되었다: 다른 client가 lock을 잡은 채 남긴 server connection에서 apply가 성공했고, statement가 다른 server connection으로 옮겨 간 apply는 모든 step을 실행하고 unlock에서야 실패했다.—Evidence: Go, PHP, TypeScript, Rust에서 명령은 lock을 잡기 전에 server session(MySQL CONNECTION_ID(), PostgreSQL pg_backend_pid())과 그 session이 lock을 이미 잡고 있는지 읽고, lock statement는 server session을 돌려주며, 모든 step의 statement 앞에서 server session을 읽는다. 나눠 쓰거나 바뀐 session은 요구(직접 연결이나 session pooling 연결)가 message에 들어 있는 session error이며 다음 statement 전에 생긴다. docs/spec/plans.md(+ko)가 요구를 적는다. 각 client의 apply check는 두 경우를 PgBouncer로 자기 database에서 실행하고 session error를 요구한다(전: 나눠 쓴 lock은 error 없이 적용되었고, 옮겨 간 session은 the advisory lock of dbspec$plans was not held at unlock으로 끝났다). 다른 apply scenario와 Rust test의 dbspec probe server는 ORM_TEST_MYSQL_SERVER_DSN과 ORM_TEST_POSTGRES_SERVER_DSN을 쓰고, Rust schema-set case는 그 DSN으로 자기 database를 만들고 지운다(PgBouncer는 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와 make client-pooler-check가 통과한다. commit T29.1은 새 private symbol의 native symbol snapshot 없이 만들어졌으므로, 다음 commit이 contracts/symbols와 contracts/interfaces.json의 hash를 기록할 때까지 feature-check가 interface_contract에서 실패했다(go: unexpected engine/dbspec/apply.go::applier.checkSession, 다른 세 client도 같다).[o]
T30audit setting이 기록할 column을 고르게 한다. 원인: audit trigger가 감사 대상 table의 모든 column을 이력 table에 복사하므로 크거나 민감한 column을 이력에서 뺄 수 없었다. 인수 조건: 목록이 많아야 하나인 audit into <history> operation <col> action <col> previous <col> [exclude (col, ...) | include (col, ...)]. operation column은 언제나 기록한다. 이력 table은 identity key, action, previous column 말고는 기록하는 column만 같은 이름과 type으로 갖는다. trigger는 MySQL, PostgreSQL, SQLite에서 기록하는 column만 복사한다. introspection은 marker 없이 trigger에서 setting을 되살린다. 잘못된 사용은 rule이 있는 diagnostic이다. docs/spec/dbspec.md Audit(+ko)은 암호화가 필요한 값을 codec이 쓰기 전에 암호화하므로 trigger가 ciphertext를 복사한다고 적는다. 공유 vector는 dbspec-compare-check가 비교하고, Go, PHP, TypeScript, Rust에서 세 database의 physical case를 실행한다.—증거: Go(engine/dbspec의 AuditSetting.Exclude, Include, Records, Excluded, parser 목록, validator.auditLists), PHP(Setting::$exclude, $include, records, excluded, auditLine, Parser::auditListTokens, auditLists), TypeScript(auditRecords, auditExcluded, auditLine이 있는 src/dbspec/audit.ts와 parse 목록), Rust(Setting::Audit { lists }, AuditList, Setting::records, audit_line)가 목록을 같게 parse, 검사, emit, 렌더링, introspect한다. 목록의 column은 table의 column이고 한 번만 나와야 하며, 두 목록은 둘째 keyword에서 거부하고, operation column은 언제나 기록하고 기록하는 column 집합 하나는 한 가지로만 적으므로 어느 목록에서도 거부한다. 각각 적은 column에서 setting error다. database는 기록하는 column만 가지므로 schema text는 setting을 기록하지 않는 column의 column 순서 exclude 목록으로, 또는 목록 없이 쓴다. manifest text는 쓴 목록을 두고, include setting과 그와 같은 exclude setting은 schemaHash가 하나다. introspection은 audit_insert의 insert에서 기록하는 column을 읽어 그 형식으로 되살린다. 비교는 두 table에 다 있는 column 가운데 기록하는 집합이 다르면 drop_audit과 add_audit을 더하므로, setting이 기록하지 않는 새 column은 add_column으로 남는다(docs/spec/plans.md, +ko). 이력 table 검사는 네 client 모두에서 어긋난 곳마다 하나씩 이력 table 이름에서 보고한다. PHP와 Rust는 하나로 합친 error를 냈으므로, 자기 rule case setting-audit-shape, immutable-and-audit-on-cascade-child, audit-history-shape는 이제 Go와 TypeScript처럼 어긋난 곳마다 error 하나를 기대한다. Vector: tests/dbspec/cases.json의 audit-exclude, audit-include, audit-list-spacing, invalid case 아홉 개(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)와 hashes case audit-list-in-schema-text, 세 dialect의 statement와 behavior step(secret을 빼는 card의 insert, update, soft delete와 label을 넣는 tag의 insert, update가 다른 column 없는 이력 table에 기록되고, delete는 거부된다)이 있는 tests/dbspec/ddl.json audit-columns, tests/dbspec/plans.json comparison audit-recorded-columns. Physical: contracts/fixtures/audit_columns.dbs와 feature audit_triggers의 coverage case audit_selected_columns(Go TestCoverageAuditSelectedColumns, clients/php/tests/coverage_audit_columns.php, TypeScript와 Rust coverage_audit_selected_columns)가 client로 operation 7과 8을 쓰고 MySQL, PostgreSQL, SQLite에서 이력 column과 row를 요구한다. Red: vector를 더하고 이전 code로 Go는 TestSharedVectors, TestRenderVectors, TestCompareSchemas의 subtest 16개(unexpected 'exclude', expected the end of the line), TypeScript는 dbspec test 197개 중 17개가 실패했고, PHP는 dbspec_test, dbspec_manifest_test, dbspec_render_test, dbspec_plan_test가 첫 새 case에서 멈췄으며, Rust shared_dbspec_vectors는 panic했다. Green: dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check(runner 네 개가 두 번 같다), 세 database에서 ddl_audit_columns와 schema_audit_columns를 실행한 dbspec-ddl-check와 dbspec-introspect-check, dbspec-introspect-ts-check, dbspec-introspect-php-check, dbspec-introspect-rust-check, node scripts/features/coverage.mjs --feature audit_triggers(client 네 개, database 세 개, 각 두 run), node scripts/features/check.mjs --run --feature audit_triggers, 다시 기록한 symbol inventory로 interface-check, rust-check, ts-check, go-fmt-check, rust-fmt-check가 통과한다. 정정: T30는 Rust Setting enum의 doc comment를 symbol 기록 뒤에 rustfmt가 다시 줄바꿈해서 make interface-check와 feature-check의 interface_contract case가 실패하는 상태로 commit됐다. T30가 바뀐 symbol을 기록했고, 그 뒤 interface-check와 make feature-check(feature contract 17개)가 통과한다.[o]
T31모든 transaction 진입점이 operation id를 받게 한다. 원인: audit 대상 write는 operation id가 필요하지만, Rust Db::transaction_once(한 번 실행하고 재시도하지 않으며 callback 오류 type을 지킨다)는 operation id를 받지 않았다. operation(id)는 transaction과 transaction_send builder에만 있어 transaction_once를 거치는 모든 audit 대상 write가 CONFIG로 실패했다. 인수 조건: transaction_once는 operation(id)를 받는 builder이고 기존 .await 호출부는 바뀌지 않는다. 네 client의 모든 transaction 진입점이 operation id를 받는지 확인하며, MySQL, PostgreSQL, SQLite owner case, docs/operations/usage.md, docs/spec/interface-implementation.md, interface와 symbol 기록을 갖춘다.—증거: Rust Db::transaction_once(f)는 operation(id)를 가진 builder TransactionOnce를 돌려준다. 이를 await하면 TransactionOnceFuture(clients/rust/orm/src/tx.rs)가 실행된다. 이 state machine은 transaction이나 savepoint를 시작하고, task-local frame 안에서 callback을 한 번 실행하고, commit, release, rollback을 하며, 이전 결과를 그대로 지킨다: TransactionOnceError::Orm, Callback, Rollback, rollback fault, 이어 가는 panic이다. callback bound는 Fut: Future<Output = Result<T, E>>인 FnOnce() -> Fut이고 async || closure가 이를 만족하므로 기존 .await 호출부는 그대로 compile된다. ORM이 기다리는 단계는 box한 Send future이므로, callback, 그 future, T, E가 Send이면 이 future도 Send다. operation id를 가진 중첩 transaction_once는 CONFIG를 담은 TransactionOnceError::Orm을 돌려준다. Go Transaction, PHP transaction, TypeScript transaction은 Go WithContext와 TypeScript withSignal handle을 거쳐도 이미 operation id를 받으며, 이것이 그 client의 진입점 전부다. Red: audit_operation_id에 transaction_once(...).operation(10)을 넣은 cargo test --test audit는 "no method named operation found for opaque type impl Future<Output = Result<(), TransactionOnceError<orm::Error>>>"로 compile에 실패했다. Green: audit_operation_id(clients/rust/orm/tests/audit.rs)가 SQLite, MySQL, PostgreSQL case database에서 통과한다. transaction_send와 transaction_once의 write는 operation 9와 10으로 기록되고, id 없는 transaction_once는 CONFIG를 담은 Callback을, id를 가진 중첩 호출은 CONFIG를 돌려준다. orm library test 33개가 통과하며, 그중 send savepoint case는 이제 operation(7)을 정한 transaction_once future가 Send이기를 요구하고, one-shot과 savepoint 끝 case도 통과한다. rollback test 10개가 통과한다. audit_triggers feature의 coverage case audit_operation_entry_points(Go TestCoverageAuditOperationEntryPoints, PHP와 TypeScript coverage_audit_triggers, Rust coverage_audit_operation_entry_points)는 client의 모든 진입점으로 쓰고 item_history와 중첩 CONFIG를 확인하며, 네 client에서 이 실행의 MySQL, PostgreSQL, SQLite database로 통과한다. 기록한 Rust symbol로 interface-check가 통과하고, audit_triggers feature 검사, feature 문서, test-faults clippy, rustfmt, gofmt, docs-check, docs-rules-check, version-check, checklist-check가 통과한다. docs/operations/usage.md, docs/spec/interface-implementation.md, Rust README pair가 builder를 적는다. 대체됨: T35가 세 builder의 operation(id)를 .audit(values)로 바꾼다. transaction_once의 builder 형식은 남는다.[o]
T31.1cargo가 build한 위치에서 Rust symbol 도구를 찾는다. 원인: tests/interfaces/check의 buildRust는 tests/interfaces/rust/target/debug/orm-interface-symbols를 돌려주었지만, Makefile은 모든 cargo 명령에 CARGO_TARGET_DIR=clients/rust/target을 주므로 cargo는 도구를 그곳에 build했다. 새 worktree에서 go run ./tests/interfaces/check --self-test는 "fork/exec …/tests/interfaces/rust/target/debug/orm-interface-symbols: no such file or directory"로 실패했고, 예전 도구가 남은 checkout은 그 낡은 build를 실행했다(main checkout에는 2026-10-02의 build가 있었다).—증거: buildRust는 --message-format json-render-diagnostics로 build하고 cargo가 orm-interface-symbols에 대해 보고한 실행 file을 돌려주며, 보고가 없으면 오류를 돌려준다. TestBuildRustReturnsBuiltExecutable(tests/interfaces/check/rust_tool_test.go)은 CARGO_TARGET_DIR을 임시 directory로 정한다. 변경 전에는 "buildRust = …/tests/interfaces/rust/target/debug/orm-interface-symbols, want the executable cargo built in CARGO_TARGET_DIR …"로 실패했고, 변경 후 통과하며, 새 worktree에서 go run ./tests/interfaces/check --self-test가 통과한다. go vet과 gofmt가 통과한다.[o]
T32soft delete한 행을 명시적으로 되돌린다. 원인: soft_delete가 있는 table은 soft delete한 행의 unique key 값을 남기고 감사 대상 table은 physical DELETE를 거부하므로, 지운 연결(unique key를 가진 link 행)을 다시 더하면 DUPLICATE_KEY로 실패했다. planner는 언제나 soft delete filter를 더하므로 soft delete한 행을 읽거나 되돌리는 호출이 없었다. 인수 조건: 네 client에서 primary key나 unique key 하나로 찾은 soft delete한 행을 다른 column의 새 값과 함께 명시적으로 restore한다. restore는 UPDATE로 soft delete column을 비우고(audit은 다른 update처럼 그 operation과 함께 기록한다) 되돌린 model을 반환한다. 지워지지 않은 행과 없는 행의 결과는 문서에 정한다. 기본 읽기는 바뀌지 않는다. 공유 planner와 conformance vector, exclude 목록을 가진 감사 대상 table을 포함한 MySQL, PostgreSQL, SQLite owner case를 갖춘다.—증거: IR kind restore(engine/ir, PHP Validator, TypeScript와 Rust validate)는 primary key나 unique key 하나의 모든 column을 한 번씩 지정하는 eq predicate의 where와, update처럼 검사하는 새 값의 set을 받는다. planner(Go restoreStep, PHP Planner::restoreStep, TypeScript restoreStep, Rust restore_step)는 UPDATE <table> SET <새 값>, <soft delete> = NULL[, <operation> = ?] WHERE <key> AND <soft delete> IS NOT NULL을 만들고, soft_delete가 없는 table, 다른 predicate, 일부나 다른 key, primary key, identity, soft delete, operation column의 할당을 IR_INVALID로 거부한다. model method restore(Go Core.Restore와 generated Restore, PHP Model::restore, TypeScript Model.restore, Rust orm::model::restore와 orm-build template)는 모든 primary key column에 값이 있으면 primary key를, 아니면 column 모두에 값이 있는 첫 unique key(이름 순서)를 key로 쓰고(없으면 CONFIG), 나머지 지정 값을 restore와 함께 쓴 뒤 key로 행을 읽는다. 지워지지 않은 행은 아무것도 쓰지 않고 그대로 반환하며, 없는 행은 NO_ROWS, operation id 없는 감사 대상 restore는 CONFIG다. 네 generator와 docs/dsl.md에서 restore는 예약 column 이름이다. Red: Go TestRestorePlansGuardedUpdate는 "IR_INVALID: unknown kind "restore""로 실패했고, Go owner test는 "Restore undefined (type *orm.Core has no field or method Restore)"로 compile에 실패했으며 TestColumnNameRules는 "restore: got <nil>, want "reserved""로 실패했다. PHP engine_test와 TypeScript engine.mjs는 "IR_INVALID: unknown kind "restore""로 실패했고, PHP와 TypeScript generator test는 restore column을 받아들였으며, PHP와 TypeScript database case는 "Restore is not a column of membership"로 실패했다. Rust는 "IR_INVALID: unknown kind "restore"", "cannot find function restore in module orm::model", "reserved_method_names_are_not_columns: restore: ()"로 실패했다. 새 값으로 바꾼 변경은 네 client에서 먼저 "IR_INVALID: restore takes no set or optimistic"과 "restore of membership names every column of its primary key or of one unique key once with an eq value"로 실패했다. Green: contracts/fixtures/restore.dbs의 공유 scenario(unique key (team_id, member_id)와 note를 빼는 audit을 가진 membership, unique key (name)를 갖고 audit이 없는 label: 다시 삽입하면 DUPLICATE_KEY, 기본 읽기는 NO_ROWS, restore는 operation 4로 새 note를 쓰고 이력은 insert 1, 지운 update 2, 살아 있는 update 4를 가지며, 두 번째 restore는 행을 그대로 두고, 없는 key는 NO_ROWS, key 값 없는 restore는 CONFIG)가 Go TestRestoreSoftDeletedRow, PHP runtime_db_test, TypeScript dbspec_runtime_db, Rust restore_soft_deleted_row로 SQLite, MySQL, PostgreSQL case database에서 통과하고, audit_triggers의 coverage case soft_delete_restore로 네 client에서 세 database에 두 번 통과한다. planner coverage case planner_restore와 planner_restore_rejects_non_key(contracts/fixtures/planner.json)가 네 client에서 통과한다. conformance vector restore(rollback하는 transaction 안에서 soft_record를 seq와 새 name으로 restore)는 이 실행의 bench database에서 go run ./tests/conformance/check run이 MySQL, PostgreSQL, SQLite에서 26 vectors × 4 languages identical을 보고한 뒤 기록했고, soft_record는 선언한 counter table이다. rule Model.restore, 기록한 symbol, restore sequence로 interface-check가 통과하며, 세 출력의 --results는 delete_recursive sequence 기대값을 false에서 기록된 NO_ROWS로 고친 뒤 통과한다. feature 검사, go-model-check, ts-model-check, go vet, gofmt, rustfmt, Rust clippy 변형, docs-check, docs-rules-check, version-check가 통과한다. docs/operations/usage.md, docs/spec/protocol.md, docs/spec/dbspec.md, docs/spec/interfaces.md, docs/spec/interface-implementation.md, docs/spec/dsl.md(+ko)가 호출을 적는다.[o]
T33실행 중인 transaction 안에서 operation id를 정한다. 원인: 먼저 삽입하는 행(audit 대상이 아닌 operation 행)의 key를 operation id로 쓰는 작업 단위는 transaction이 시작한 뒤에야 id를 알지만, 네 client는 transaction을 시작할 때만 id를 받았으므로 그 감사 대상 write가 CONFIG로 실패했다. 인수 조건: 네 client에서 실행 중인 transaction의 호출이 첫 감사 대상 write 전에 id를 정한다. 그 전의 감사 대상 write는 CONFIG다. 중첩 transaction에서도 다른 id를 정하면 문서에 정한 오류다. MySQL, PostgreSQL, SQLite owner case를 갖춘다.—증거: Go db.Utils().SetOperation(id) error, PHP $db->utils()->setOperation(int|string $id): void, TypeScript db.utils().setOperation(id: OperationId): void, Rust db.utils().set_operation(id)?가 연결의 활성 transaction에 operation id를 정한다. 활성 transaction 밖에서는 CONFIG로 실패하고, 다른 type의 id(Go: int64나 string이 아닌 값, TypeScript: safe integer나 빈 문자열이 아닌 문자열이 아닌 값)는 CONFIG다. transaction이 option이나 이전 호출로 이미 가진 id는 아무것도 바꾸지 않고, 다른 id는 중첩 transaction 안에서도 CONFIG the transaction already has the operation id <old>; it cannot change to <new>다. id는 transaction에 속한다. 중첩 transaction은 그 id를 쓰고, rollback한 savepoint(callback 오류, Go runtime.Goexit, Go와 Rust panic, Rust transaction_once)는 그 아래에서 쓴 행도 되돌아가므로 시작할 때의 id로 되돌린다. release한 savepoint는 id를 유지한다. Rust는 id를 transaction의 mutex에 두고 statement마다 복사해 쓴다. Red: Go go vet는 "db.Utils().SetOperation undefined (type *orm.Utils has no field or method SetOperation)", PHP는 "Call to undefined method Orm\Utils::setOperation()", TypeScript는 "setOperation outside a transaction"과 "TypeError: db.utils(...).setOperation is not a function", Rust는 "no method named set_operation found for struct Utils<'a>"로 실패했다. Green: 공유 scenario(transaction 밖 CONFIG, 호출 전 감사 대상 insert CONFIG, id 7 설정, 같은 id 재설정은 변화 없음, id 8과 중첩 id 9는 CONFIG, 10을 정하고 실패한 중첩 transaction 뒤의 감사 대상 update는 CONFIG, 그 뒤 id 11로 씀, option 12인 transaction은 12를 받고 13을 거부, 이력은 7, 7, 11, 12)가 Go TestSetOperationInTransaction, PHP runtime_db_test, TypeScript dbspec_runtime_db, Rust set_operation_in_transaction으로 SQLite, MySQL, PostgreSQL case database에서 통과하고, audit_triggers의 coverage case audit_operation_set_in_transaction으로 네 client에서 세 database에 두 번 통과한다. Rust case는 중첩 transaction_once의 rollback도 확인한다. transaction_once의 Send 확인을 포함한 orm library test, rollback test, Utils method에 setOperation을 더하고 symbol을 기록한 interface-check, feature 검사, docs-check, docs-rules-check, gofmt, rustfmt, Rust clippy 변형, version-check가 통과한다. docs/operations/usage.md, docs/spec/interfaces.md, docs/spec/dbspec.md(+ko)가 호출과 규칙을 적는다. T35로 대체됨: handle의 audit 기본값과 transaction 값으로 삽입하는 audit 기록이 operation id를 대신하므로 setOperation과 operation id를 제거했다.[o]
T34driver type 없는 TypeScript 선언을 배포한다. 원인: @polyspec/orm-typescript 0.0.2가 배포한 dist/dbspec/apply.d.ts와 introspect.d.ts는 pg(type은 devDependency뿐), mysql2/promise, node:sqlite에서 type을 import했으므로, package를 import하는 code의 skipLibCheck 없는 type 검사가 실패했다(pg가 있고 @types/pg가 없으면 TS7016, driver가 없으면 TS2307). MySQL이나 SQLite driver만 설치해도 그렇다. 인수 조건: 공개 선언은 driver module을 참조하지 않고, 필요한 connection type은 client 자신의 type이며, skipLibCheck를 끄고 pg type 없이 pack한 package를 import하는 code를 type 검사하는 test가 있다(Red 먼저). mysql2와 node:sqlite도 같은 방법으로 확인한다.—증거: clients/typescript/src/dbspec/connections.ts가 apply와 introspection이 호출하는 method만으로 DbspecMySqlConnection, DbspecPostgresConnection, DbspecSqliteConnection(과 DbspecMySqlQueryResult, DbspecSqliteStatement)을 선언한다. apply connection type은 그 별칭이고, apply는 MySQL과 PostgreSQL statement를 query의 object 형식으로 실행하며, runtime pool 확인은 driver import를 유지하지만 그 import는 선언에 나오지 않는다. tests/typecheck.ts는 mysql2 connection, pool connection, pool, pg client, pool client, pool, node:sqlite DatabaseSync가 이 type에 맞기를 요구한다. clients/typescript/tests/package-declarations.mjs(npm run typescript:test에 포함)는 pg, mysql2, node:sqlite를 import하는 선언 file을 거부하고, npm pack으로 package를 만들어 ordered-json, @types/node와 함께, pg, @types/pg, mysql2 없이 설치한 뒤 skipLibCheck: false로 tsc를 실행한다. Red: "declaration files that import a driver module: dist/dbspec/apply.d.ts, dist/dbspec/introspect.d.ts"와 apply.d.ts, introspect.d.ts의 "error TS2307: Cannot find module 'mysql2/promise' …", "Cannot find module 'pg' …". Green: 두 case가 통과하고, npm run typescript:check, dbspec-introspect.mjs(63), dbspec-apply-physical.mjs(122), dbspec-apply-cleanup.mjs(3)가 통과하며, package-check, 기록한 TypeScript symbol로 interface-check, feature 검사, docs-check, docs-rules-check가 통과한다. docs/spec/plans.md(+ko)가 type을 적는다.[o]
T35감사 대상 작업 단위를 선언한 audit table의 row로 기록한다 (T31과 T33의 operation id를 대신한다). 원인: 감사 대상 write는 어느 table도 정의하지 않은 operation id만 실었으므로 그 id가 기록된 작업 단위의 id라는 보장이 없었고, "operation"이라는 말은 setting이 적는 audit 기록 table과 달랐다. 인수 조건: setting이 audit column과 audit 기록 table을 정하고, 그 column은 세 database에서 기록의 primary key로 가는 실제 restrict foreign key이며 introspection이 되살린다. 연결 handle이 audit 기본값을 갖고 transaction이 그 변경의 값을 받으며, client는 기록 하나를 삽입하고 insert, update, soft delete, restore하는 모든 감사 대상 row에 그 key를 쓴다. 중첩 audit과 audit 없는 감사 대상 write는 CONFIG다. "operation"은 dbspec, 문서, 네 client API에서 사라진다. vector, fixture, 네 client와 세 database의 coverage case를 갖춘다.—증거: audit into <history> column <col> references <table> action <col> previous <col> [exclude (...) | include (...)]가 네 parser, emitter, renderer, 비교, trigger introspection(Go engine/dbspec, PHP Dbspec, TypeScript src/dbspec, Rust orm-schema)에서 operation 형식을 대신한다. validator는 non-null audit column, 감사 대상이나 이력 table이 아니고 자신은 감사하지 않는 set의 기록 table, column type과 같은 column 하나의 primary key, 선언한 foreign key (<col>) references <table> (<key>) on delete restrict on update restrict, audit column type의 previous column을 요구한다. introspection은 audit column의 유일한 foreign key에서 기록 table을 읽는다. tests/dbspec/cases.json, ddl.json, plans.json은 audit table, foreign key와 그 index를 가진 새 형식을 쓰고(behavior step은 audit row를 먼저 넣으며 audit_seq 99의 raw insert는 실패한다), 무효 vector 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, audit-column-nullable을 더한다. fixture audit.dbs, audit_columns.dbs, restore.dbs, add_tables_and_columns/*.dbs도 따른다. runtime model은 audit column과 기록 entity를 갖고, planner는 기록 entity를 가진 slot audit을 bind하며, executor는 transaction의 기록 key를 쓰거나 CONFIG("a write of an audited table needs an audit: run it in a transaction with audit values" 또는 다른 기록 entity)로 실패한다. API: Go db.Audit(defaults) *DB와 orm.Audit(map[string]any), PHP $db->audit(Model $defaults): Db와 transaction(..., ?array $audit = null), TypeScript db.audit(defaults): Db와 TransactionOptions.audit, Rust Db::audit(defaults) -> Db와 transaction, transaction_send, transaction_once의 .audit(pairs)다. audit(defaults)는 같은 연결의 handle을 돌려주며 연결은 바꾸지 않는다. transaction은 시작 전에 값을 검사하고(기본값이 없거나 기록 table의 column이 아닌 값은 CONFIG), 시도마다 callback 전에 기본값과 값을 합쳐(값이 이긴다) 삽입하며, 중첩 transaction은 바깥 audit을 유지한다(그곳의 audit은 CONFIG). operation id, 그 option, Go SetOperation, PHP와 TypeScript setOperation, Rust set_operation과 OperationId는 제거했다. Red: 네 dbspec suite가 새 vector에서 실패했다(Go rule case의 unexpected 'operation', expected 'column', PHP "expected operation", TypeScript dbspec-ts-check의 실패 34개, Rust "'column' is not allowed here"). Go owner test는 compile에 실패했고(API 변경 뒤 orm.Operation undefined, 이어서 cannot use auditOf(...) as map[string]any), PHP는 "Unknown named parameter $audit", TypeScript는 "db.audit is not a function", Rust는 "no method named audit found for struct orm::Transaction"로 실패했다. Green: owner case auditCase(기록 [[1 create] [2 default]], 이력 audit 1, 1, 2, audit_seq 999의 raw insert 거부, 실패한 callback은 기록을 남기지 않음, 기본값 없음과 모르는 값은 CONFIG)가 Go TestAuditRecordTransaction, PHP runtime_db_test, TypeScript dbspec_runtime_db, Rust audit_record_transaction으로 SQLite, MySQL, PostgreSQL case database에서 restore와 add-tables-and-columns case와 함께 통과한다. coverage case audit_history, audit_transaction_entry_points, audit_selected_columns, soft_delete_restore가 네 client에서 세 database에 두 번 통과한다. dbspec-go-check, dbspec-ddl-check(foreign key behavior step 포함), dbspec-compare-check, dbspec-introspect-compare-check와 PHP, TypeScript, Rust의 dbspec, introspection, plan, apply 검사가 통과한다. Db method의 audit, 새 PHP transaction signature, 기록한 symbol로 interface-check가 통과하고, feature 검사, docs-check, docs-rules-check, go-model-check, ts-model-check, gofmt, rustfmt, Rust clippy 변형, version-check가 통과한다. docs/spec/dbspec.md, usage.md(Audited writes), protocol.md, interfaces.md, interface-implementation.md, dialects.md, plans.md(+ko)와 Rust README pair가 설계를 적는다. T35.2로 일부 바뀜: audit 값은 연결 설정의 audit source에서 오고, audit(defaults)와 그 handle은 제거했다.[o]
T35.1문서 집합이 다른 집합의 table을 소유하지 않고 쓰게 한다 (T35의 일부). 원인: 집합은 그 문서들이 쓰는 모든 문서를 담아야 했고, 집합의 모든 문서는 동등했다: manifest, 렌더링한 statement, install, addTablesAndColumns, 코드 생성이 모두를 다뤘다. 그래서 감사 대상 table이 core 집합의 audit table을 참조하는 module 집합은 core 문서를 나열해 audit을 소유하거나(core 집합 뒤에 설치하면 일부 table만 있어 CONFIG로 실패하고 생성 코드가 core model을 반복했다), 빼서 use diagnostic으로 실패했다. 인수 조건: 집합은 소유한 문서와 따로 주는 외부 문서다. 외부 문서는 parse하고 검사하지만 렌더링, 설치, 변경, 비교, 생성하지 않는다. manifest hash는 소유한 문서와 쓰는 외부 table의 정의를 포함한다. install, addTablesAndColumns, 연결은 쓰는 외부 table을 확인하고 CONFIG로 실패한다. 전부 아니면 전무 규칙, 비교, plan은 소유한 table만 다룬다. generator는 외부 문서를 받는다. 공유 vector와 네 client, 세 database의 owner case를 갖춘다.—증거: dbspec 문서는 external 표시를 갖는다(Go Document.External, PHP $external, TypeScript external, Rust external). 어느 소유 문서도 use로 닿지 않는 외부 문서는 그 header에서 use 오류다. manifest text는 소유한 문서를, 새 external text는 외부 문서마다 쓰는 table을 column, primary key, unique key와 함께 담는다. manifestHash는 둘을 포함하므로 외부 문서의 쓰지 않는 table이 바뀌어도 그대로다. schema text는 소유한 table과 외부 문서마다 use 줄 하나를 담는다. render는 외부 문서를 건너뛰고, schema text 확인에는 외부 문서가 필요 없다. plan apply는 database를 하나의 schema로 보므로 target에 use 줄이 있는 plan은 plan 오류(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)이고, addTablesAndColumns는 plan text를 parse하지 않고 plan을 만든다. ExternalDifferences(PHP externalDifferences, TypeScript externalDifferences, Rust external_differences)는 없는 table, 없는 column, 다른 type이나 nullability, 다른 primary key, 없는 unique key를 적는다. install, addTablesAndColumns, 생성한 schema 값으로 하는 연결(Go ConnectSchema, PHP Orm::connectSchema, TypeScript Db.connectSchema, Rust Db::connect_schema)은 집합에 외부 문서가 있을 때만 database를 introspect하고 어떤 statement보다 먼저 CONFIG "the tables that the set uses from external documents differ from the database: ..."로 실패한다. runtime model은 manifest text를 external text와 함께 읽고 외부 table의 entity를 갖지 않는다. 생성한 schema 값은 external text를 담으며(Go orm.Schema.External와 ExternalText, PHP EXTERNAL_TEXT, TypeScript externalText, Rust Schema::with_external와 orm_external.dbs) 비어 있지 않을 때만 쓰므로 기존 생성 코드는 그대로다. generator: Go와 PHP orm-gen gen --use <file.dbs>, TypeScript orm-gen gen --use <file.dbs>, Rust Builder::uses. Red: Go go vet는 "document.External undefined (type *Document has no field or method External)"와 "undefined: runtimemodel.LoadFileSet", PHP는 "Undefined property: Orm\Dbspec\Manifest::$externalText", "FAIL plan/parse/target-uses-external-document", "flag provided but not defined: -use", TypeScript make dbspec-ts-check는 "FAIL plan parse target-uses-external-document"와 "FAIL sets external-document-unused", owner case는 "SCHEMA_INVALID ... document ext_core is not in the declared document set", generator는 "Unknown option '--use'", Rust는 "error[E0609]: no field external on type Document", "error[E0599]: no associated function or constant named with_external found for struct Schema", "no method named uses found for struct orm_build::Builder"로 실패했다. Green: tests/dbspec/cases.json은 text, hash, 만드는 table을 가진 set external-document-used, external-document-unrelated-table-changed(같은 manifestHash), external-document-used-table-changed(다른 hash), external-document-unused를, tests/dbspec/plans.json은 parse case target-uses-external-document를 더한다. dbspec-go-check, dbspec-php-check, dbspec-ts-check, dbspec-rust-check, dbspec-compare-check가 통과한다. contracts/fixtures/external의 owner case(Go TestExternalDocuments, schema_set_test와 schema-set의 PHP와 TypeScript external_documents, Rust external_documents)가 SQLite, MySQL, PostgreSQL case database에서 통과한다: core 집합 전에는 모든 호출이 ext_account와 ext_audit을 적은 CONFIG이고 아무것도 만들지 않는다. member install은 ext_post와 ext_post_history만 두 번 만든다. 감사 대상 create는 source의 actor를 core의 ext_audit에 기록한다. member_v2는 정확히 ext_post.summary와 ext_post_history.summary를 더한다. 바뀐 외부 정의는 ext_account.nick을 적은 CONFIG다. schema_install의 coverage case schema_install_external_documents가 네 client에서 세 bench database에 통과하고, generator test는 소유한 model만 만든다. external text field와 기록한 symbol로 interface-check, feature 검사, go-model-check, ts-model-check, TypeScript type 검사와 선언, gofmt, rustfmt, Rust clippy 변형, docs-check, docs-rules-check, version-check가 통과한다. docs/spec/dbspec.md(External documents), plans.md, schema.md, usage.md(+ko)가 설계를 적는다.[o]
T35.2audit 값을 연결의 source에서 받는다 (T35의 일부). 원인: T35는 audit 기본값을 연결 handle(audit(defaults))에 두었으므로 요청마다 handle을 만들어 쓰는 code에 넘겨야 했고, 기록 table은 그것을 정하는 audit setting이 아니라 handle의 model에서 왔다. 인수 조건: 네 client에서 audit(defaults)와 그 handle을 제거한다. 연결 설정이 process당 한 번 현재 요청의 audit 값을 돌려주는 audit source를 받고, transaction은 그 변경의 값을 받는다. 기록 table은 references가 정하는 table이다. source가 없음, 기록 table의 column이 아닌 값, 값이 없는 기록은 transaction 시작 전에 CONFIG이고, source의 오류는 그대로 transaction을 실패시킨다. MySQL, PostgreSQL, SQLite owner case와 coverage case를 갖춘다.—증거: Go Config.AuditSource func(ctx context.Context) (map[string]any, error)는 transaction의 context로 불린다. PHP new Config(..., auditSource: \Closure), TypeScript connect({ ..., auditSource: () => Record<string, unknown> }), Rust AuditSource = Arc<dyn Fn() -> Result<Vec<(String, Param)>> + Send + Sync>인 Config.audit_source: Option<AuditSource>다. 호출별 orm.Audit(map), PHP audit:, TypeScript { audit }, Rust .audit(pairs)는 남는다. transaction은 BEGIN 전에 등록한 set의 감사 대상 table이 쓰는 기록 table 하나를 찾고(없음, 둘 이상, 등록한 set 밖, column 하나짜리 primary key가 없음은 CONFIG), source를 한 번 부르고, source 값과 호출 값을 합치며(호출 값이 이긴다), column과 값이 있는지 확인한다. 시도마다 callback 전에 기록을 삽입한다. runtime model과 planner slot audit은 기록 table 이름을 갖고, 감사 대상 write는 이를 transaction의 기록과 비교한다. Red: 바뀐 test가 이전 code에서 실패했다. Go go vet는 "unknown field AuditSource in struct literal of type orm.Config", PHP runtime_db_test는 case 12개에서 "Unknown named parameter $auditSource", TypeScript dbspec_runtime_db는 "an audit transaction whose source fails returns the source error"와 "CONFIG: the transaction has audit values but the connection has no audit defaults: set them with db.audit(defaults)", Rust는 "error[E0425]: cannot find type AuditSource in crate orm"로 실패했다. Green: owner case auditCase(source 없음 CONFIG, column이 아닌 source 값 CONFIG, 실패하는 source는 그 오류를 돌려줌, 호출 값이 source를 이김, 기록 [(1, create), (2, default)])가 Go TestAuditRecordTransaction, PHP runtime_db_test, TypeScript dbspec_runtime_db, Rust audit_record_transaction으로 SQLite, MySQL, PostgreSQL case database에서 restore와 add-tables-and-columns case와 함께 통과한다. coverage case audit_history, audit_transaction_entry_points, audit_selected_columns, soft_delete_restore가 네 client에서 세 database에 통과한다. Go orm test, test-faults와 live-db를 켠 Rust workspace test, Send를 유지하는 transaction_once와 transaction_send future, audit 없는 Db method와 기록한 symbol로 interface-check, audit_triggers feature 검사, 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, Rust clippy 변형이 통과한다. docs/operations/usage.md(Audited writes), config.md, interfaces.md, interface-implementation.md, protocol.md, dbspec.md(+ko)와 Rust README pair가 source를 적는다.[o]
T35.3schema definition fixture의 audit 렌더링을 T35가 선언한 대로 렌더링한다. 원인: T35가 contracts/fixtures/audit.dbs를 audit 기록 table로 바꿨지만 contracts/fixtures/schema_definition.json의 dbspec_render_ddl case는 예전 fixture의 statement(operation_id, ix_item_operation, audit table과 foreign key 없음)를 유지했으므로, make feature-check에서 네 client의 schema_definition coverage가 실패했다.—증거: 기대하는 MySQL, PostgreSQL, SQLite statement는 현재 audit.dbs의 Go dbspec.Render 결과이며 audit table, fk_item_audit, audit_seq trigger를 갖는다. Red: make feature-check가 Go, PHP, Rust, TypeScript의 schema_definition에서 실패했다. Rust coverage_dbspec_render_ddl은 기대 쪽에 operation_id가 있는 "assertion left == right failed: mysql"로 실패했다. Green: node scripts/features/coverage.mjs --feature schema_definition이 네 client의 coverage를 오류 없이 실행한다.[o]
T35.4PHP에서 schema 값으로 등록한 set의 audit 기록을 plan한다. 원인: PHP는 등록한 모든 set의 runtime model을 생성 class에서 만들었으므로(Registry::model), module set을 생성 class 전에 또는 없이 schema 값으로 등록한(utils()->schema()->install($schema)) 연결에서는 모든 audit transaction이 Db::auditRecord에서 SCHEMA_HASH_MISMATCH: no loaded generated models registered manifest ...로 실패했다. Go, Rust, TypeScript는 manifest text에서 runtime model을 만든다. 인수 조건: 네 client에서 schema 값으로 등록한 set은 manifest text와 external text에서 runtime model을 얻고, audit 기록 table은 그것을 소유한 set에서 찾는다. MySQL, PostgreSQL, SQLite에서 그 등록 순서의 Red test를 갖춘다.—증거: PHP Registry::schema(Schema)는 값으로 등록한 set의 text를 기록하고(Db::registerSet이 부른다), Registry::model은 set의 bootstrap이 실행되었으면 생성 class를, 아니면 RuntimeModel::loadSet으로 한 번 만든 model을 쓴다. Registry::generated는 그 전에 만든 model을 버린다. 외부 table은 entity가 아니므로 기록 table은 소유한 set에서만 온다. Go(runtimemodel.LoadSet을 쓰는 Schema.Model)와 Rust(Manifest::load_set을 쓰는 Schema::manifest, tx.rs의 audit_record)는 이미 등록한 모든 set을 text에서 만든다. 두 client의 외부 문서 owner case는 이제 module set contracts/fixtures/external/board.dbs도 값으로 설치하며 그대로 통과한다. Red: clients/php/tests/audit_external_sets_test.php(core와 board를 값으로 등록하고, member를 그 뒤에 생성하거나 bootstrap 전에 값으로 등록한 뒤 감사 대상 create)가 여섯 case에서 "SCHEMA_HASH_MISMATCH: no loaded generated models registered manifest sha256:0f94ef9772a2f21531b12cbe7e5563ba5a830d4e7ae91d9f2fd4b9a76fc59ffe"로 실패했다. Green: 여섯 case가 SQLite, MySQL, PostgreSQL에서 source의 actor를 가진 ext_audit 기록 하나와 ext_post_history의 그 key로 통과한다. Go TestExternalDocuments와 Rust external_documents가 세 database에서 통과한다. PHP runtime_db_test, schema_set_test, add_tables_and_columns_test(각 12), model_test(66), dbspec-php-check(556)가 통과한다. 변경에 대한 make owner-check는 schema_install, audit_triggers, interface_contract를 골라 여섯 단계를 통과했다.[o]
T35.5Go 외부 문서 owner case에서 실패하는 query 없이 table이 있는지 확인한다. 원인: Go case TestExternalDocuments는 native 연결로 table에서 SELECT COUNT(*)를 읽어 있는지 확인했다. pool_mode = transaction인 PgBouncer를 거치면 아직 없는 ext_post를 읽는 실패가 driver가 이름을 준 prepared statement를 server session에 남기므로, member install 뒤의 읽기가 "FATAL: prepared statement name is already in use (SQLSTATE 08P01)"로 실패했고 case는 ext_post가 없다고 보고했다.—증거: case는 catalog(current_schema()의 information_schema.tables, MySQL DATABASE()의 information_schema.TABLES, SQLite sqlite_master)를 묻는다. 이 질의는 없는 table에서도 실패하지 않는다. Red: make check가 client-pooler-check에서 postgres의 "external_documents_test.go:141: ext_post does not exist after the member install"로 실패했다. Green: make client-pooler-check가 네 client에서 통과하고, make owner-check는 schema_install을 골라 검증과 coverage를 통과했다.[o]
T36owner 검사를 바뀐 file에서 고른다. 원인: owner 검사는 동작이 바뀐 기능에서 손으로 골랐으므로, 네 client의 schema_definition coverage가 실패하는 채로 T35와 T35.2가 commit되었다. contracts/fixtures/schema_definition.json은 T35가 바꾼 contracts/fixtures/audit.dbs를 렌더링하지만 둘을 잇는 명령이 없었다. 인수 조건: 명령 하나가 선언한 입력에 바뀐 file이 있는 모든 기능을, 바뀐 file을 적는 fixture까지 포함해 나열하고 실행하며, 단계마다 RUN, PASS나 FAIL, 경과 시간, 기한을 갖는다. 바뀐 audit.dbs는 schema_definition을 고른다. AGENTS.md가 실행 방법을 적는다.—증거: scripts/features/owners.mjs(make owner-check, PATHS="<paths>" 또는 HEAD와 다른 file과 추적하지 않는 file)는 fixtures나 tests에 바뀐 file이 있는 기능과 선언한 fixture file이 그 file을 적는 기능을 고르고, 각 기능을 고른 path를 출력하며, 기능마다 검증 명령(scripts/features/check.mjs --run --feature <id>)과 coverage(scripts/features/coverage.mjs --feature <id>)를 그 단계의 process group을 끝내는 10분 기한을 가진 단계로 실행한다. --list는 출력만 한다. scripts/features/owners.test.mjs는 make feature-check에서 실행한다. AGENTS.md와 docs/spec/feature-coverage.md(+ko)가 규칙을 적는다. Red: 선언한 fixture와 test만 보는 선택에 대해 node --test scripts/features/owners.test.mjs가 "FAIL a changed fixture selects every feature whose fixtures render it elapsed=3ms: selected audit_triggers"로 실패했다. Green: 세 case가 통과한다(audit.dbs는 schema_definition.json이 적으므로 schema_definition과 audit_triggers를 고르고, test file과 tests/dbspec/cases.json은 그 기능을 고르며, README.md와 docs/checklist.md는 아무것도 고르지 않는다). T37 변경에 대한 make owner-check는 model_queries와 interface_contract를 골라 네 단계(검증 명령 23개와 2개, 각 coverage 실행)를 통과했다. docs-check와 docs-rules-check가 통과한다.[o]
T36.1owner 검사를 고를 때 적힌 JSON file이 적는 file까지 따라간다. 원인: T36은 선언한 fixture 하나가 바뀐 file을 적을 때만 기능을 골랐다. interface_contract는 contracts/symbols/rust.json을 적는 contracts/interfaces.json을 선언하고, 그 snapshot은 모든 Rust source file을 적으므로, clients/rust/orm/src/tx_send_tests.rs의 변경(T38)은 아무것도 고르지 않았고 바뀐 symbol은 make check에서만 interface-check를 실패시켰다("rust: changed clients/rust/orm/src/tx_send_tests.rs::state").—증거: selectOwners는 적힌 JSON file마다 따라가 그 file이 적는 file에 대해 기능을 고르며, 이유는 <path> (named by <json file>)다. 이제 모든 client의 source file은 interface_contract를 고른다. Red: 새 case "a file named by a file that a fixture names selects the feature"가 "Expected values to be strictly deep-equal"(고른 기능 없음)로 실패했다. Green: scripts/features/owners.test.mjs의 네 case가 통과하고, T38 변경에 대한 make owner-check가 interface_contract를 골라 검증과 coverage를 통과했다.[o]
T37utility의 internal transaction helper에 네 client에서 한 이름과 한 자리를 준다. 원인: utility statement를 연결의 활성 transaction이나 새 transaction에서 실행하는 helper는 Go Utils.run, Rust Utils::run, 따로 둔 internal class의 PHP UtilsSql::run, module 함수인 TypeScript inTx였고, 활성 transaction이나 연결로 읽는 helper는 Go Utils.read, Rust Utils::reader, PHP UtilsSql::scalar, TypeScript module read였다. 한 개념이 첫 commit부터 여러 이름과 자리를 가졌다. 인수 조건: 모든 client에서 Utils가 active(활성 transaction을 요구하며 없으면 CONFIG), run(활성 transaction이나 retry 없는 새 transaction), read(활성 transaction이나 연결)를 갖고, schema, privilege, AES utility는 자기가 가진 Utils로 이를 부른다. 동작은 바뀌지 않는다.—증거: Go는 Utils.active, run, read를 유지한다. PHP는 UtilsSql을 제거한다: Utils가 internal active, run, read(전 scalar), exec를 갖고 SchemaUtils, PrivilegeUtils, AesUtils는 (Db $db, Utils $u)를 받는다. TypeScript Utils는 internal active, run(전 inTx), read(전 module read), db를 갖고 세 utility는 u: Utils를 받는다. Rust는 Utils::reader를 read로 바꾸고 외부 table 확인을 read()로 introspect하는 Utils method로 둔다. transaction을 거부하는 확인(MySQL install, MySQL과 SQLite addTablesAndColumns)은 다른 개념이므로 남는다. Red: 동작 없는 이름 변경이라 없다. symbol을 기록하기 전까지 interface 검사가 바뀐 member를 보고했다(TypeScript inTx와 read 없음, Utils.run과 Utils.read 예상 밖). Green: PHP runtime_db_test, model_test, add_tables_and_columns_test, schema_set_test(각 12)가 SQLite, MySQL, PostgreSQL에서, aes_json_test, rollback_test, dbspec-php-check가 통과한다. TypeScript typescript:test(361), dbspec-ts-check(275), schema-set과 add-tables-and-columns(각 12), package-declarations가 통과한다. Rust orm lib test(35), add_tables_and_columns, external_documents, aes_json, bench schema의 integration program(세 database에서 privilege와 AES rotation을 포함한 30 case), rustfmt, 세 clippy 변형이 통과한다. 기록한 symbol로 interface-check가 통과한다.[o]
T38Rust send savepoint test에 transaction pooler가 섞지 않는 table을 준다. 원인: tx::send_tests(N9.2.1의 clients/rust/orm/src/tx_send_tests.rs)는 CREATE TEMPORARY TABLE orm_send_savepoint_probe에 쓰고 연결을 닫으면 그것이 사라진다고 보았다. pool_mode = transaction인 PgBouncer를 거치면 temporary table은 client 연결이 닫힌 뒤에도 server session에 남고 뒤의 실행이 그 session을 받을 수 있으므로, make check의 client-pooler-check가 schema pg_temp_52에서 postgres_send_savepoint "outer commit must succeed: ... duplicate key value violates unique constraint "orm_send_savepoint_probe_pkey" ... Key (id)=(1) already exists"로 실패했다. 이 test는 statement가 우연히 한 server session을 쓸 때만 통과했다.—증거: test는 실행마다 이름이 orm_send_savepoint_probe_<process id>_<nanoseconds>인 일반 table을 만들고 끝에서 지우므로, 모든 server session이 같은 table을 보고 다른 실행의 row를 만나지 않는다. Red: 위의 make check 실패. Green: make client-pooler-check가 mysql_send_savepoint, postgres_send_savepoint, sqlite_send_savepoint와 함께 네 client에서 통과하고, 세 send test가 직접 실행에서 통과하며, test-faults를 켠 orm의 rustfmt와 clippy가 통과한다. test의 바뀐 signature를 contracts/symbols/rust.json에 기록했고 interface-check가 통과한다. make owner-check(T36.1 포함)는 이 test file에 interface_contract를 골라 검증과 coverage를 통과한다.[o]
T39생성 모델이 scan path와 output path를 쓴 방식에 의존하지 않게 한다(네 client). 원인: 생성 모델은 document와 scan한 소스에만 의존해야 한다. Rust orm_build::Builder::try_generate는 상대 out_dir를 CARGO_MANIFEST_DIR에 이어 붙이고 manifest path를 쓴 그대로 include_str!에 넣었다. 그래서 같은 소스와 document가 out, 절대 path, ./out, ..를 거친 path마다 다른 generated source를 만들었다. TypeScript, Go, PHP 생성기에는 그런 의존이 없었다: TypeScript scan은 파일 내용만 읽고 output directory를 resolve하며, Go는 package pattern을 절대 파일 path로 load하고 --out을 resolve하고, PHP는 scan이 없고 path를 쓰지 않는다.—근거: Rust는 output directory를 만든 뒤 canonical path로 바꾸므로 include_str!은 한 directory에 한 path를 쓴다. 각 client는 fixture 하나(src의 소스 하나와 tests의 소스 하나, scan이 없는 PHP는 document)에서 scan, output, document path를 상대 path, 절대 path, ./를 넣은 path, 끝에 /를 붙인 path로 써서 생성하고, 출력이 byte 단위로 같으며 두 directory의 호출을 담는지 확인하는 case를 가진다: 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), PHP gen writes the same models however the paths are written(clients/php/tests/orm_gen_test.php). model_generation 검증은 이제 generate-scan.mjs와 orm_gen_test.php도 실행한다. docs/usage.md와 한국어 짝이 규칙을 적는다. Red: 새 Rust case가 "the generated source of the absolute paths differs from the relative paths"로 실패했다: include_str! path가 상대 path로 쓰면 <CARGO_MANIFEST_DIR>/../../…/out/orm_manifest.dbs, ./로 쓰면 <dir>/./out/orm_manifest.dbs였다. Go, TypeScript, PHP case는 변경 전에도 통과했다. Green: 바뀐 path의 make owner-check가 model_generation과 interface_contract를 골라 owners/model_generation/verification, owners/model_generation/coverage, owners/interface_contract/verification, owners/interface_contract/coverage를 통과하며, 새 case 네 개가 그 안에서 통과한다. Rust coverage_model_generation_check는 build한 model을 canonical 임시 path와 OUT_DIR path로 비교한다. orm-build의 clippy -D warnings, go vet ./generator, gofmt, rustfmt, docs-rules-check가 통과한다.[o]
T40데이터베이스 I/O 없이 schema set을 등록하고 install에서 데이터베이스를 확인한다(네 client). 원인: 요청마다 연 연결에는 schema set을 연결마다 등록해야 하는데, 열린 연결에 set을 등록하는 공개 호출은 install(schema)뿐이었고, 그 호출은 매번 transaction 안에서 hash를 확인하고 DDL을 렌더링하고 외부 table을 introspect하고 소유한 table마다 존재를 query했다(PHP 요청의 39%, PostgreSQL에서 statement 약 30개). connect helper(connectSchema)도 외부 문서를 쓰는 set이면 데이터베이스를 introspect했다. 또 install은 데이터베이스가 set과 같다는 것을 증명하지 않았다: table 이름과 외부 table만 확인하고 column, key, index, foreign key는 보지 않았다.—근거: utils().schema().register(schema)(Go Register, PHP register, TypeScript register, Rust register, Rust와 TypeScript에서 동기 호출)는 manifest hash를 확인하고 statement 없이 set을 등록하며, connectSchema도 같은 방법으로 등록한다. install은 데이터베이스를 한 번 introspect해 외부 table을 확인하고, 소유한 table이 하나도 없을 때만 렌더링한 statement를 적용하며(전부 또는 없음), 다시 introspect해 소유한 table이 set과 다르면 CONFIG the database differs from the document set: <kind> <table>[.<name>]; …로 실패한다. addTablesAndColumns도 같은 비교로 끝난다. 비교는 새 schema 작업 InstalledDifferences(Go engine/dbspec, PHP Dbspec::installedDifferences, TypeScript installedDifferences, Rust orm_schema::dbspec::installed_differences)이며 addTablesAndColumnsSteps의 소유 table 비교를 함께 쓴다. contracts/interfaces.json은 SchemaUtils.register를 동기 결과인 output Registered로 선언하고, contracts/validate.go는 statement를 실행하는 operation의 async 규칙에서 그것을 뺀다. docs/spec/schema.md(언제 무엇을 부르는지를 담은 새 6절 "Schema registration"), docs/spec/protocol.md, docs/spec/dbspec.md, docs/operations/usage.md, docs/interfaces.md와 한국어 짝이 규칙을 적는다: 데이터베이스는 요청마다가 아니라 install과 upgrade 때 확인한다. 공유 vector: contracts/fixtures/install/changed_database.json(contracts/fixtures/install/note.dbs의 case renamed_column, 데이터베이스마다 기대 message 하나). Red: Go TestInstallVerifiesTheDatabase, PHP와 TypeScript install_verifies_the_database, Rust install_verifies_the_database가 SQLite, MySQL, PostgreSQL에서 install over a changed column = <nil>(Rust Ok(()))로 실패했다. register test는 compile이나 호출에서 실패했고(Register undefined, Call to undefined method Orm\SchemaUtils::register(), register is not a function, no method named register), register 호출을 뺀 Go test는 "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: register test는 연결이 보내는 statement를 driver spy(Go RegisterDriver로 넣은 countingDriver, PHP StatementCountingPdo, TypeScript pool method 감싸기, Rust는 empty()가 남기는 것을 확인한 sqlx::query record의 logger)로 세어 register에서 0을 얻고, connectSchema는 connect와 같은 수의 statement를 쓴다. install test는 세 데이터베이스에서 기대 message를 얻는다. 바뀐 path의 make owner-check는 schema_install과 interface_contract를 골라 owners/schema_install/verification, owners/schema_install/coverage, owners/interface_contract/verification, owners/interface_contract/coverage를 통과한다.[o]
T41make owner-check가 변경이 건드리는 입력을 가진 make target을 실행하게 한다. 소유 검사는 fixture나 test가 바뀐 file인 기능만 골랐으므로 문서 변경은 아무것도 실행하지 않았고, T40 커밋은 make docs-check를 red로 남겼다. contracts/check-inputs.json은 CHECK_TARGETS의 모든 target에 대해 그 target이 읽는 repository path를 선언하고(*는 path 한 단계 안, **는 여러 단계), scripts/features/owners.mjs는 고른 기능보다 먼저, 입력이 바뀐 path를 맞추는 target마다 make <target>을 30분 기한과 RUN, STEP, PASS, FAIL 줄과 함께 실행한다. make repo-check와 scripts/features/owners.test.mjs는 CHECK_TARGETS의 target이 입력을 선언하지 않을 때, 항목이 그 밖의 target을 적을 때, pattern이 추적되는 file을 하나도 맞추지 않을 때 실패한다. AGENTS.md와 한국어 쌍은 이 규칙과 함께, 커밋 전에 바뀐 모든 file의 이름을 test와 script에서 찾는 일을 적는다.—증거: 변경 전 바뀐 checklist 쌍으로 make owner-check는 select 0 feature(s)를 보고하고 아무것도 실행하지 않았다. 이제 docs/checklist.md는 checklist-check, version-check, repo-check, docs-rules-check, docs-check, docs-verify-idempotent를 고른다. scripts/features/owners.test.mjs는 pattern case와 선언 검사를 포함해 7/7 통과하고, 이 항목의 바뀐 path로 make owner-check는 make target 일곱 개를 골라 통과한다.[o]
T41.1make owner-check가 고른 make target에 owner-check가 받은 서버 환경, decimal 환경, Rust target directory를 준다. scripts/features/owners.mjs는 각 target을 그냥 make <target>으로 시작했으므로, owner-check에 main checkout의 값을 주어도 worktree에서 target은 없는 worktree의 .runtime/servers/env와 .runtime/decimal-env를 읽었고, cargo는 worktree 자신의 clients/rust/target에 build했다. 이제 owner-check recipe가 ORM_OWNER_TEST_ENV와 ORM_OWNER_CARGO_TARGET_DIR를 넘기고, makeArguments는 각 target에 TEST_ENV=, DECIMAL_ENV=, CARGO_TARGET_DIR=를 make command line으로 준다. command line은 make 변수가 Makefile의 값을 바꾸는 유일한 방법이다. 그러면 worktree 검사는 main checkout의 서버와 target directory를 쓴다.—증거: .runtime이 없는 main의 새 worktree에서 main checkout의 TEST_ENV, DECIMAL_ENV, CARGO_TARGET_DIR를 준 make owner-check PATHS=clients/go/orm/cancel_test.go는 고른 target 12개 가운데 8개를 모두 <worktree>/.runtime/servers/env is missing; run make test-servers로 실패했고 worktree에 2.0 GB의 clients/rust/target를 남겼다. 변경 뒤 같은 명령은 target 12개와 transactions feature를 모두 통과하고 worktree에 clients/rust/target를 만들지 않는다. scripts/features/owners.test.mjs는 makeArguments를 포함해 8/8 통과한다.[o]
T42client가 보내는 모든 statement에 대해 event를 publish한다. Go, PHP, Rust, TypeScript가 같은 의미로 OnQuery hook을 대신한다(docs/operations/usage.md "Statement events"). 연결에 등록한 subscriber(Go DB.Subscribe, PHP Db::subscribe, Rust Db::subscribe와 Subscription::unsubscribe, TypeScript Db.subscribe)는 statement 문장, 가린 bind, 출처에 따른 kind(select, insert, update, delete, begin, commit, rollback, savepoint, release, rollback_to, schema, utility), statement가 가리키는 table, 경과 시간, 연결에서의 transaction 번호 또는 없음, 오류를 statement 뒤 operation이 이어지기 전에 동기로 받는다. 연결의 모든 handle이 subscriber를 공유한다. 실패한 subscriber는 나머지를 멈추고 그 오류를 cause로 가진 새 code SUBSCRIBER로 operation을 실패시킨다. 네 planner의 모든 plan step이 tables를 가지며 planner가 table 이름을 쓰는 곳에서 기록한다(contracts/fixtures/planner.json이 새 case planner_tables와 함께 적는다). renderer는 install statement마다 table을 주고, add step은 effect의 table을 가진다. 모든 client는 같은 transaction 제어 statement를 자기 SQL로 실행한다(MySQL START TRANSACTION, option을 붙인 PostgreSQL BEGIN 하나, SQLite BEGIN IMMEDIATE, 연결의 첫 transaction 전에 한 번 만드는 SQLite row lock table, COMMIT, ROLLBACK, savepoint). 그래서 Go client는 database/sql transaction을 쓰지 않고, PHP와 TypeScript는 driver의 transaction 호출을 쓰지 않는다. 취소된 MySQL이나 PostgreSQL transaction은 session을 닫고 취소된 SQLite transaction은 rollback한다. table 권한 다섯은 모든 client에서 statement 하나로 읽는다. conformance 실행기는 모든 statement를 그 event로 기록하고 transaction 번호는 vector 안에서 다시 매긴다.—증거: Red: 각 client의 구현 전에 vector test가 모든 case에서 실패했다(db.subscribe is not a function, Call to undefined method Orm\Db::subscribe(), Rust no method named subscribe). 이전 TypeScript planner는 planner case에서 tables: undefined로 실패했다. Green: 공유 vector tests/events/vectors.json(fixture contracts/fixtures/statement_events.dbs, case 여덟 개: model statement, commit한 savepoint, rollback한 transaction, rollback한 savepoint, 데이터베이스가 DUPLICATE_KEY로 거부한 statement, 실패하는 subscriber, 구독 해제, table을 가리키는 install의 statement)가 Go(TestStatementEvents*), PHP(statement_events_test.php), Rust(--test statement_events), TypeScript(statement-events.mjs) 각각에서 MySQL, PostgreSQL, SQLite로 8/8 통과한다. planner case는 각 client에서 6/6 통과한다. go run ./tests/conformance/check run에서 네 실행기가 데이터베이스마다 vector 26개 모두에 같은 출력을 냈고, 그 출력으로 기대값을 기록했으며, 그 model statement, bind, 결과는 이전 기대값과 같다. interface check가 네 언어에서 통과한다. 검사: coverage case statement_events(feature-check가 고른 database에서 vector를 실행하는 각 client의 실행부)가 Go, PHP, Rust, TypeScript에서 데이터베이스마다 두 번 통과한다. Go coverage case는 처음에 subtest 여덟 개를 따로 된 test로 보고했고, 이제 TestCoverageStatementEvents 안에서 실행하며 MySQL, PostgreSQL, SQLite에서 실행마다 정확히 한 번 통과한다. 넓은 검사는 항목마다 실행하지 않고 마지막 전체 suite에서 실행한다.[o]
T7.17.2.10.2펜스 예제를 해석하지 않고 물리 마크다운 블록 위치를 확인한다(T7.17.2.10.1 선행). 우선순위: 실행 가능한 문서 파싱의 첫 단계. 원인: 주석 문자열 검색만으로 예제를 실제 스키마로 오인하며 주변 원문을 보존할 수 없다.—증거: Go/PHP/Rust/TypeScript의 빠진 스캐너 Red가 Green이다. physical-envelope-check가 소유별 공통 범위·진단 21개, 소스·줄·블록 제한·초과 6개와 네이티브 인코딩을 두 번 실행했다. 모든 원문 조각이 앞뒤·본문 UTF-8과 CRLF 바이트를 정확히 보존한다. PHP는 기존 128M 제한에서 64 MiB 소스를 받으며 최대 할당량은 71319552바이트다. RSS가 아니다. macOS arm64에서 Go vet, Rust 1.98.1 범위 내 엄격한 Clippy·포맷, TypeScript 컴파일, PHP 문법과 문서 쌍·diff 검사가 통과했다. 어휘 스캐너이며 마크다운 AST·그림 공동 검증·임포트 기능 결과는 아니다.[o]
T7.17.2.10.2.1소유 시작 펜스와 들여쓴 목록 예제를 구분한다. 원인: 공백 0–3개를 허용하는 시작 규칙이 목록 안의 예제를 실제 스키마로 인식한다.—증거: 네 스캐너 모두 list-example이 수정 전에 실패하고 소유 시작의 들여쓰기를 금지한 뒤 두 번 통과했다. 다른 펜스 예제는 해석하지 않는다.[o]
T7.17.2.10.2.2표현 검사기가 거절한 새 소스 블록 설명을 고친다. 원인: 새 영문 제목과 작업 설명이 금지 단어를 사용했다.—증거: 기술적 기준이나 검사기 규칙을 바꾸지 않고 같은 문서 쌍 검사를 통과했다.[o]
T7.17.2.10.3물리 주석과 그림을 함께 파싱한다(T7.17.2.10.2 선행). 우선순위: 다음 물리 문서 단계. 원인: 원문 범위만으로 그래프 메타데이터·표시 이스케이프·모순된 참조를 검증할 수 없다. 완료 기준: 전체 문법을 명시하고 모든 그래프 필드·주변 마크다운을 보존하며 안전한 위치로 모순을 거절한다. 기능 활성화 전에 네 클라이언트의 연결 부하와 실제 그림 파싱·렌더링을 검증한다. 원인: T8이 Mermaid physical 문서를 dbspec으로 대체하므로 주석이 달린 Markdown 경로를 이어가지 않는다. 재시도: T8.1이 physical Markdown 문서를 유지하기로 판정하면.—Evidence: dbspec으로 대체됐다. 이 항목이 parse하려던 annotated Markdown physical-document 경로는 더 없다. T8.2.6.4가 Mermaid source와 함께 지웠고, dbspec 문서가 table과 diagram을 함께 담는다(docs/spec/dbspec.md "Diagrams").[o]
T7.17.2.10.3.4전체 파서 호출로 문서 입력 자원 제한을 검증한다. 원인: 스캐너 검사만으로 보존한 문서의 할당량을 증명하지 못했고 PHP가 주변 본문을 복제해 허용한 64 MiB에서 기존 128M을 초과했다.—증거: 원문·범위를 copy-on-write로 유지하고 즉시 할당하는 prefix/suffix 필드를 제거해 관리되는 Red가 Green이다. physical-document-limits-check가 네 소유별 바이트·줄·블록 상한과 초과 6개를 두 번 실행하며 정확한 원문·안전한 루트 자원 오류를 유지한다. 제한은 64 MiB/200000줄/4096블록 그대로다. PHP 최대 할당량은 71319552바이트이며 RSS가 아니다. 관련 PHP 메타데이터·진단과 연결된 2000테이블/60000컬럼/10000FK 문서 왕복도 두 번 통과했다. 범위 내 vet·Clippy·컴파일·문법·문서 쌍 검사가 통과했다. 부분/출력의 명시적인 할당은 별도 메모리 예산이 필요하며 128M에서 완전한 편집 사본 두 개를 보존한다고 주장하지 않는다. 소유·남은 문법/출력 기준·임포트 활성화는 미완료다.[o]
T7.17.2.10.3.1권위 있는 임포트 활성화 전에 실제 물리 문서 소유를 검증한다. 원인: 어휘 원문 범위만으로 권위 있는 문서를 검증하지 못한다. 완료 기준: 명시한 물리 문법이 실제 블록을 구분하고 모호한/미지원 소유를 안전하게 거절하며 네 클라이언트에서 펜스 예제·본문을 보존한다. 기존 HTML 보호는 회귀 검증이며 일반 HTML 파서를 만들라는 요구사항이 아니다. 원문 범위만으로 임포트를 활성화하지 않는다.—Evidence: T7.17.2.10.3과 함께 대체됐다. 권위 있는 import는 이름과 선언을 가진 문서로 된 dbspec document set을 읽으므로 확인할 physical-document 소유가 남지 않는다.[o]
T7.17.2.10.3.1.1소스 스캐너에서 명시적 종료를 가진 HTML 블록을 보호한다. 원인: HTML 주석·원시 요소·처리 지시·선언·CDATA에 스키마 모양 펜스가 들어갈 수 있다.—증거: 네 기존 스캐너 모두 공통 주석 사례가 실패하고 HTML 내부를 해석하지 않는 구현 뒤 통과했다. physical-envelope-check가 Go/PHP/Rust/TypeScript 소유별 새 HTML 보존·진단 29개, 100000줄 주석·원시 요소 부하 2개, 기존 원문 21개, 제한·초과 6개와 네이티브 인코딩을 두 번 실행했다. 블록마다 종료를 한 번 검색하며 줄·본문 목록을 할당하지 않는다. macOS arm64에서 범위 내 Go vet, Rust 1.98.1 엄격한 Clippy·포맷, PHP 문법, TypeScript 컴파일, 문서 쌍·체크리스트와 diff 검사가 통과했다. 표현 검사기가 거절한 새 한글 표현을 규칙 약화 없이 고쳤다. 빈 줄로 끝나는 HTML·일반 컨테이너·그래프/그림 검증·임포트·네이티브/플랫폼 증거는 미완료다.[o]
T7.17.2.10.3.1.2이름 있는 블록 HTML을 빈 줄이나 EOF까지 보호한다. 원인: div/table 종료 태그로 마크다운 HTML 소유가 종료되지 않으므로 스캐너가 여전히 내부 펜스 예제를 활성화한다.—증거: 네 기존 스캐너 모두 이름 있는 태그 예제가 실패하고 제한된 ASCII 태그 비교와 빈 줄 상태 구현 뒤 통과했다. physical-envelope-check가 소유별 62개 이름의 시작·종료 형태(124개), 추가 원문·진단 23개, 100000줄 블록 사례를 두 번 실행했다. 명시적 HTML 29개·원문 21개·기존 긴 HTML 2개·제한/초과 6개와 네이티브 인코딩도 두 번 통과했다. 바이트를 보존하며 중첩 시작·종료 태그는 종료 규칙을 바꾸지 않고 원시 HTML은 계속 명시적 종료를 요구한다. macOS arm64에서 범위 내 Go vet, Rust 1.98.1 엄격한 Clippy·포맷, PHP 문법, TypeScript 컴파일, 문서 쌍·체크리스트와 diff 검사가 통과했다. 문단에 따라 시작이 달라지는 임의의 완전한 태그·일반 컨테이너·그래프/그림 공동 파싱·임포트·네이티브/플랫폼 검증은 미완료다.[o]
T7.17.2.10.3.2원문 스캐너의 범위를 적는다. 원인: docs/schema.md가 스캐너에 없는 태그·문단 커서를 서술했다.—증거: docs/spec/schema.md(+ko)는 스캐너가 임의의 HTML 태그나 전체 Markdown container를 분류하지 않으며, 문서 reader가 자기 원문 소유를 확정하고 graph metadata를 그림과 함께 검증한 뒤에 import를 켠다고 적는다. 문서 쌍·체크리스트와 diff 검사가 통과했고 실행 코드는 바뀌지 않았다. 다음 단계는 물리 메타데이터/그림 공동 파싱이다.[o]
T7.17.2.10.3.3네 클라이언트에서 가역 표시 이스케이프와 공동 문서 보존을 검증한다. 원인: 실제 Mermaid가 따옴표 엔티티 별칭의 역슬래시를 거절해 첫 표시 초안의 실제 파싱이 실패했다.—증거: 네 파서 누락 Red가 Green이다. 공통 사례가 모든 그래프 필드·CRLF 본문·병렬 FK·빈 그래프를 유지하고 변형 7개를 정확하고 안전한 줄/경로로 거절하며 표시 가역성을 검증한다. physical-document-check가 소유별 두 번 실행하고 Chromium의 특수 이름·병렬 FK 렌더링도 두 번 통과했다. 네 소유 모두 인덱스·키·CHECK 각 2000개를 가진 연결된 2000테이블/60000컬럼/10000FK 그래프를 문서 출력/파싱/재출력에서 유지했다. PHP 최대 할당량은 기존 128M에서 117030912바이트이며 RSS가 아니다. macOS arm64에서 범위 내 컴파일·lint와 문서 쌍 검사가 통과했다. 실험 단계 파서는 권위 있는 임포트를 활성화하지 않는다. 소유와 전체 자원/문법 기준은 상위 항목에 남아 있으며 네이티브 플랫폼 결과를 주장하지 않는다.[o]
T7.17.2.10.1물리 마크다운 문법 구현 전에 표시 제약을 확인한다(T7.17.2.9 선행). 우선순위: 손실 없는 주석 포함 문서에 필요하다. 원인: 논리 Mermaid 필드로 임의의 물리 문자열을 직접 표현하거나 SQL 카디널리티를 검증할 수 없다.—증거: 현재 논리 파서·물리 교환·물리 임포트 설계를 확인하고 공식 ER 문법을 검사했다. 양쪽 스키마 문서에 따옴표·ID·카디널리티·모순·일반 본문·제한된 소스 기준을 명시했다. 기존 논리 파싱은 이 기능을 제공하지 않는다. 문서 쌍·체크리스트·diff 검사가 통과했다. 필수 기준의 기록이며 구현된 문법·렌더러 증거·임포트 기능은 아니다.[o]
T7.17.2.9엄격한 물리 그래프 JSON을 읽고 출력한다(T7.17.2.8 선행). 우선순위: 주석 포함 마크다운의 필수 입력. 원인: 디코딩된 값 API로는 일반 JSON 디코더가 덮어쓴 중복 멤버를 찾을 수 없다.—증거: 네 클라이언트의 빠진 텍스트 API Red가 Green이다. physical-json-check가 클라이언트별 공통 문법·값 30개, 숫자 표기 6개, 네이티브 디코더의 바이트·깊이·노드 제한·초과 6개, 전체 레코드 왕복과 네이티브 인코딩을 두 번 검사했다. 추가 공통 출력 크기 2개도 클라이언트마다 두 번 통과했다. 인덱스·키·CHECK를 포함한 연결된 2000테이블/60000컬럼/10000FK 그래프의 모든 필드를 비교하며 14123977바이트 결과를 멱등하게 출력하고 관련 그래프·레코드 회귀도 두 번 통과했다. PHP의 동일 부하는 기존 128M 제한에서 최대 할당량 93290496바이트(89 MiB)이며 RSS가 아니다. macOS arm64에서 Go vet, Rust 1.98.1 엄격한 소유 Clippy·범위 내 포맷, Node 26.10.0 TypeScript 컴파일, PHP 문법, 문서 쌍·diff 검사가 통과했다. 마크다운·임포트·DDL·실행·네이티브·플랫폼 검증은 미완료 요구사항이다.[o]
T7.17.2.9.1PHP 텍스트를 최종 값 트리로 직접 디코딩한다. 원인: 전체 객체 디코딩과 고정 초기 등록 캐시가 연결된 부하 사례의 기존 128M 제한을 초과한다. 테이블 앞의 인덱스가 반복 컬럼 데이터보다 먼저 캐시를 채운다.—증거: 재현된 메모리 소진이 직접 트리 구성과 제한된 FIFO 짧은 문자열·작은 객체 공유로 Green이다. 원래 fixture·메모리 제한의 모든 필드 텍스트 왕복을 두 번 통과하며 객체·목록 구분, 이스케이프 중복, copy-on-write 분리를 검증했다. 256M 원인 측정 실행은 완료 증거로 사용하지 않았다.[o]
T7.17.2.9.2TypeScript Unicode 검증을 선언된 언어 범위에서 수행한다. 원인: 현재 ES2022 컴파일에 isWellFormed 타입 선언이 없다.—증거: 재현된 컴파일 실패가 직접 코드 포인트 검증으로 Green이다. 억제나 실행 대안 없이 원문·이스케이프의 짝 없는 surrogate 거절과 소유 컴파일을 통과했다.[o]
T7.17.2.9.3문서 검사기가 거절한 새 표현을 고친다. 원인: 새 문서 쌍의 기록이 금지 단어를 사용했다.—증거: 기술적 기준이나 검사기 규칙을 바꾸지 않고 재현된 docs-rules-check 실패가 Green이며 ID·상태 쌍도 통과했다.[o]
T7.17.2.8물리 그래프에서 인덱스·키·CHECK 참조를 검증한다(T7.17.2.4–7 후속). 우선순위: 권위 있는 물리 문서의 선행 조건. 원인: 독립 레코드만으로 소유, 전역 ID, 공유 제약 이름, 기반 인덱스 일치를 검증하지 못한다.—증거: 네 언어의 이전 형태 Red가 필수 배열·폴백 없는 구현으로 Green이다. physical-graph-check가 Go/PHP/Rust/TypeScript에서 새 공통 레코드·제한 사례 45개와 기존 그래프 28개·제한 5개를 두 번 실행하고 관련 레코드 회귀, 참조 분리·빈 배열 요소를 검증했다. 기존 시간 제한 아래에서 클라이언트마다 2000테이블/60000컬럼/10000FK와 인덱스·키·CHECK 각 2000개를 보존했다. PHP 최대 할당량은 기존 128M 제한에서 72 MiB이며 RSS가 아니다. macOS arm64에서 Rust 1.98.1 엄격한 소유 Clippy·범위 내 포맷, Go vet, Node 26.10.0 TypeScript 컴파일, PHP 문법, 문서 쌍·diff 검사가 통과했다. SQL 문법, 미지원 객체, 임포트, DDL, 실행, 네이티브·플랫폼 검증은 별도 요구사항이다.[o]
T7.17.2.7불변 물리 기본·고유 키 레코드를 보존한다(T7.17.2.6 후속). 우선순위: 전체 물리 그래프 구성의 선행 조건. 원인: 인덱스만으로 이름 있는 제약, 키 컬럼 순서, 지연 검사, 기반 인덱스 ID를 유지하지 못했다.—근거: 네 클라이언트의 빠진 API Red가 Green이다. physical-key-check가 Go/PHP/Rust/TypeScript에서 공통 정확한 필드·보존·거절 35개와 개수·UTF-8·전체 크기 제한 14개를 두 번 실행하고 값·참조 분리, 잘못된 인코딩, 빈 배열 요소를 검증했다. 기존 컬럼·CHECK·인덱스·FK 레코드 회귀도 클라이언트마다 두 번 통과했다. 시간 중첩 메타데이터를 보존하지만 방언·타입 검증을 한 것으로 처리하지 않는다. macOS arm64에서 Rust 1.98.1 엄격한 소유 Clippy·범위 내 포맷, Node 26.10.0 TypeScript 컴파일, PHP 문법, 문서 쌍 규칙, diff 검사를 통과했다. 그래프 참조 검증, 물리 임포트, DDL, 실행, DB 대조, 네이티브·다른 플랫폼 검증은 별도 요구사항이다.[o]
T7.17.2.6불변 물리 인덱스 레코드를 보존한다(T7.17.2.5 후속). 우선순위: 무손실 임포트와 전체 그래프 구성의 선행 조건. 원인: 물리 레코드가 순서 있는 식·컬럼 인덱스 항목, 포함 컬럼, 접두 길이, 정렬·NULL 순서, 부분 조건을 표현하지 못했다.—근거: 네 클라이언트의 빠진 API Red가 Green이다. physical-index-check가 Go/PHP/Rust/TypeScript에서 공통 필드·보존·거절 46개와 개수·UTF-8·전체 크기 제한 20개를 두 번 실행하고 중첩 참조 분리, 빈 배열 요소, 인코딩, 네이티브 수치 사례도 검증했다. 혼합 항목 순서와 반복 컬럼을 유지한다. 기존 CHECK 벡터 24개·제한 17개와 컬럼 25개도 클라이언트별 두 번 통과했다. macOS arm64에서 Rust 1.98.1 엄격한 소유 Clippy·범위 내 포맷, Node 26.10.0 TypeScript 컴파일, PHP 문법, 문서 쌍 규칙, diff 검사를 통과했다. 기본·고유 제약과 이를 뒷받침하는 인덱스를 구분한다. 그래프 참조 연결, SQL 파싱, 임포트·DDL·실행 활성화와 DB·다른 플랫폼 검증은 아니다.[o]
T7.17.2.5.1새 CHECK 문서 표현과 점으로 구분하는 하위 항목 ID를 고쳤다. 원인: docs-rules-check가 새 금지 표현들을 거절했고 checklist-check가 하이픈 ID를 거절했다.—근거: 두 소유 검사에서 거절을 재현했고 레코드 의미와 검사 규칙을 바꾸지 않고 통과했다.[o]
T7.17.2.5불변 물리 CHECK 레코드를 보존한다(T7.17.2.4 후속). 우선순위: 무손실 물리 임포트의 선행 조건. 원인: 물리 그래프가 CHECK 식과 강제/검증 상태를 표현하지 못한다.—근거: 네 클라이언트에서 빠진 API로 실패한 Red가 Green이다. physical-check-check가 Go/PHP/Rust/TypeScript에서 동일한 정확한 필드·거절 24개와 바이트·개수·빠진 필드 17개를 두 번 실행하고 인코딩·분리된 값 사례와 클라이언트별 기존 컬럼 25개 회귀를 검증했다. macOS arm64에서 Rust 1.98.1 엄격한 소유 Clippy·포맷 검사, Node 26.10.0 TypeScript 컴파일, PHP 문법, 문서 쌍 규칙을 통과했다. 이 단위는 구조 레코드이며 그래프 연결, SQL 검증, 임포트, DDL, DB 대조, 실행은 아니다.[o]
I1interfaces.md와 Mermaid 다이어그램에 공통 구조, 소유 규칙, 상태 전이를 정의한다.——[o]
I2모델 문법에 맞춰 contracts/interfaces.json, 생성 구성 요소 페이지, 심볼 검사를 다시 생성한다.—근거: go run ./tests/interfaces/check --generate --record --self-test와 make interface-check가 매니페스트, 두 구성 요소 페이지, 네 심볼 스냅샷, 소스 변경 반례를 다시 생성하고 대조했으며 생성물이 최신이고 네 언어가 모두 통과했다.[o]
I3모델 문법과 프로세스 내 플래너에 맞춰 구현 대조표를 다시 작성한다.—근거: 영문·국문 매트릭스가 각 언어의 planner와 생성 모델 소유 위치, owner 테스트 위치, 공유 conformance 검사, SQLite datetime 범위를 명시하며 make interface-check와 문서 쌍 검사가 통과했다.[o]
I4네 클라이언트에서 연결, 트랜잭션, 관계, 쓰기, 시간대를 물리 데이터베이스로 검증한다.——[o]
I4.1PHP request 레코드 선언 20개를 공통 필드·중첩 타입 정의와 대조한다. 소스 변경 반례는 검사에 실패하고 PHP 단독 선언 검사는 통과해야 한다.——[o]
I4.2모든 인터페이스 오류 표기와 기록된 오류 결과를 docs/spec/errors.yaml과 대조한다. 알 수 없거나 중복된 표기와 잘못된 오류 결과는 실패해야 한다.——[o]
D1VitePress로 Markdown 페이지를 빌드하고 구현 상태와 로컬 검색을 제공한다.——[o]
D2Mermaid 다이어그램을 SVG로 렌더링하고 JavaScript 없는 페이지 내용을 검증한다.——[o]
D3/orm/ 링크, 앵커, 직접 HTML 경로, 검색, 모바일 탐색, 반복 빌드를 검사한다.——[o]
D4현재 페이지를 https://polyspec.github.io/orm/ 에 배포하고 검증한다. 원인: 정적 사이트를 두 번 빌드해 동일한 결과를 확인했고 로컬 정적 검사를 통과했으며 현재 Pages URL도 HTTP 200을 반환하지만, 이 저장소는 로컬 전용이고 개발 규칙이 현재 커밋의 push·게시를 금지한다.—증거: G5.24의 docs-pages 실행이 사이트를 빌드하고 검사해 배포했다. 그 Pages artifact(github-pages/artifact.tar)에는 file 179개가 있고, https://polyspec.github.io/orm/ 의 같은 경로에서 받은 file은 index.html, dbspec.html, checklist.html, ko/checklist.html을 포함해 모두 artifact와 byte 단위로 같다(179/179, HTTP 200). Mermaid 도표의 좌표는 빌드하는 platform의 font에 따라 달라지므로, build는 한 platform 안에서 비교하고(make docs-verify-idempotent) platform 사이에서는 비교하지 않는다.[o]
M1네 클라이언트에 모델 생성과 connect, 체인, 연결자, 그룹, 연산자, 값 형태, 컬럼 비교, 튜플, 서브쿼리를 구현한다.——[o]
M2관계, 조인, 컬럼 선택, 정렬, 그룹, limit, finder, 집계, 페이지를 구현한다.——[o]
M3쓰기를 구현한다: set, setRaw, new<Name>, plus, minus, create, creates, duplication, update, update(true), save, delete.——[o]
M4실행 흐름 단위 트랜잭션, savepoint, 재시도, 옵션, 행 잠금, connection.utils()를 구현한다.——[o]
M5예약 컬럼 이름과 생성 메서드와 충돌하는 이름을 거부한다.——[o]
N1검증, 계획, 방언, DDL을 PHP, Rust, TypeScript로 이식하고, Go 클라이언트는 엔진 패키지를 직접 호출한다.——[o]
N2언어마다 생성기 하나를 제공한다: ormgen gen --lang go, vendor/bin/orm-gen, orm-gen npm bin, orm-build crate.——[o]
N2.1실패 가능한 styled setter 다음 Rust Result 처리를 모델 호출과 구분한다. 우선순위: 생성 모델 컨트롤러 컴파일에 필요하다. 수락 기준: 추적 expect/unwrap 소스 사례가 수정 전 실패하고 생성은 Result 메서드를 만들지 않으면서 후속 호출의 모델을 유지하며 알 수 없는 모델 메서드는 계속 실패한다. Rust 소스 분석은 클라이언트 전용이며 런타임과 다른 클라이언트는 바꾸지 않는다.—증거: 소유 Result 처리 사례는 잘못된 expect 열 메서드로 먼저 실패했고 생성 사례 10개와 집중 Clippy가 통과한다. 생성 컨트롤러의 styled setter expect 호출이 컴파일되고 설정 사례를 포함한 CLI 68개가 통과한다. 체크리스트 검사는 통과하며 기존 문서 규칙 문제는 남아 있다.[o]
N2.1.1실패 가능한 Rust 모델 setter 뒤의 Result 변환을 구분한다. 우선순위: 컨트롤러 컴파일 전. 완료 기준: 추적 RED가 Result 메서드를 컬럼으로 읽는 오류를 재현하고 오류 변환과 추출 뒤 모델 호출을 유지하며 성공 값 변환과 원래 모델을 구분하고 알 수 없는 모델 호출과 실패하지 않는 setter의 Result 메서드를 거부한다. 소유 생성 사례와 생성 컨트롤러 사례가 통과해야 한다. 범위: Rust 소스 분석. 의존: N2.1.—근거: 생성 RED가 map_err와 map을 컬럼으로 처리하는 오류를 재현했고 생성 사례 14개와 생성 모델 사례 1개, 두 부분의 엄격한 Clippy와 문서 쌍 검사가 통과했다.[o]
N3네 클라이언트에서 MySQL, PostgreSQL, SQLite의 utils().schema().install()을 구현한다.——[o]
N3.1한 process에서 여러 schema의 생성 PHP model을 읽고, 각 model 요청을 연결에 등록된 그 schema의 engine으로 계획한다. 우선순위: core schema와 module schema를 한 연결에서 쓰는 데 필요하다. 원인: PHP registry는 process 전체에 schema hash 하나만 유지하고, 연결은 engine 하나만 가지며, install()은 테이블을 만들지만 manifest를 등록하지 않는다. 기준: 생성된 PHP model class마다 자기 schema hash를 가진다. 연결은 connect schema의 engine으로 시작한다. utils().schema().install()은 manifest hash를 내용과 대조하고 그 engine을 연결에 추가한다. 연결에 등록되지 않은 schema의 요청은 SCHEMA_HASH_MISMATCH로 실패한다. schema 하나의 사용은 그대로다. 여러 schema 연결 owner case를 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 실행한다.—증거: PHP owner case는 변경 전 두 번째 생성 bootstrap을 읽을 때 SCHEMA_HASH_MISMATCH: models of schema ... are loaded together로 실패했다. 변경 후 PHP와 TypeScript owner test의 several_schemas, unregistered_schema, edited_manifest가 MySQL, PostgreSQL, SQLite에서 두 번 통과한다. Go TestSchemaSet*와 Rust schema_set_*는 core schema와 decimal schema를 한 연결에서 쓰며 세 database에서 두 번 통과한다. PHP unit, model, schema database, SQLite concurrency, grouped-row, unselected-column 검사가 통과하고, 바뀐 symbol과 page에 대해 PHP interface symbol, feature page, 문서 규칙, 정적 문서 검사가 통과한다.[o]
N3.2네 클라이언트가 docs/spec/protocol.md의 여러 스키마 규칙을 따르게 한다: 연결은 자기에게 등록된 schema set만 계획한다. 원인: T8.8.1 통합 뒤 모든 클라이언트가 process가 읽은 어떤 set으로든 요청을 계획했다. Go engineFor는 어느 연결에서든 어떤 model의 schema로 engine을 만들었고, TypeScript와 PHP는 generated code가 import되거나 bootstrap된 모든 set을 계획했으며, Rust는 model이 품은 manifest로 계획했다. 그래서 set이 없거나 다른 버전의 set을 가진 데이터베이스의 연결이 요청을 계획하고 실행에서야 실패하거나 엉뚱한 table에서 실행했다. Go DB.plan은 schema를 확인하기 전에 plan cache를 읽어서, 편집한 generated code의 요청이 같은 hash와 모양의 cache된 plan으로 실행되었다. Criteria: 네 클라이언트에서 연결이 등록하지 않은 schema의 요청은 cache된 plan이 있어도 실행 전에 SCHEMA_HASH_MISMATCH로 실패한다. generated connect helper와 install()이 그 set을 연결에 더하고 raw 연결은 아무것도 더하지 않는다. 편집한 manifest는 CONFIG로 실패한다. case several_schemas, unregistered_schema, edited_manifest가 네 클라이언트에서 MySQL, PostgreSQL, SQLite로 통과한다. Depends on: N3.1.—Evidence: 결정과 이유: (1) raw 연결(Go orm.Connect, PHP Orm::connect, Rust Db::connect, TypeScript Db.connect)은 set 없이 시작한다. 그 연결에 데이터베이스가 무엇을 가졌는지 알려준 것이 없기 때문이다. set의 generated code는 connect helper(Go model.Connect, PHP Polyspec\Orm\Tests\Model\connect, Rust model::connect, TypeScript connect)를 가지며, helper는 연결을 열고 connectSchema(Go orm.ConnectSchema, PHP Orm::connectSchema, Rust Db::connect_schema, TypeScript Db.connectSchema)로 자기 set을 등록한다. 잊을 단계가 없는 호출 하나이고, schema는 그것을 담은 code에서 온다. model을 쓰는 code는 helper로 옮기고 raw connect는 tool에 남긴다. raw connect에 schema 매개변수를 더하지 않는다. (2) install()과 helper는 generated schema 값, 곧 manifest text와 선언한 manifestHash(Go *orm.Schema, PHP Orm\Schema, Rust &orm::Schema, TypeScript { manifestText, manifestHash })를 받는다. dbspec manifest text에는 hash가 없어서 선언한 hash가 있어야 편집한 text가 실패하기 때문이다. 불일치는 어떤 statement보다 먼저 CONFIG이고, install은 자기가 설치한 set을 등록한다. (3) Go에서 schema 확인이 plan cache보다 먼저 실행된다(DB.plan이 먼저 engineFor를 부르고, 이것이 요청 schema의 text와 등록을 확인한다). 그래서 편집한 manifest는 cache로 계획되지 않는다. Rust Db::plan은 이미 cache 전에 manifest를 확인했고 이제 거기서 등록도 확인하며, TypeScript와 PHP는 cache 전에 등록을 확인한다. Red: T24에서 Go case TestSchemaSetUnregistered*(어떤 package도 등록하지 않은 set의 model)와 TestSchemaSetEditedManifest*(같은 요청을 plan한 뒤 편집한 manifest)가 MySQL, PostgreSQL, SQLite에서 각각 3/3 <nil>로 실패했다. 요청이 계획되고 실행되었다. 등록 확인을 끈 상태에서 새 unregistered_schema case가 Rust(raw 연결에서 SCHEMA_HASH_MISMATCH 대신 DRIVER, MySQL, PostgreSQL, SQLite), PHP와 TypeScript(raw 연결의 bench 읽기, bench 연결의 decimal 읽기와 쓰기)에서 실패했다. Green(MySQL, PostgreSQL, SQLite): Go TestSchemaSet*, TestSchemaSetUnregistered*, TestSchemaSetEditedManifest* 9/9, Rust schema_set_* 9/9, PHP clients/php/tests/schema_set_test.php와 TypeScript clients/typescript/tests/schema-set.mjs 각 3 case × 3 database. installed_elsewhere는 unregistered_schema에 합쳤다: 연결은 다른 연결이 설치한 set을 쓰지 않는다. 바뀐 code와 호출자의 owner check는 모두 통과한다: go test -tags ormtest ./clients/go/orm ./clients/go/decimalmodel(test 112개), go test ./clients/go/model ./generator(test 41개), bench database에서 Go featurecoverage TestCoverage* database마다 26개; TypeScript tsc --noEmit, tests/engine.mjs, generate.mjs, generate-scan.mjs, dsn.mjs, models-scan.mjs(2), 3 database의 model.mjs, clock.mjs, driver-error.mjs, dbspec_runtime_db.mjs, sqlite-concurrency.mjs, rollback.mjs(--conditions=orm-test), 3 database의 coverage 파일 composite_keys, constraints_and_relations, model_queries, model_writes, parameter_chunking, transactions(2), schema_install(2), audit_triggers, dsn, interface_contract; PHP schema_set, engine, relation_keys, dsn, hostcodec, runtime_model, orm_gen, model(11 × 3), aes_json, clock, driver_error, runtime_db, sqlite_concurrency, rollback test, 3 database의 coverage 파일 17개, make php-without-mysql-check; Rust cargo test -p orm --features test-faults(test 91개), orm-build(18개), integration binary(database 실행 30개), 3 database의 그 coverage case 10개와 orm의 coverage_dsn, coverage_audit_triggers, coverage_authenticated_encryption, coverage_interface_contract case, workspace와 test-faults, live-db의 clippy -D warnings; make example-check(complex와 thin-slice), make group-rows-physical-check(실행 12개), make unselected-column-physical-check(실행 6개), make decimal-physical-check(실행 24개), 3 database에서 relations vector의 PHP, TypeScript, Rust conformance runner, go test ./contracts/..., 기록한 symbol과 hash로 interface-check(Db.connectSchema 추가, SchemaUtils.install은 GeneratedSchema를 받음), docs-rules-check, checklist-check, git-check, go-fmt-check, go-model-check, rust-fmt-check. docs/spec/protocol.md, dbspec.md, schema.md, usage.md, interfaces.md, interface-implementation.md, config.md, 생성한 component diagram, PHP와 Rust README와 각 한국어 짝이 규칙을 적는다.[o]
N3.3네 클라이언트에 utils().schema().register(manifestJson)를 제공한다. 우선순위: 스키마를 한 번 설치하고 이후의 모든 연결에 등록하는 데 필요하다. 원인: 연결에 manifest를 추가하는 공개 호출은 객체도 만드는 install()뿐이어서, 그 밖에 set을 등록하려면 내부 registerEngine이 필요하다. 기준: register는 manifest hash를 내용과 대조하여 다르면 CONFIG로 실패하고, statement를 실행하지 않아 아무것도 만들지 않으며, install()이 statement 다음에 하는 것과 똑같이 manifest의 engine을 연결에 추가한다. install()은 같은 등록을 사용한다. register_schema와 register_edited_manifest 사례가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다. 의존: N3.1.—증거: 변경 전 PHP와 TypeScript case는 register가 없어 실패했고, Go와 Rust case는 컴파일되지 않았다. 변경 후 register_schema와 register_edited_manifest가 clients/php/tests/schema_set_test.php, clients/typescript/tests/schema-set.mjs, Go TestRegister*, Rust register_*에서 기존 schema set case와 함께 MySQL, PostgreSQL, SQLite로 두 번 통과한다. 설치 전에 등록한 연결은 없는 module 테이블을 읽을 때 DRIVER를 받으므로 register는 아무것도 만들지 않는다. PHP와 TypeScript는 register 전의 module 요청을 SCHEMA_HASH_MISMATCH로 거부한다. Go와 Rust는 아직 연결의 스키마를 유지하지 않으며 N3.2가 이를 기록하고, Rust register는 manifest만 대조한다. Rust install도 이제 바뀐 manifest에 CONFIG를 반환한다. contracts/interfaces.json이 SchemaUtils.register를 가지고, native signature가 contracts/validate.go를 통과하며, snapshot hash가 기록된 symbol과 일치한다.[o]
N3.3.1이 branch가 utils().schema().register(manifestJson)를 제공하지 않음을 기록한다 (N3.3 다음). 원인: N3.3은 main에서 완료되어 register가 설치된 manifest를 연결에 더하지만, T8.8.1 통합이 그것을 대신했고 이 branch의 어느 client에도 그 호출이 없다. 그것을 제공한다고 적은 기록은 잘못 알리고, 두 번째 등록 호출은 한 일을 하는 두 방법이 된다. 결정: set이 연결에 들어가는 방법은 둘뿐이다. set의 generated code는 연결을 열고 자기 set을 그 연결에 등록하는 connect helper를 제공하고, utils().schema().install(schema)는 generated schema 값의 table을 만들고 그 set을 같은 연결에 등록한다. N3.2가 둘을 구현한다. 이유: set을 한 번 설치하고 이후의 모든 연결에서 쓰는 일은 connect helper가 다룬다. helper는 set을 담은 code에서 자기가 여는 모든 연결에 set을 등록한다. 공개 register()는 orm에 없는 동작 없이 API만 더한다.—Evidence: git grep -n "schema().register|register(manifest|registerEngine"은 그 호출을 main을 기술하는 docs/checklist.md, docs/checklist.ko.md, CHANGELOG.md, CHANGELOG.ko.md의 N3.3 기록과 T8.8.1 기록에서만 찾는다. checklist 밖의 docs/ 문서, contracts/의 파일(interfaces.json과 네 symbol 기록), test 어디에도 없다. Check: docs-rules-check(영어 문서 24개, 한국어 번역 24개), checklist-check, git-check가 통과한다.[o]
N4세 데이터베이스에 연결 시간대를 적용한다: PostgreSQL 오프셋 시간대, 시각 읽기, SQLite 시계 기본값, MySQL 명칭 시간대 오류.——[o]
N5프로세스 내 러너로 conformance 검사기를 실행하고 벡터를 다시 기록한다. 네 출력이 일치할 때만 기대값을 기록하고 거부 시 파일을 보존하며 TypeScript의 SQLite 정수 결과를 정확히 유지한다. 세 데이터베이스 기대값 파일의 write_cycle.price를 고정 소수부 텍스트로 기록했다.—증거: 출력 누락·불일치 및 벡터 누락·추가 사례는 실패하고 기록·TypeScript 정수 사례는 통과한다. make conformance-check는 MySQL, PostgreSQL, SQLite 각각 25개 벡터 × 네 클라이언트와 클라이언트별 동일한 두 실제 실행 및 행·카운터 상태 불변을 검증하며 통과한다.[o]
N5.1MySQL, PostgreSQL, SQLite에서 네 클라이언트를 모두 실행하고 모든 벡터를 기록된 기대값과 비교하며, 반복 실행이 관찰 대상 데이터베이스 상태를 바꾸지 않는지 확인한다.—증거: make conformance-check가 통과한다. 각 데이터베이스에서 25개 벡터를 네 클라이언트와 비교하고 실제 결과 테스트가 각 클라이언트를 두 번 실행해 출력이 같고 행과 카운터가 바뀌지 않음을 확인한다.[o]
N5.1.1네 출력을 모두 요구하고 오래된 출력과 달라진 반복 결과를 거부하며 실행 시간을 제한한다. 모든 테이블 행과 선언된 counter를 검사하고 선언된 테스트 counter만 복원하며 MySQL, PostgreSQL, SQLite에서 counter 관찰과 정리를 테스트한다.——[o]
N5.1.1.1JSON 숫자를 float64 반올림 없이 비교하고 기록한다. 2^53을 넘는 서로 다른 정수를 구분하고 동등한 십진 표현은 같은 값으로 처리한다.——[o]
N5.1.1.2중첩 JSON 실행 증거와 기대값의 중복 키를 거부하고 데이터베이스 기대값 파일이 없으면 실패한다.——[o]
N5.1.1.3실행기 명령 로그에서 -dsn과 --dsn 양쪽 표기의 데이터베이스 연결 문자열을 가린다.——[o]
N5.1.1.4어느 테이블에도 AUTOINCREMENT가 없어 sqlite_sequence가 존재하지 않을 때 SQLite 상태를 읽는다. 테이블이 없음을 확인했을 때만 카운터 0개로 처리하고 행 변경과 테이블이 있을 때의 시퀀스 변경은 계속 감지하며 다른 질의 오류는 거부한다.——[o]
N5.1.1.4.1SQLite 카운터 기준을 두 체크리스트 언어로 기록한다. sqlite_sequence 테이블이 없음을 확인한 경우만 카운터 0개로 처리하고 다른 질의 오류는 실패시킨다. 문서 표현 규칙과 항목 ID·상태 일치를 검증한다.——[o]
N5.1.2네 conformance 실행기 모두 예상 밖 오류를 전파하고 쓰기 실패 시 상태를 정리하며 세 데이터베이스에서 공통 집계 스칼라 기준을 강제한다.—증거: N5.1.2.9–N5.1.2.11이 실행기 오류와 쓰기 경로를 검증하고, N5.1.2.12는 각 데이터베이스에서 네 실행기를 두 번씩 실행하여 출력과 행·카운터 상태가 같은지 확인하며, N5.1.2.13은 세 데이터베이스에서 Rust 불리언 집계 결과를 검증한다. make conformance-result-check가 통과하고 기능 목록은 중앙 Go 실행기 테스트를 client 소유 근거와 구분해 등록한다.[o]
N5.1.2.1Go 값 변환, host 인코딩, 생성 모델 대입이 잘못된 형식, null, 범위 초과, 지원하지 않는 값을 오류로 보고한다. 유효한 값을 실행 테스트로 검증하고 행 조립의 대입 오류를 전파하며 insert 필드를 쓰기 전에 검증한다.——[o]
N5.1.2.2PHP와 TypeScript의 잘못되거나 유한하지 않은 집계 스칼라를 거부하고 공통 9개 사례로 가장 가까운 binary64 변환을 검증한다.——[o]
N5.1.2.3네 클라이언트의 생성 모델에서 decimal 컬럼 값을 정확히 보존한다. 생성된 decimal 필드는 48.0450을 포함해 고정 소수부 자릿수 텍스트를 binary64 변환 없이 받고 반환하며, 정밀도 1–18 또는 소수부 자릿수 0–정밀도를 벗어난 선언은 실패한다. SQLite는 배율을 적용한 부호 있는 정수를 저장하고 MySQL과 PostgreSQL은 decimal 값을 저장한다. scalar 집계에는 별도의 유한 binary64 기준을 유지한다.—증거: 네 클라이언트에서 공통 decimal 사례가 통과한다. make decimal-physical-check는 MySQL, PostgreSQL, SQLite에서 각 생성 모델을 두 번 실행하여 소유 테스트 24건, rollback, 데이터베이스 행과 카운터의 상태 불변을 검증한다. 기능 목록은 decimal 소유 테스트를 등록하며 전체 기능 coverage 감사는 G1에서 계속 관리한다.[o]
N5.1.2.3.1정확한 값 기준을 바꾸지 않고 Rust decimal 실제 데이터베이스 assertion의 불필요한 as_deref()를 제거한다. 원인: get_large_value()가 이미 빌린 선택적 문자열을 반환하므로 cargo clippy -p orm-tests --all-targets -- -D warnings가 clients/rust/tests/src/decimal_physical.rs:22에서 실패했다.—근거: assertion이 빌린 선택적 문자열을 직접 비교하도록 바뀌었고 orm 및 orm-tests 표적 Clippy가 통과했으며 변경한 Rust 실제 사례가 MySQL·PostgreSQL·SQLite에서 정확한 값 9007199254740993과 rollback을 유지하며 통과했다.[o]
N5.1.2.4Go, PHP, Rust, TypeScript 스키마 빌더의 auto 컬럼이 NULL을 허용하지 않는 부호 있는 i64 기본 키이도록 강제한다. 각 언어는 동일한 허용·거부 Mermaid 사례를 실행하고 거부된 선언의 원천 행을 보고한다.——[o]
N5.1.2.4.1실제 SQLite INTEGER PRIMARY KEY AUTOINCREMENT 컬럼을 각 스키마 도구에서 부호 있는 i64 자동 키로 읽고, 자동 키가 아닌 INTEGER 컬럼은 i32로 유지한다. 클라이언트에서 실제 SQLite 가져오기와 스키마 빌드를 검증한다.——[o]
N5.1.2.5Go scalar·collection 키의 문자열 대체 경로를 정확한 타입별 키 표현으로 바꾼다. 지원하지 않는 값은 실패하고 서로 다른 값은 충돌하지 않으며 relation, 분할 질의, collection 호출자가 오류를 전파한다.—증거: Go 소유 RED/GREEN 사례가 타입별 scalar·복합 key, 잘못된 값, byte 스캔 타입, relation key, 분할 질의 중복 제거, 모델 key callback, collection 조회를 검증한다. Go client 전체와 실행기 단위 사례가 통과하고 MySQL·PostgreSQL·SQLite에서 모델 조건·join·relation·컬럼·subquery가 통과한다. Go vet, 문서, 규칙, 체크리스트 검사도 통과한다.[o]
N5.1.2.6네 클라이언트와 세 데이터베이스에서 SQL NULL, 인코딩된 null 값, 조회하지 않은 값 스타일 컬럼을 구분한다. 완료된 Go·PHP·TypeScript·Rust 소유 사례는 공통 13개 사례와 MySQL·PostgreSQL·SQLite 실제 데이터베이스 사례로 저장 텍스트, getter, 행 배열, 모델 JSON 출력, 오류를 검증한다. TypeScript 출력 검증 사례도 각 데이터베이스에서 두 번 통과한다.——[o]
N5.1.2.6.1Go codec과 생성 모델 setter·getter에서 값 스타일 상태를 명시적으로 구분한다. MySQL, PostgreSQL, SQLite에서 공통 상태, 행 배열, 모델 JSON, 오류를 검증한다.——[o]
N5.1.2.6.2PHP 값 스타일 컬럼의 setter, getter, codec, 모델 출력에 StyledValue를 사용한다. MySQL, PostgreSQL, SQLite에서 공통 상태 사례와 실제 모델 동작을 검증한다.——[o]
N5.1.2.6.3TypeScript 값 스타일 컬럼의 setter, getter, codec, 모델 출력에 StyledValue를 사용한다. MySQL, PostgreSQL, SQLite에서 공통 상태·codec 사례와 실제 모델 동작을 검증한다.——[o]
N5.1.2.6.4Go conformance 쓰기 실행기에 명시적인 값 스타일 값을 전달하고 쓰기 전에 각 setter 오류를 반환한다. MySQL, PostgreSQL, SQLite에서 모델 생성, 실행기 컴파일, Go 모델·런타임 테스트를 검증한다.——[o]
N5.1.2.6.5Rust 값 스타일 컬럼 codec과 생성 setter·getter에 StyledValue를 사용한다. null 불허 컬럼의 SqlNull을 setter에서 거부하고 미조회 필드를 보고하며 SQL NULL과 인코딩된 null을 저장, 행 배열, 모델 JSON에서 보존한다.—증거: 공통 13개 사례와 Rust 라이브러리·생성기 테스트 23/23 통과, 생성된 null 불허 스타일 setter가 SqlNull을 거부하고 인코딩된 JSON null을 수용하는 사례 1/1 통과, orm-tests integration --case json_values가 MySQL·PostgreSQL·SQLite에서 각각 두 번 및 setter 사례 추가 후 한 번 더 통과, 작업공간 Clippy·서식·영한 문서·체크리스트 검사 통과.[o]
N5.1.2.6.5.1명시적인 StyledValue 값 래퍼에 맞춰 Rust 모델 JSON 출력 검사를 갱신하면서 정렬된 JSON 멤버 순서와 숫자 텍스트를 보존한다. 원인: 완료된 스타일 컬럼 구현은 {"kind":"value","value":...}를 출력하지만 기존 정렬 텍스트 검사는 이전의 래퍼 없는 문서를 예상했다.—증거: cargo test --offline --locked -p orm --test model_output -- --exact json_output_keeps_the_ordered_json_text가 두 정렬 fixture에서 통과한다.[o]
N5.1.2.6.6TypeScript 값 스타일 JSON 출력에서 정의되지 않았거나 생략되는 멤버를 모델 변경 전과 인코딩·대입 값 읽기 시 거부한다. 소유 RED 사례는 StyledValue.value({missing: undefined}).toJSON(), 중첩 객체, 빠진 배열 위치, 숨은 멤버와 심볼 멤버, 지원하지 않는 객체, 대입 후 변경을 검증한다. 소유 사례와 TypeScript 빌드가 통과하고 model.mjs --case styledStates --dialect가 MySQL·PostgreSQL·SQLite에서 각 두 번 통과하며 영한 문서·체크리스트 검사도 통과한다.——[o]
N5.1.2.6.6.1TypeScript conformance 결과의 값 스타일 값에 정의되지 않은 멤버가 있으면 CODEC_ENCODE 코드를 검사한다. 추적된 중앙 결과 테스트는 종전 진단 문구 검사에서 실패하고 코드 검사 뒤 5/5 통과하며 TypeScript 값 스타일 소유 사례와 영한 문서·체크리스트 검사도 통과한다.——[o]
N5.1.2.7생성 Rust 모델 필드를 비공개로 두고 조회하거나 대입하지 않은 필드 접근을 COLUMN_UNSELECTED로 거부한다. SQL 열 선택을 유지하고 일부 필드만 채운 모델 대신 GroupRows로 그룹 값과 검증한 행 개수를 반환한다.—증거: MySQL·PostgreSQL·SQLite에서 생성 모델과 그룹 사례를 실행하고 잘못된 그룹 개수를 거부하며 Rust code가 결과 타입으로 컴파일된다.[o]
N5.1.2.8실패한 실행을 포함하여 모든 conformance 실행 뒤 모든 테이블 행과 선언된 카운터를 확인한다. 실행기 오류와 남은 데이터베이스 상태 변경을 함께 보고한다. SQLite 오류 사례와 MySQL·PostgreSQL·SQLite 실제 데이터베이스 사례로 실패 보고와 정리를 검증한다.——[o]
N5.1.2.9PHP와 TypeScript conformance 실행기가 잘못된 파생 정수와 결과 값을 거부하고 ordered JSON 숫자를 정확히 보존하며 예상 밖 벡터 오류를 전파하고 쓰기 벡터를 트랜잭션에서 실행한다. 결과 사례와 MySQL, PostgreSQL, SQLite에서 행·카운터를 바꾸지 않는 동일한 두 실행을 검증한다.——[o]
N5.1.2.10Go conformance 실행기가 잘못된 질의 바인딩과 누락된 조회 필드를 거부하고 예상 밖 오류를 보존하며 집계 binary64 값을 검증하고 쓰기 벡터를 트랜잭션에서 실행한다. 오류 사례와 MySQL, PostgreSQL, SQLite에서 행·카운터를 바꾸지 않는 동일한 두 실행을 검증한다.——[o]
N5.1.2.11Rust conformance 실행기가 잘못된 바인딩과 파생 정수 값을 거부하고 다른 binary64 비트의 집계 평균을 거부하며 예상 밖 벡터 오류를 전파하고 실패한 쓰기 벡터를 롤백한다. 잘못된 암호문 접두를 RED·GREEN 사례로, MySQL·PostgreSQL·SQLite에서 실패한 쓰기 상태를 검증한다. N5.1.2의 네 클라이언트 성공 비교는 계속 필요하다.——[o]
N5.1.2.12실제 결과 테스트가 MySQL, PostgreSQL, SQLite에서 정확한 네 client 목록을 각각 두 번 실행하고 두 출력을 비교하며 모든 행과 카운터가 이전 상태로 돌아오는지 검증한다. Rust 실행기가 빠지면 실행 전에 실패해야 한다. 중앙 Go 실행기 테스트를 기능 목록에 등록하고 실행하되 client 소유 증거로 사용하지 않는다.—증거: 네 client 중 세 개만 있는 경우 실패하며 수정 후 client/데이터베이스 12개 사례와 각 반복 실행 및 중앙 Go 실행기 테스트가 통과한다.[o]
N5.1.2.13선택한 Rust 불리언 그룹 컬럼을 GroupRows 반환 전에 선언된 타입으로 해석한다. 소유 RED 사례는 불리언 그룹 결과의 정수 0/1을 재현하고 잘못된 불리언을 거부해야 하며, 수정 후 다른 타입 컬럼을 바꾸지 않고 GREEN이 되어야 한다.—증거: Rust 라이브러리 16/16과 Clippy가 통과했고 MySQL, PostgreSQL, SQLite에서 aggregates 벡터가 각각 두 번 기대값과 같으며 행과 카운터가 바뀌지 않았다.[o]
N6컴파일러 서비스, 메시지 정의, WASM·FFI 진입점, 배포 유닛을 제거하고 Makefile과 CI를 갱신한다.——[o]
N7문자열로 받은 SQLite datetime 텍스트를 저장 형식인 소수 여섯 자리 형태로 비교한다.—근거: Go TestConnectionTimeZone, PHP 모델 모음, TypeScript 모델 모음이 SQLite에서 소수부가 없거나 0인 문자열과 분수부 문자열을 비교하고 날짜만 있는 datetime을 거부하며 생성된 값을 6자리 형식으로 읽는다. 문자열 datetime 입력을 지원하는 MySQL·PostgreSQL에서도 같은 사례가 통과한다.[o]
N8orm-build로 150개 테이블 Rust 컴파일 검사를 실행한다.—근거: make rust-150-check가 150개 엔티티를 생성하고 모든 생성 모델의 getter, setter, chain을 호출하는 Rust crate를 컴파일했다.[o]
N9한 번 실행하는 Rust 트랜잭션에서 콜백의 오류 타입을 보존한다. MySQL, PostgreSQL, SQLite에서 rollback, commit, 중첩 savepoint 동작과 서로 다른 콜백·rollback 오류를 검증한다.——[o]
N9.1statement_timeout_ms를 busy_timeout으로 적용해 SQLite 잠금 대기를 제한한다. 양수인 연결 timeout은 선언한 한도에서 CANCELED를 반환하고 이후 작업을 위해 연결을 유지해야 한다.—증거: 200 ms 한도를 사용하는 SQLite 소유 사례와 생성 model 사용자 사례가 통과하고 Rust format·Clippy가 통과한다.[o]
N9.2Send 서비스 callback을 통과해야 하는 생성 모델 쓰기에 Db::transaction_send를 제공한다. callback과 그 future가 Send임을 요구하고 트랜잭션 옵션과 재시도 동작을 보존하며 반환 future의 Send 요건을 소유자 테스트로 검증한다.—증거: ORM 컴파일과 요건 테스트가 통과한다.[o]
N10네 클라이언트에서 클라이언트 clock을 마이크로초로 기록한다. 원인: TypeScript 클라이언트는 밀리초 해상도의 Date로 clock을 읽어, now bind slot이 datetime(6) column에 .mmm000을 저장하고, Go, PHP, Rust 클라이언트는 실제 소수 여섯 자리를 저장한다. 기준: docs/spec/protocol.md가 now bind slot의 규칙 하나를 적는다: 마이크로초 해상도의 wall clock을 소수 여섯 자리로 자르고, statement마다 한 번 읽는다. TypeScript 클라이언트는 wall clock과 monotonic clock으로 마이크로초를 읽는다. clock_microseconds 사례가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다. 의존: 없음.—증거: TypeScript clock_microseconds case가 변경 전 SQLite에서 저장 값이 모두 000으로 끝나 실패했다. 변경 후 clients/typescript/tests/clock.mjs, clients/php/tests/clock_test.php, Go TestClockMicroseconds*, Rust clock_microseconds_*가 MySQL, PostgreSQL, SQLite에서 두 번 통과한다. TypeScript model·engine suite, Rust Clippy·formatting, PHP syntax, feature manifest, 문서 규칙 검사가 통과한다.[o]
N11네 클라이언트에서 모든 driver 오류를 catalog code를 가진 ORM 오류로 보고한다. 원인: PHP 클라이언트는 catalog에 없는 driver 오류를 PDOException 그대로 돌려주어, orm:audit이나 orm:immutable trigger가 거부한 쓰기에 ORM code가 없다. Go 클라이언트는 그런 오류를 바꾸지 않고 돌려주고, Rust 클라이언트는 catalog에 없는 code SQLX를 보고하며, Go, PHP, Rust 클라이언트는 SQLite trigger 거부 1811을 FOREIGN_KEY로 보고한다. PHP, Rust, TypeScript 클라이언트는 CHECK 위반을 CONSTRAINT로 바꾸지 않는다. 기준: docs/spec/errors.yaml이 목록에 없는 모든 driver 오류에 DRIVER를 두고, driver 메시지와 driver 오류를 원인으로 유지한다. SQLite 1811은 FOREIGN_KEY가 아니다. trigger_refused(DRIVER)와 check_refused(CONSTRAINT) 사례가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다. 의존: 없음.—증거: 변경 전 PHP case는 세 데이터베이스 모두에서 PDOException을 던졌고, Go는 MySQL과 PostgreSQL에서 code가 없고 SQLite에서 FOREIGN_KEY였으며, Rust는 SQLX와 FOREIGN_KEY, TypeScript는 CHECK 위반에 DRIVER를 보고했다. 변경 후 clients/php/tests/driver_error_test.php, clients/typescript/tests/driver-error.mjs, Go Test(Trigger|Check)Refused*, Rust refusal이 MySQL, PostgreSQL, SQLite에서 두 번 통과한다. PHP model·schema set·clock suite, TypeScript model·schema set·SQLite concurrency·engine·DSN suite, Go client·engine·generator package, Rust workspace가 decimal과 coverage 환경이 필요한 case를 빼고 통과한다. Rust Clippy, 바뀐 선언의 symbol 기록, 문서 규칙 검사가 통과한다.[o]
N12TypeScript 생성기 scan에서 model chain의 method 호출만 타입으로 만든다. 원인: --scan은 소스 파일의 모든 method 호출 이름을 모아, model이 아닌 class의 this.enabled() 같은 호출이 enabled column을 가진 모든 model에 enabled method를 더하고, 그 호출을 지우면 method도 사라진다. 기준: docs/operations/usage.md가 scan이 model로 해석하는 receiver를 적는다. receiver가 model로 해석되지 않는 호출은 method를 더하지 않는다. 생성기 owner 사례가 model이 아닌 호출은 아무것도 더하지 않고, new, binding, 타입이 있는 parameter, callback, row, collection을 거친 model 호출은 자기 model에 method를 더함을 보인다. 의존: 없음.—증거: clients/typescript/tests/generate-scan.mjs가 변경 전 14건으로 실패했다. 다른 receiver에 대한 호출이 model에 name, isClose, readCount, aliasGhost, getGhost를 선언했고, ltSeq와 neName은 모든 model에 선언되었다. 변경 후 두 case가 두 번 통과한다. 저장소 build도 browser test의 호출인 newPage를 모든 model에 선언했었다. generator, TypeScript type check, model, schema set, SQLite concurrency, clock, driver error, styled value suite가 통과하고, model 18개를 가진 project가 다시 생성한 model로 type check를 통과하며, 다시 생성한 model은 다른 receiver에 대한 호출의 선언만 잃는다. scanner와 생성 model의 symbol 기록, feature manifest, 문서 규칙 검사가 통과한다.[o]
N13네 클라이언트에서 트랜잭션이나 savepoint의 callback이 실패하고 rollback도 실패하면 두 오류를 모두 보고한다. 원인: PHP 클라이언트는 callback 오류를 rollback 오류로 바꾸고, TypeScript 클라이언트는 rollback 오류를 버리며, Go 클라이언트는 트랜잭션 rollback 오류를 버리고 savepoint rollback 오류만 돌려주고, Rust 클라이언트는 두 오류를 CONFIG 문자열로만 보고하고 savepoint rollback 오류만 돌려준다. 기준: docs/spec/errors.yaml이 ROLLBACK을 가진다. 그런 실패의 오류는 code ROLLBACK을 가지고, 메시지에 두 오류를 적으며, callback 오류와 rollback 오류를 유지한다. ORM test는 SQLite에서 callback이 model 호출만 하는 동안 ROLLBACK을 일으키는 trigger로 트랜잭션을 종료하고, MySQL과 PostgreSQL에서는 test 연결이 트랜잭션의 server session을 종료한다. rollback_failed와 savepoint_rollback_failed case가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다. 의존: N11.—증거: 변경 전 PHP는 MySQL과 PostgreSQL에서 rollback의 PDOException을 그대로 던지고 SQLite에서 ROLLBACK code가 없었으며, TypeScript는 callback 오류만 보고하고 처리되지 않은 PostgreSQL error event로 process가 종료되었고, Go는 여섯 실행 모두에서 callback 오류만 보고했고, Rust에는 Error::Rollback이 없었다. 변경 후 clients/php/tests/rollback_test.php, clients/typescript/tests/rollback.mjs, Go Test(Savepoint)?RollbackFailed*, Rust rollback이 MySQL, PostgreSQL, SQLite에서 두 번 통과한다. 트랜잭션 안의 Go 취소 case는 이제 MySQL과 PostgreSQL에서 취소를 유지한 ROLLBACK을 기대한다. PHP, TypeScript, Go client suite와 Rust workspace가 decimal과 coverage 환경이 필요한 case를 빼고 통과하며, Rust Clippy, Go vet, symbol 기록과 hash, feature manifest, 문서 규칙 검사가 통과한다.[o]
N14MySQL에서 database clock을 마이크로초로 기록하고, SQLite 상대 value function에서 bind한 clock의 소수부를 유지한다. 원인: MySQL dialect가 soft delete를 CURRENT_TIMESTAMP로, now value function과 그 상대 형식을 NOW()로 렌더링하므로 soft delete가 datetime(6) column에 초 단위 값을 저장하고, created_ts <= now() 조건이 같은 초에 먼저 만든 row를 놓친다. SQLite 상대 형식은 datetime(clock, modifier)를 렌더링하여 bind한 clock의 소수부를 버린다. PostgreSQL은 마이크로초를 저장하고 비교한다. 기준: docs/spec/protocol.md가 규칙 하나를 적는다. ORM이 column에 database clock을 쓸 때 update 시각과 soft delete는 column이 선언한 소수 자릿수를 쓰고(MySQL CURRENT_TIMESTAMP(p)), MySQL now value function과 그 상대 형식은 NOW(6)을 쓰며, SQLite 상대 형식은 bind한 clock의 여섯 소수 자리를 유지한다. clock_soft_delete_microseconds와 clock_now_condition case가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다. conformance 기대값이 새 SQL을 가진다. 의존: 없음.—증거: 변경 전 clock_soft_delete_microseconds가 네 클라이언트 모두 MySQL에서 모든 저장값이 .000000으로 끝나 실패했고, clock_now_condition이 MySQL에서 now()와 secondsLater(0), SQLite에서 secondsLater(0)으로 실패했으며, PostgreSQL은 둘 다 통과했다. 변경 후 Go TestClock*, clients/php/tests/clock_test.php, clients/typescript/tests/clock.mjs, Rust clock이 clock_microseconds와 함께 MySQL, PostgreSQL, SQLite에서 두 번 통과한다. datetime(6)과 datetime column의 soft delete case를 포함한 Go engine과 planner test, PHP와 TypeScript engine과 model suite, Rust orm library test와 Clippy, 문서 규칙과 build 검사가 통과한다. functions vector는 일치하는 네 출력에서 NOW(6)과 SQLite 상대 형식으로 기록했다. 공유 orm_bench database는 main에서도 bench/sql과 다르므로 MySQL과 PostgreSQL은 bench/sql로 seed한 별도 database에서 실행했다.[o]
N15migration ledger의 시각을 모든 database에서 마이크로초로 저장한다. 원인: ledger orm_schema_migrations는 MySQL에서 timestamp를 선언하고 SQLite에서 CURRENT_TIMESTAMP로 쓴 TEXT를 쓰므로, started_at과 finished_at은 두 database에서 초 단위로 저장되고 PostgreSQL에서만 마이크로초를 유지한다.인수 기준: docs/spec/protocol.md가 하나의 규칙을 적는다. MySQL은 CURRENT_TIMESTAMP(6)으로 쓰는 timestamp(6), PostgreSQL은 CURRENT_TIMESTAMP로 쓰는 timestamptz, SQLite는 소수 여섯 자리를 요구하는 CHECK가 있고 tool clock을 UTC로 받는 TEXT를 선언한다. command가 기존 ledger를 쓰기 전에 tool은 그 시각 column을 검증하고, 다른 시각 column을 가진 ledger는 MIGRATION_HISTORY_PRECISION으로 실패하고 바뀌지 않는다. docs/operations/usage.md가 이전 MySQL 또는 SQLite ledger를 변환하는 statement를 적는다. migration_ledger_microseconds, migration_ledger_whole_seconds, migration_ledger_earlier case가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다. 의존: 없음.증거: 변경 전 migration_ledger_microseconds는 네 client 모두 MySQL과 SQLite에서 초 단위 시각으로 실패하고 PostgreSQL에서 통과했으며, migration_ledger_whole_seconds는 초 단위 ledger에서 migration이 실행되어 네 client 모두 세 database에서 실패했다. 변경 후 Go TestMigrationLedger*, clients/php/tests/ledger_test.php, clients/typescript/tests/ledger.mjs, Rust cli의 migration_ledger_* case가 별도 database의 MySQL, PostgreSQL, SQLite에서 두 번 통과한다. Go internal/ormgen test, Rust cli 전체 test, PHP schema database test, TypeScript schema tool parity test가 통과한다. symbol inventory는 바뀐 file의 새 symbol을 가진다. interface check는 다른 작업의 기록되지 않은 symbol을 계속 보고하고, Clippy는 tests/unit/typed_bind_validation.rs의 finding 하나를 보고하며, 이 항목은 둘 다 바꾸지 않는다.[o]
N16네 클라이언트에서 모든 데이터베이스의 트랜잭션 rollback을 실패시키는 test fault를 제공한다. 원인: orm은 callback 오류와 실패한 rollback을 함께 보고하지만, rollback을 실패시키는 지원 방법을 제공하지 않았다. ORM 자신의 test는 MySQL과 PostgreSQL에서 server session을 종료하거나 SQL로 쓴 SQLite trigger를 설치하며, SQL 없는 test는 이를 할 수 없다. 기준: docs/spec/protocol.md §3.1이 규칙을 적는다. 클라이언트의 test entry point만 fault를 설정하고, DSN, 설정 값, 환경 변수는 이를 설정하지 않는다. Go 함수 FailNextRollback은 build tag ormtest에서만, Rust module orm::testing은 feature test-faults에서만 있고, TypeScript subpath @polyspec/orm-typescript/testing은 Node condition orm-test에서만 찾아지며, PHP class Orm\Testing\Faults는 package autoloader 밖에 있다. callback이 실패한 다음 트랜잭션의 rollback은 실행된 뒤 새 catalog code FAULT로 보고되어, 트랜잭션은 callback 오류와 FAULT 오류를 가진 ROLLBACK을 반환한다. commit은 fault를 소비하지 않고, 그다음 실패한 트랜잭션은 callback 오류만 반환한다. rollback_fault case가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과하고, 각 클라이언트는 production entry point가 fault에 닿지 않음을 확인한다. 의존: N13.—증거: 구현 전, fault를 설정만 하는 entry point로 rollback_fault는 열두 실행 모두에서 실패했다. Go TestRollbackFault*와 PHP, TypeScript, Rust case가 MySQL, PostgreSQL, SQLite에서 ROLLBACK 대신 callback 오류만 보고했다. 구현 후 Go TestRollbackFault*(build tag ormtest), clients/php/tests/rollback_test.php, clients/typescript/tests/rollback.mjs(condition orm-test), Rust rollback(feature test-faults, transaction_send와 transaction_once 포함)이 rollback_failed, savepoint_rollback_failed와 함께 MySQL, PostgreSQL, SQLite에서 두 번 통과한다. entry case TestRollbackFaultEntry, rollback_fault_entry와 PHP, TypeScript entry 확인이 통과한다. test-faults의 Rust Clippy, ormtest의 Go vet, 문서 build, idempotence, 규칙, checklist, feature manifest, 새 symbol의 interface check가 통과한다. main에서 실패하는 검사(feature coverage, Git subject, 다른 작업의 interface inventory, TypeScript codec vector, schema record, orm-build의 Clippy와 formatting, Go decimal과 coverage case, PHP AES JSON과 audit service, TypeScript model suite, conformance 결과, performance 검사)는 이 변경에서도 같은 실패를 보고한다.[o]
N17PHP 클라이언트에서 PDO driver 확장을 요구하지 않는다. 원인: clients/php/composer.json이 ext-pdo_mysql을 요구하고 pdo_pgsql과 pdo_sqlite는 제안만 했으므로, 클라이언트는 mysql:// DSN이 연결을 열 때만 Pdo\Mysql을 참조하는데도 공식 PHP image처럼 pdo_mysql이 없는 PHP에서 composer install이 거부되었다. 수용 기준: composer.json은 ext-pdo_mysql, ext-pdo_pgsql, ext-pdo_sqlite 가운데 어느 것도 요구하지 않고 각각을 제안한다. scripts/php-without-mysql.sh와 make php-without-mysql-check가 공식 PHP image php:8.5.10-cli-trixie에서 실행하는 case clients/php/tests/sqlite_without_mysql.php는 manifest를 검사하고 pdo_mysql 없이 SQLite에서 row를 만들고 읽는다. 의존: 없음.—근거: case는 변경 전에 requires ["ext-pdo_mysql"]로 실패했고 변경 뒤 통과한다. composer validate, PHP unit test, checklist와 문서 검사가 통과한다.[o]
N18네 클라이언트에서 한 가지 DSN 모드로 MySQL에 TLS 연결하고, PHP와 TypeScript 클라이언트에서 알 수 없는 DSN 매개변수를 거부한다. 원인: 어떤 클라이언트도 MySQL TLS 매개변수를 밝히지 않았다. PHP와 TypeScript 클라이언트는 알 수 없는 매개변수를 모두 무시했으므로 ssl-mode=VERIFY_CA&ssl-ca=<file>이 TLS 없이 연결되었고, PHP driver는 host 이름을 함께 검사하지 않고는 server 인증서를 CA로 검사할 수 없으므로, 모든 클라이언트가 구현할 수 있는 모드는 VERIFY_IDENTITY 하나다. 수용 기준: docs/spec/config.md가 절대 ssl-ca와 host 이름을 가진 ssl-mode=VERIFY_IDENTITY를 공유 MySQL TLS 모드 하나로 밝히며, 이는 CA와 host 이름을 검사하는 TLS이고 RSA password 교환이 없다. Go, PHP, TypeScript 클라이언트는 다른 ssl-mode, 없거나 상대인 ssl-ca, ssl-mode 없는 ssl-ca, ssl-mode와 함께 쓴 socket, IP 주소 host에 CONFIG를 반환한다. PHP와 TypeScript 클라이언트는 각 scheme의 밝힌 집합 밖의 매개변수에 CONFIG를 반환한다. Rust 클라이언트는 매개변수를 계속 sqlx에 전달하며 VERIFY_IDENTITY로 test한다. 네 클라이언트의 TLS case는 make test-servers의 TLS server에 연결하고, 다른 CA의 인증서와 다른 host를 이름한 인증서를 거부한다. 의존: 없음.—증거: scripts/test-servers.sh tls는 test CA, localhost를 이름한 primary 인증서, 같은 CA가 서명하고 orm-mismatch.invalid를 이름한 replica 인증서, 두 번째 CA를 발급하고, ALTER INSTANCE RELOAD TLS로 불러오며, TCP로 해석되는 host localhost.로 ORM_TEST_MYSQL_TLS_DSN, ORM_TEST_MYSQL_TLS_OTHER_CA_DSN, ORM_TEST_MYSQL_TLS_MISMATCH_DSN을 쓴다. RED: clients/typescript/tests/mysql_tls.mjs와 clients/php/tests/mysql_tls.php는 거부해야 할 DSN을 모두 받고, Ssl_version이 비었고, 잘못된 두 server를 모두 받아 실패했다. clients/go/orm/mysql_tls_test.go는 parse한 DSN의 TLS field가 없어 compile에 실패했다. GREEN: 셋은 primary의 TLSv1.3과 잘못된 두 server의 거부로 통과한다. clients/rust/orm/tests/mysql_tls.rs는 sqlx가 ssl-mode를 직접 구현하므로 바로 통과했다(2개 중 2개). PHP DSN parse case, TypeScript database 묶음, TypeScript DSN, engine, generator, schema tool case, PHP schema, ledger, clock, driver error, rollback case, MySQL, PostgreSQL, SQLite의 Go, PHP, TypeScript DSN coverage case가 통과한다. TypeScript codec vector case와 PHP AES JSON, audit service, 빈 schema case는 ORM main에서 같게 실패하며 DSN 매개변수를 읽지 않는다. 후속: 클라이언트는 받는 다른 매개변수가 서로 다르다. Go 클라이언트는 그것을 go-sql-driver/mysql과 pgx에, Rust 클라이언트는 verify_ca도 받는 sqlx에 전달한다. 이를 PHP와 TypeScript처럼 Go와 Rust에서 거부하면 Rust 클라이언트에 ssl-mode=verify_ca를 주는 MySQL DSN도 거부된다.[o]
N19네 클라이언트에서 연결을 통해 매니페스트의 기존 테이블에 없는, NULL을 허용하거나 기본값이 있는 컬럼을 추가한다. 우선순위: 설치가 실제 데이터베이스에 이미 있는 테이블에 컬럼을 추가하는 데 필요하다. 원인: utils().schema().install(manifestJson)은 없는 테이블만 만들고 기존 테이블은 유지하며, migration tool은 자기 연결, ledger, log 디렉터리로 매니페스트 하나를 실제 데이터베이스 전체와 비교해 계획하므로, module 하나의 매니페스트에는 다른 모든 테이블의 삭제를 계획한다. 가져오기는 테이블이 자기 키를 참조하는 외래 키도 버렸으므로 그런 테이블은 매니페스트와 일치하지 않았다.인수 기준: docs/spec/schema.md §8은 utils().schema().addColumns(manifestJson)(Go AddColumns, Rust add_columns)을 기술한다. 이 호출은 매니페스트의 기존 테이블에 없는 컬럼 가운데 NULL을 허용하거나 기본값이 있는 컬럼을 모두 추가하고, 바뀐 각 테이블의 audit 트리거를 새 컬럼을 기록하도록 교체하며, 추가한 컬럼을 table.column으로 반환하고, 없는 테이블과 다른 매니페스트의 테이블은 바꾸지 않으며, 다른 모든 차이(필수인 없는 컬럼, 삭제되거나 바뀐 컬럼, 키, 인덱스)에는 변경 전에 새 catalog code SCHEMA_DIFFERS를 반환한다. 반복 호출은 아무것도 추가하지 않는다. MySQL에서 트랜잭션 안의 호출은 CONFIG를 반환한다. §6은 테이블이 자기 키를 참조하는 외래 키를 그 테이블 자신에 대한 관계로 가져온다고 기술한다. fixture contracts/fixtures/add_columns_*로 add_columns, add_columns_differs, add_columns_transaction, add_columns_edited_manifest case가 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다. 의존: 없음.근거: RED: 호출이 없었으므로 clients/php/tests/add_columns_test.php와 clients/typescript/tests/add-columns.mjs는 열두 번의 실행 모두에서 실패했고, clients/go/orm/add_columns_test.go와 clients/rust/orm/tests/add_columns.rs는 compile되지 않았다. 호출이 있고 가져오기가 이전 상태일 때, addcol_tag가 자기 키를 가지는 add_columns case는 네 클라이언트와 세 데이터베이스에서 SCHEMA_DIFFERS로 실패했다. SQLite는 addcol_tag의 rebuild를, MySQL과 PostgreSQL은 ADD CONSTRAINT fk_addcol_tag_parent_addcol_tag_seq를 계획했다. 첫 PHP 실행은 가져오기가 MySQL decimal 기본값을 scale의 자릿수로, PostgreSQL 문자열 기본값을 따옴표와 함께 읽는다는 것도 보였다. 호출은 이 기본값을 값으로 비교한다. GREEN: 네 case가 PHP, TypeScript, Go TestAddColumns*, Rust add_columns*에서 MySQL, PostgreSQL, SQLite로 두 번 통과한다. 행은 유지되고, 새 컬럼은 NULL이나 기본값을 읽으며, audit 대상 update는 {"note":"later","rank":4}를 기록하고, log 테이블은 바뀌지 않으며, 없는 테이블은 install 전까지 없고, 다른 일곱 가지 차이는 각각 컬럼을 추가하지 않고 SCHEMA_DIFFERS를 반환한다. Rust catalog 읽기와 실제 스키마 가져오기는 orm-build에서 orm_schema::catalog와 orm_schema::live로 옮겨, runtime과 tool이 하나의 Catalog trait로 catalog를 읽는다. PHP schema, schema tool, schema database, ledger, schema set case, TypeScript schema tool, schema case, SQLite import, ledger, engine, schema set, generator, DSN, driver error case, Go internal/ormgen, engine, client package, Rust workspace와 orm-build --features cli test, TypeScript type check, workspace와 test-faults의 Rust Clippy, 문서 기록이 통과한다. decimal, coverage, send-savepoint 환경이 필요한 case, physical_graph_vectors의 시간 제한, 공유 데이터베이스에서 다른 test의 테이블을 발견하는 빈 데이터베이스 case가 있는 TypeScript model suite, orm-build의 formatting과 cli Clippy 지적은 ORM main에서도 같은 방식으로 실패한다.[o]
N19.1네 클라이언트의 addColumns에서 기존 테이블의 PostgreSQL 컬럼 타입과 잘린 인덱스 이름을 저장된 형식으로 비교한다. 원인: PostgreSQL은 char(n)을 varchar(n)으로, 모든 blob 타입을 bytea로 저장하고, 63 byte보다 긴 인덱스 이름은 잘린 뒤 digest를 붙여 저장하므로, 가져오기는 매니페스트가 선언한 것과 다른 raw 타입과 인덱스 이름을 읽는다. addColumns는 longblob, char(64), 긴 인덱스 이름을 가진 설치된 core와 board 매니페스트에 SCHEMA_DIFFERS를 반환했다.인수 기준: docs/spec/schema.md §8은 dialect가 선언한 타입으로 저장하는 컬럼은 선언한 타입을 가지고, 물리 이름이 선언한 인덱스의 물리 이름과 같은 인덱스는 선언한 이름을 가진다고 기술한다. fixture contracts/fixtures/add_columns_*는 longblob 컬럼, char(8) 컬럼, PostgreSQL 이름이 63 byte보다 긴 인덱스를 가지며, N19 case가 이 fixture로 Go, PHP, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 통과한다.근거: RED: 새 fixture로 네 case가 네 클라이언트의 PostgreSQL에서 SCHEMA_DIFFERS(addcol_item.payload changed from raw=blob to raw=longblob)로 실패했다. 타입 비교만 있고 인덱스 비교가 없을 때 PHP add_columns는 PostgreSQL에서 DROP INDEX "addcol_item_ix_addcol_item_enabled_and_created_at__b10618c7774a"와 CREATE INDEX "addcol_item_ix_addcol_item_enabled_and_created_at_stored_together"로 실패했다. GREEN: 네 case가 PHP, TypeScript, Go TestAddColumns*, Rust add_columns*에서 MySQL, PostgreSQL, SQLite로 두 번 통과하고, 설치된 core, member, page, board 매니페스트에 대한 계획은 MySQL, PostgreSQL, SQLite에서 아무것도 추가하지 않고 차이를 찾지 않는다.[o]
T7.17.2.4네 클라이언트에서 제한된 불변 물리 그래프를 해석한다(T7.17.2.1–3 필요). 원인: 독립적으로 유효한 객체만으로 참조 소속과 고유성을 증명할 수 없다.—근거: 네 언어의 API 누락·PHP 128MiB 초과·Go 네이티브 정수 버전 거부가 Red에서 Green이다. 공개 physical-graph-check가 공통 그래프 28개·개수/바이트 제한 5개를 언어마다 두 번 실행했고 기존 컬럼 25개·FK 26개 회귀도 통과했다. 정확한 형태·전역 ID/이름·순서 있는 소속 해석·순환/자기/공유 참조·값 없는 위치 오류·분리된 스냅샷을 검증한다. 각 언어에서 연결된 테이블 2000개·컬럼 60000개·FK 10000개를 보관한다. PHP 프로세스 할당 최대값은 기존 128M 제한에서 62MiB이며 RSS나 다른 언어의 메모리 측정은 아니다. macOS arm64에서 소유 Rust 1.98.1 Clippy·명시한 Node 26.10.0의 TypeScript 컴파일·영한 기록·diff 검사가 통과했다. 물리 임포트·전체 물리 스키마·DDL·DB conformance는 완료하지 않는다.[o]
T7.17.2.3물리 FK를 보존하고 제한된 객체 검증을 공통으로 사용한다(T7.17.2.1/T7.17.2.2 필요). 원인: 논리 관계가 독립 FK 이름·컬럼 쌍 순서·변경/삭제 동작·지연 조건을 잃는다.—근거: 네 언어의 API 누락 Red가 Green이다. 반복 가능한 physical-fk-check가 FK 26개와 기존 컬럼 25개 벡터를 Go/PHP/Rust/TypeScript마다 두 번 실행했고 sparse/별칭 참조·64/65컬럼도 검증했다. 독립 이름·순서 있는 쌍·자기/공유 참조·서로 다른 동작·미지정 상태·옵션을 정확하게 보존하고 모순은 안전하게 거절한다. 컬럼 전용 검증을 공통 객체 모듈로 교체했으며 Rust는 오래된 ColumnError 없이 RecordError를 사용한다. macOS arm64에서 Rust 1.98.1 소유 Clippy·명시한 Node 26.10.0의 TypeScript 컴파일·PHP 문법·영한 체크리스트·문서 규칙·diff 검사가 통과했다. 각 언어가 FK 2000개를 두 번 생성/보관했고 두 번째 실행은 Go 7.36ms·PHP 9.50ms·Rust debug 22.95ms·TypeScript 12.27ms였다. 객체 보관 측정이며 연결 그래프·압축·메모리·DB·다른 플랫폼의 인수 증거는 아니다. 그래프 참조 해석·임포트·DDL·실행은 별도 요구다.[o]
T7.17.2.2네 클라이언트에서 불변 물리 컬럼을 보존한다(T7.17.2.1 필요). 원인: 논리 컬럼이 원래 타입/기본값/생성/옵션을 손실 없이 담지 못한다.—근거: 네 언어의 API 누락 Red·TypeScript 타입 좁히기 컴파일 실패·PHP 참조 공유 런타임 Red가 Green이다. 공개 physical-column-check가 공통 정확 필드/거부 벡터 25개를 Go/PHP/Rust/TypeScript마다 두 번 실행했고 전체/필드/옵션 제한·인코딩·sparse 옵션·입출력 분리도 검증했다. 명시한 Node 26.10.0의 TypeScript 컴파일·소유 Rust Clippy·PHP 문법·영한 체크리스트·문서 규칙·diff 검사가 통과했다. 안정적 ID·원문 SQL·없음/NULL/리터럴/표현식 구분·identity/computed 생성·옵션 순서를 보존한다. 구조적 교환이며 텍스트 해석·SQL 문법 검증·물리 임포트·DB conformance·실행 권한은 아니다.[o]
T7.17.2.1논리 정규화 없이 물리 한정 이름을 보존한다. 원인: 논리 식별자 규칙이 임의의 물리 이름을 표현하지 못한다.—근거: 네 클라이언트의 API 누락과 TypeScript sparse array 거부 Red가 Green이다. 추적되는 physical-identity-check가 공통 벡터 10개를 Go/PHP/Rust/TypeScript마다 두 번 실행해 정확한 키/거부 결과를 검증하고 불변 요소·잘못된 인코딩·UTF-8 바이트 제한도 확인한다. TypeScript 컴파일·소유 Rust Clippy·PHP 문법·영한 체크리스트·문서 규칙·diff 검사가 통과했다. 선택적 catalog/schema/column·필수 table·정확한 대소문자/UTF-8·충돌 없는 이름 키를 정규화 없이 보존한다. 물리 임포트·주석 해석·DDL·마이그레이션·DB 의존 conformance는 활성화하지 않는다.[o]
N9.2.1Send savepoint의 기존 borrowed-Box 경고를 제거한다. 원인: 엄격한 Clippy가 orm/src/tx.rs의 savepoint_send에서 실패한다.—근거: borrowed-Box lint와 추적되는 직접 빌린 callback 컴파일 사례가 수정 전에 실패하고 &SendOperation 및 호출자의 as_ref()로 수정한 뒤 통과한다. 공개 rust-send-savepoint-check가 Rust 1.98.1의 엄격한 라이브러리 Clippy와 MySQL/PostgreSQL/SQLite 소유 테스트 세 개를 각각 두 번 실행했다. Send future·직접 callback 참조·중첩 commit/rollback·원래 callback 오류·트랜잭션 프레임 복구·해제된 savepoint의 외부 rollback·정확한 테스트 행·연결 재사용을 확인했다. 각 테스트의 기한은 10초다. 연결 전용 임시 테이블만 사용하며 사용자 테이블은 건드리지 않는다. 공통 Make 환경이 기존 테스트 진입점에도 SQLite URI를 선언한다. macOS arm64에서 소유 라이브러리/테스트 Clippy·수정 파일 포맷·영한 기록·diff 검사가 통과했다. Rust 구현 수정이며 새로운 트랜잭션 의미나 네 언어의 DB conformance는 아니다.[o]
L1모든 언어에서 .mmd 파일로 schema.json을 빌드한다.—근거: make schema-cross-language-check가 같은 Mermaid 원본을 Go·PHP·Rust·TypeScript로 빌드하고 전체 JSON 매니페스트를 비교해 12개 엔티티와 schema hash 16198b563e2e3cae가 네 출력에서 같음을 보고했다.[o]
L2모든 언어에서 마이그레이션과 import 도구를 제공한다.—증거: Go SQLite 가져오기 테스트가 DECIMALINT(13,4)와 DECIMALINT(16,0)을 논리 decimal 타입으로 정규화하고 manifest를 빌드한다. PHP schema 도구가 SQLite·MySQL·PostgreSQL에서 마이그레이션과 가져오기를 통과한다. Rust CLI 가져오기와 TypeScript SQLite 가져오기 테스트가 decimal 정밀도와 scale을 보존한다. 네 SQLite 가져오기 경로가 같은 논리 매핑을 사용하며 입력을 조용히 버리지 않는다.[o]
T7.3결정적인 ormgen diff와 파괴적 변경 검사를 구현한다.——[o]
T7.5Go, PHP, Rust, TypeScript에 YAML 1.2와 point 변환을 구현한다. MySQL, PostgreSQL, SQLite에서 point DDL과 SQL을 검증한다.——[o]
T7.9같은 SQL, bind, 타입 결과, 연결 수, 픽스처로 Rust mysql_async 0.37.1과 sqlx 0.9를 비교한다. 두 측정 작업 모두 필요한 2배 개선이 없으므로 sqlx를 유지한다.——[o]
T7.10지원 데이터베이스가 같은 안전한 매개변수 실행 구조를 제공할 수 없으므로 모든 공개 API에서 multi_statement를 제외한다. 네 클라이언트의 실행 가능한 제외 검사는 G4에서 다룬다.——[o]
T7.13AES 버전 컬럼을 검증하고, 쓰기 시 현재 버전을 저장하고, Go, PHP, Rust, TypeScript에 상태 조회와 트랜잭션 회전 API를 제공한다.——[o]
T7.14테이블·컬럼 주석을 manifest, 스키마 해시, import, DDL, diff, SQLite 메타데이터에 포함한다.——[o]
T7.15MySQL, PostgreSQL, SQLite에 마이그레이션 실행 잠금, 트랜잭션 범위, 상세 복구 상태를 추가한다.——[o]
T7.16세미콜론 분리를 방언을 아는 SQL 문장 파서로 바꾸고 문장 단위 실패 위치를 유지한다.——[o]
T7.17DDL, diff, 구조화된 계획, 검증, 복구, 멱등 마이그레이션, 검증된 롤백에 MMD, manifest JSON, 메타데이터가 있는 ORM SQL, 라이브 DB 스키마 원본을 받는다.——[o]
T7.17.1공개 라이브러리에 Rust DSN 전용 카탈로그 연결을 노출하고 CLI도 같은 구현을 사용한다.—근거: 모듈 누락 컴파일 Red와 없는 SQLite 파일 생성 Red를 수정했다. 실제 MySQL/PostgreSQL 반복 카탈로그 읽기·SQLite FK/내용 보존을 포함한 공개 API 소유 테스트 4개 및 기존 SQLite CLI 자동 rowid 임포트 회귀가 통과했다. 기존 동작 추출이며 무손실 물리 임포트나 임의 데이터 디코딩 완료가 아니다.[o]
T7.17.1.1공개 데이터 접근 확장 전에 카탈로그/도구 셀의 손실 디코딩을 거부한다. 원인: 미지원 셀은 NULL이 되고 잘못된 UTF-8은 대체되며 unsigned 오버플로가 순환한다. 기준: MySQL/PostgreSQL/SQLite 소유 물리 Red/Green·비밀값 없는 명시적 오류·정상 카탈로그/CLI 경로 유지.—근거: 7개 손실 사례 Red → 세 DB Green; 카탈로그 테스트 4개와 SQLite CLI 자동 rowid 사례 통과.[o]
T7.17.1.2잘못된 숫자/boolean 접근자 기본값을 검증된 결과로 바꾸고 카탈로그/마이그레이션 소유 경로에서 오류를 전달한다. T7.17.1.1 관련 항목. 원인: 잘못된 정수 문자열은 0이 되고 잘못된/NULL boolean은 false가 된다. 기준: 필수 변환은 값을 포함하지 않는 오류를 반환하고 선택적 정수만 NULL을 허용하며 카탈로그·마이그레이션 호출자가 트랜잭션 정리를 유지하면서 오류를 전달한다. 소유 접근자·실제 카탈로그·CLI 회귀 사례 통과.—근거: Result API 누락 컴파일 Red → 접근자 2개 Green(세 DB 포함); 카탈로그 4개·원시 셀 1개·CLI 5개 회귀 통과. 잘못된 SQLite 이력을 값 노출/변경 없이 거부하고 세 DB 계획/적용/롤백/복구 경로를 유지했다.[o]
T7.17.1.3Rust 도구 조회는 전체 행을 먼저 적재하지 않고 스트리밍 중 결과 누적을 제한한다. 원인: 카탈로그/도구 fetch_all 결과 적재에 상한이 없다.—근거: 예산 API 누락 컴파일 Red와 여러 문장 런타임 Red를 수정하고 MySQL prepared unsigned 오버플로 회귀를 재현·수정했다. 조회/카탈로그/접근자 소유 테스트 9개·CLI 11개가 통과했다. 실제 세 DB에서 정확/초과 예산·잘못된 상한·bind·빈 결과·값 없는 오류·연결 재사용을 검증했다. 기본 상한은 100,000행·JSON 결과 64MiB이며 잘린 성공 대신 거부하고 한 문장만 받는다. 누적 출력 제한이며 드라이버 패킷 메모리/DB 실행 제한이나 임의 SQL 결과 타입 완료를 의미하지 않는다.[o]
T7.17.1.4빈 결과와 중복 별칭에서도 순서 있는 제한된 조회 컬럼 메타데이터를 노출한다.—근거: 결과 API 누락 컴파일 Red를 수정하고 실제 MySQL·PostgreSQL·SQLite에서 중복 별칭 순서·빈 결과 컬럼·공개 카탈로그 조회를 검증했다. 소유 통합 테스트 10개·구현 후 메타데이터 예산 단위 회귀 1개·CLI 11개가 통과했다. 예약된 연결 하나에서 prepared 메타데이터와 검증된 행을 제공하며 2,048컬럼·이름/타입 64KiB로 제한한다. SQL 그리드 기반이며 읽기 전용 격리·전체 데이터 타입 완료는 아니다.[o]
T7.17.1.5방언 AST 검증과 DB가 강제하는 읽기 전용 범위 및 drop 시 폐기되는 연결로 제한된 카탈로그 조회를 노출한다.—근거: API 누락 컴파일 Red를 수정하고 소유 통합 테스트 11개·구현 후 정책/해제 단위 회귀 2개·CLI 11개가 통과했다. 실제 세 DB에서 MySQL 함수 내부 쓰기 거부·PostgreSQL/SQLite 읽기 전용 상태·행 보존·조회 오류 뒤 재실행을 검증했다. 이벤트 기반 중단 사례로 연결 풀 재사용 금지를 확인했으며 취소 지연은 미측정이다. 변경/트랜잭션 문장·쓰기 CTE·SELECT INTO·잠금·실행 주석을 거부하고 SQL 원문/오류를 보존하며 파서/방문 깊이를 제한한다. 초기 MySQL 세션 변수 검증 가정을 서버 설정 변경 없이 바로잡고 확인된 실패 테스트 fixture를 정리했다. OS/함수 샌드박스나 전체 SQL 타입 지원은 아니다.[o]
T7.19AES blind-index 스키마 선언, 키 기반 동등 조건, 쓰기 동기화를 추가한다.——[o]
T7.20큰 루트 IN 조건을 나누고, IN이 아닌 매개변수를 유지하고, 행을 합치고, 개수 결과를 더하고, 안전하지 않은 query 형태를 거부한다.——[o]
T7.21soft_delete 스키마 지시어를 추가하고, 읽기와 갱신에 활성 행 조건을 적용하고, 삭제를 시각 갱신으로 바꾼다.——[o]
T7.17.1.6텍스트와 구분하며 바이트를 보존하는 바이너리 셀을 갖춘 제한된 Rust 타입형 읽기 전용 그리드 결과를 추가한다. 원인: 도구 값은 임의 바이트를 거부하거나 UTF-8 바이너리를 텍스트로 해석하므로 SQL 그리드 값을 정확히 표현하지 못한다.—근거: GridCell/API 누락 컴파일 Red가 Green이며 실제 세 DB의 바이너리/NULL/빈 값/텍스트 사례가 통과했다. 구현 후 추가한 예산·변경 정책·빈 메타데이터·bind 회귀도 통과했다. 소유 통합 테스트 12개·CLI 11개·라이브러리 5개가 통과했으며 스트리밍/메타데이터 예산과 읽기 전용 강제/해제는 기존 구현을 공유한다. 숫자/시간 타입·다른 client의 그리드 API는 미완료이며 바이너리 지원만으로 조회 타입을 완료하지 않는다.[o]
T7.17.1.7Rust 그리드의 네이티브 부동소수점 비트와 unsigned 정수 범위를 보존한다. 원인: 엄격한 카탈로그 스칼라 디코딩이 이 조회 값을 거부한다.—근거: 타입 variant 누락 컴파일 Red는 Green이며 실제 세 DB float64·PostgreSQL float32/부호 있는 0/NaN·MySQL u64::MAX 사례가 통과했다. 숫자 NULL·바이트 예산은 추가 회귀다. 통합 13개·CLI 11개·라이브러리 5개가 통과하며 엄격한 카탈로그 의미와 읽기 전용 해제를 유지한다. decimal/시간 타입은 미완료다.[o]
T7.17.1.8실수 변환 없이 유한 Rust 그리드 decimal 정밀도와 선언 scale을 보존한다. 원인: 그리드 값에 decimal 태그가 없고 고정 정밀도 모델 디코딩은 임의 SQL numeric 결과를 표현하지 못한다.—근거: decimal variant 누락 컴파일 Red가 실제 MySQL 65자리·PostgreSQL 끝자리 0 scale·SQLite 네이티브 저장 클래스에서 Green이다. 추가 PostgreSQL 200자리/지수·숫자 NULL·바이트 예산·비유한 거부 회귀가 통과했다. 통합 14개·CLI 11개·라이브러리 5개가 통과했다. BigDecimal 0.4.11을 고정하고 공개 드라이버 디코딩 뒤 PostgreSQL 이진 dscale을 복원하며 값이 달라지는 scale 조정을 거부한다. 비유한 numeric 지원·시간 타입은 미완료다.[o]
T7.17.1.9명시적 테이블 조회/편집을 위해 한정된 Rust 테이블 메타데이터의 선언 기본키 순서·네이티브 타입·nullable·생성 컬럼을 노출한다. 원인: 논리 카탈로그 컬럼 플래그는 복합키 순서를 잃고 신뢰할 행 식별을 확정할 수 없다.—근거: 공개 API 누락 컴파일 Red 후 실제 MySQL/PostgreSQL/SQLite 역순 복합키·생성 컬럼·nullable SQLite 키/rowid 별칭 Green. 추가 뷰·DESC/WITHOUT ROWID 키·타입 없는 컬럼·한정 이름/입력 회귀와 소유 테스트 12개가 통과했다. 메타데이터는 편집 권한이 아니며 변경·낙관적 충돌 검사는 미완료다.[o]
T7.17.1.9.1Rust 카탈로그 API로 한정된 MySQL 시스템 뷰 메타데이터를 읽는다. 원인: 실제 프로세스의 시스템 카탈로그 검사가 실패하며 관계 분류기에 SYSTEM VIEW 종류가 없었다.—근거: 실제 MySQL 네이티브 종류 확인·descriptor 실패 Red 후 신뢰할 행 식별 없는 View 분류 Green. 세 DB 일반 테이블/뷰 사례를 포함한 메타데이터/카탈로그 테스트 6개가 통과했다. 새 시스템 뷰 사례는 DB 내용을 변경하지 않으며 Rust 범위는 네 클라이언트 conformance를 증명하지 않는다.[o]
T7.17.1.10네이티브 타입형 그리드 API로 명시적 한정 Rust 테이블 페이지 조회를 추가한다. 원인: 임의 SQL 결과는 행 편집의 테이블/컬럼/키 출처를 보장하지 않는다.—근거: 공개 API 누락 컴파일 Red 후 실제 MySQL/PostgreSQL/SQLite의 정확한 컬럼·복합키 순서·인용 식별자·다음 페이지 Green. 추가 빈/뷰 페이지·offset/limit 상한·8 MiB 인코딩 초과/복구·변경 descriptor 조립 회귀가 통과했다. 소유 라이브러리/읽기 전용/메타데이터/페이지 테스트 10개 통과. 독립 offset 페이지는 요청 간 고정 스냅샷이나 편집 권한이 아니며 네 클라이언트 conformance는 별도 작업이다.[o]
T7.17.1.11테이블 페이지에서 검증된 불변 Rust 행 편집 기준을 캡처했다.—근거: API 누락 컴파일 Red가 Green이며 기준 테스트 3개와 관련 라이브러리/메타데이터/페이지/읽기 전용 테스트 13개가 통과했다. 실제 세 DB 소유 fixture에서 변경 행 충돌과 정확한 원본 값 복구를 검증했다. 비NULL 기본키 순서와 8 MiB 제한 값별 revision을 유지하고 잘못된 식별/projection을 거부하며 스키마/누락/변경/중복 충돌을 구분한다. Debug/오류에 값을 넣지 않는다. 추가 유효성/타입/한도/실제 DB 검사는 회귀 검사다. 순수 API는 읽기/잠금/쓰기/권한이 아니며 타입형 bind·잠긴 변경 실행·4클라이언트 적합성은 미완료다.[o]
T7.17.1.12네이티브 Rust 타입형 쓰기 bind를 텍스트/정수 이상으로 확장했다.—근거: 타입형 값/NULL API 누락 컴파일 Red가 Green이다. 새 소유 유효성/실제 경로 사례 3개와 라이브러리/네이티브/CLI 테스트 40개가 통과했으며 실제 세 DB 타입형 조회·저장 왕복·지원 NULL·영향 행·거부 복구·거부된 쓰기의 원본 유지·제약 오류·rollback을 포함한다. 추가 경로/타입/상한 검사는 회귀 검사다. SQLite BOOLEAN 기대값은 실제 INTEGER 저장으로 바로잡았고 완료 로그를 조기 성공으로 표시하지 않는다. 미지원 종류/비유한 사례/잘못된 decimal/인수 초과는 prepare/실행 전에 거부한다. 드라이버별 bind 모듈이 조회/실행을 공유하며 SQL 값 서식화는 없다. 테스트 소유 테이블/파일을 제거했으며 잠긴 낙관적 변경·4클라이언트 적합성은 미완료다.[o]
T7.17.1.13소유 트랜잭션에서 호출자가 승인한 네이티브 Rust update/delete·명시적 키 삽입을 실행한다. 원인: 기준/bind만으로 현재 행을 잠그거나 비교하지 못했다.—근거: API/검증 누락 컴파일 Red와 실제 INTEGER 키/commit 거부 Red가 Green이며 macOS의 실제 세 DB를 포함한 소유 라이브러리/기준/페이지/bind/변경 테스트 18개가 통과했다. 원본 잠금·descriptor/값 충돌·인용 복합키·키 변경·생성/default/NULL·영향 행·FK/unique 보존·강제 변환 rollback·취소/중단 정리·대기 중단 후 commit 소유를 검증했다. PostgreSQL 제약 거부는 rollback 확인과 원래 오류를 보존하고 소유 연결 종료는 재시도 없는 Indeterminate로 구분한다. MySQL은 InnoDB·확인된 직접 트리거 가시성·트리거가 없고 partial revoke가 비활성화됐음을 요구한다. 테스트 소유 자원을 제거했다. 추가 수명주기/default/상한 사례는 Red가 아닌 회귀 검사다. 자동 식별자(T7.17.1.13.2)·영속 권한/복구·4클라이언트 적합성·다른 플랫폼은 미완료다.[o]
T7.17.1.13.1PostgreSQL bind 준비와 commit 오류 분류를 수정했다. 실제 INTEGER 키 22P03 및 명시적 지연 제약 거부 Red가 Green이다. 그리드/조회 메타데이터를 선언 codec 타입으로 준비하고 원래 commit 오류를 보존하며 PostgreSQL 제약/트랜잭션 거부의 rollback을 확인하고 소유 연결 종료와 구분한다. 호출자의 대기 중단 후 commit도 관측된다. 소유 테스트 18개와 부모 작업 단위 커밋에 포함한다.——[o]
T7.17.1.13.1.1네이티브 Rust 변경에 실패 가능한 커밋 전 허가를 요구한다. 원인: 실패를 반환할 수 없는 단계 관측자는 영속 의도 기록 실패 시 커밋을 막을 수 없다. 기준: 삽입/갱신/삭제는 쓰기 검증 후 커밋 소유권 이전에 명시적인 동기 허가를 요구하며, 허가 실패는 CommitStarted/Committed 이벤트 없이 롤백한다. 실제 MySQL/PostgreSQL/SQLite에서 허가 실패와 승인된 커밋 소유권을 검증한다. 이 기본 기능으로 기록 구현이나 4개 클라이언트 완료를 주장하지 않는다.—근거: 필수 허가 API 누락 컴파일 Red가 Green이다. 실제 MySQL/PostgreSQL/SQLite에서 거부된 삽입/갱신/삭제가 커밋 시작 없이 원래 행을 유지하며 롤백했고, 잘못된 대입은 허가를 호출하지 않으며 승인된 허가는 커밋한다. 소유 관련 테스트 20개와 Clippy --no-deps가 통과했다. 수정 파일 서식·영한 체크리스트/문서 규칙·새 문서 빌드/정적 검사(42페이지·437대상·26도표)도 통과했다. 기존 네이티브 호출자는 호환 경로 없이 명시적 허가를 요구한다.[o]
T7.17.1.13.1.1.1최종 변경 허가 기록의 표현을 수정한다. 원인: 최종 한국어 근거에 금지 표현이 있어 문서 검사 실패 후에도 커밋됐다. 기준: 해당 표현만 교체하고 동일한 소유 문서 규칙과 영한 체크리스트 검사를 실행하며 실제 수정 이유를 별도로 기록한다.—근거: 동일 문서 규칙이 최종 한국어 기록에서 실패했고 금지 표현 교체 후 영한 21쌍이 통과했다. 영한 체크리스트 검사와 새 정적 문서 검사(42페이지·437대상·26도표)가 통과했다. 네이티브 코드와 기준은 바꾸지 않았다.[o]
T7.17.1.13.2정확한 식별자로 네이티브 Rust 자동/default 기본키 삽입을 지원한다.—근거: 실제 MySQL/PostgreSQL/SQLite 자동·고정 기본값·변하는 표현식 키의 동시 식별자·생성 값·제약/강제 변환 거부·취소 rollback을 실행했다. 소유 테스트 21개와 소유 Clippy가 통과했다. 생성/기본값 descriptor를 보존하며 키를 재평가하거나 추정하지 않는다. MySQL의 모호한 입력 조건은 쓰기 전에 거부한다. 영속 권한/복구·다른 플랫폼·4클라이언트 적합성은 별도 미완료 요구다.[o]
T7.17.1.13.2.1생성 메타데이터와 제한된 서버 반환으로 자동/고정 기본값 키를 지원한다. 자동 키·MySQL 고정 기본값 Red가 Green이며 실제 세 DB 동시성·default-only·제약/강제 변환 거부·취소 rollback을 포함한 소유 테스트 21개가 통과했다. 소유 Clippy --no-deps가 통과했다. 테스트 자원을 제거했으며 다른 플랫폼/4클라이언트 완료는 주장하지 않는다.——[o]
T7.17.1.13.2.1.1자동 삽입 descriptor/엔진 검사 전에 MySQL 대상 메타데이터 잠금을 확보한다. 실제 LOCK TABLES 검사에서 수정 전 잠금이 없음을 재현했고 수정 후 명시적 1초 서버 한도에서 네이티브 오류 번호 1205를 받았다. 테스트의 SQLSTATE/오류 번호 구분을 수정하고 측정한 원래 세션 설정을 복원한다. 소유 테스트 21개와 관련 표현식 키 커밋에 포함한다.——[o]
T7.17.1.13.2.2제한된 트랜잭션 입력 식별 조건으로 MySQL 표현식 기본값 식별자를 반환한다. 누락된 UUID 식별자 Red가 기본값 재평가·스키마 변경·트리거 추가 없이 Green이다. 기존 일치 0개·쓰기 후 정확한 값의 행 1개를 요구하고 실제 반환 키로 재검증한다. 기존 일치 행은 쓰기 전에 거부하고 취소/강제 변환은 원본 저장 행을 유지하며 rollback한다. 실제 세 DB UUID/임의 텍스트 표현식·NULL 조건·동시 요청별 식별자가 통과했다. 동시성 fixture는 분리된 인덱스 간격을 사용하며 보편적인 deadlock 없음은 주장하지 않는다. 검증/RETURNING/식별 조건 모듈과 테스트를 분리했다. 소유 테스트 21개와 소유 Clippy가 통과했고 테스트 자원을 제거했다. 다른 플랫폼·4클라이언트 완료를 주장하지 않는다.——[o]
T7.D1각 영어 페이지 옆에 한국어 .ko.md 페이지를 둔다.——[o]
T7.D2짝 페이지의 제목, 코드 블록, 표, 링크 대상을 맞춘다.——[o]
T7.D3두 경로에 언어 링크와 검색을 제공한다.——[o]
T7.D4비격식, 비유, 의인화, 모호한 설명 문구를 제거한다.——[o]
T7.D7CI에서 짝 문서 구조를 검사한다.——[o]
T7.D8CI에서 설정된 작성 규칙을 검사한다.——[o]
T7.D8.2인터페이스 요건·성능 측정값·검사 기준을 바꾸지 않고 금지 표현을 제거했다.—근거: 수정 전 npm run docs:rules-check에서 파일/표현 실패 10개를 재현했고 수정 후 영한 21쌍이 통과했다. 새 npm run docs:build 뒤 npm run docs:static-check가 42페이지·내부 대상 437개·SVG 26개와 JavaScript 없는 읽기·검색·테마·모바일 탐색을 검증했다. make checklist-check가 통과했다.[o]
T7.D8.1상시 규칙은 AGENTS.md에, 작업 산출물은 이 체크리스트에 둔다. 번호 없는 절차·작업 레인·날짜 고정 상태 서술을 제거하고 체크리스트 검사기가 재도입을 거부하며 기존 항목 ID와 상태를 보존한다.—증거: 정리 전 검사 실패 사례, make checklist-check와 영한 문서 검사 통과, 두 언어 파일과 기록을 포함한 로컬 커밋.[o]
T7.D11갱신된 기능 manifest로 기능 페이지를 다시 생성한다.—근거: make feature-docs가 17개 기능 행을 모두 생성했고 영문·국문 기능 페이지가 최신 상태이며 문서 쌍과 정적 문서 검사가 통과했다.[o]
T7.D12추적되는 모든 한국어 문서를 같은 규칙 구조를 가진 영문 원본의 충실한 번역으로 유지한다. make docs-rules-check는 docs/ 아래 쌍만, 그것도 제목, code fence, 표, 링크만 비교했다. 이제 docs/ 밖의 추적되는 모든 *.ko.md(AGENTS, README, CHANGELOG, CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, clients/rust와 tests 아래 README)도 검사하고, 모든 쌍에서 제목 절마다 목록 항목과 그 깊이를 비교한다. 검사가 찾은 어긋난 번역은 영문 원본에서 바로잡았다: AGENTS.ko.md에는 전체 묶음, 소유 검사, linux-runner 검증 규칙이 없었고, 커밋 메시지 규칙과 테스트 실행 규칙을 합쳤으며 한 문장이 끝나지 않았다. CHANGELOG.ko.md에는 Tx.Local, NewTransactionConflict, Tx.Driver(), PostgreSQL advisory lock, 순서 있는 DDL 설치의 0.0.1 항목이 없었다. README.ko.md에는 네 언어 예제, 빠른 시작 주석, 문서 링크가 없고 절 순서가 달랐으며, README.md에는 한국어 줄이 있었다. docs/codec.ko.md에는 영문 문서에 없는 PHP 배열, 사례, 벡터, 생성 코드 설명이 있었다.—증거: 새 검사와 이전 문서로 make docs-rules-check가 docs.translation-shape 오류 아홉 개로 실패했다. 그중에는 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, docs/codec.ko.md의 세 절이 있다. 이제 영문 문서 34개와 한국어 번역 34개로 통과하고 make docs-check도 통과한다(48 page, 내부 링크 544개). 바뀐 file 이름을 담은 test와 script도 통과한다: make version-check(선언 25개), make repo-check, scripts/features/owners.test.mjs(4/4). 바뀐 path로 make owner-check는 기능 0개를 선택한다.[o]
G0프로세스 내 클라이언트의 오버헤드를 측정하고 perf.md에 기록한다.—근거: 2026-09-27 make perf-check가 CPU 부하가 있을 때와 없을 때 Go·PHP에서 통과했고 PK와 100행의 모든 비율이 기록된 한도 안에 있으며 영문·국문 성능 기록에 관찰값을 남겼다.[o]
G1현재 Go, PHP, Rust, TypeScript conformance 출력을 MySQL, PostgreSQL, SQLite의 기록된 벡터 25개 모두와 비교한다. 각 클라이언트의 실제 실행 두 번에서 결과가 같고 행과 카운터가 변하지 않아야 하며 누락·추가·불일치 벡터나 데이터베이스 상태 변경은 거부한다.—근거: 추적된 사례가 추가된 데이터베이스 기대값과 빈 벡터 집합을 잘못 수용하는 문제를 먼저 재현했고 정확한 벡터 집합 검증 후 통과했다. 격리된 make conformance-check가 클라이언트·데이터베이스 실제 사례 12개를 모두 통과하고 각 데이터베이스의 벡터 25개를 네 클라이언트와 비교했다. 별도 SQLite 실제 실행 두 번의 상태 digest도 같았다.[o]
G1.1.2모든 지원 client와 database의 현재 실행 coverage 보고서, 정확한 사례 ID, 결과와 상태가 같은 두 번의 실행을 요구한다. 변경 반례 테스트는 누락된 구현 선언, 언어 테스트, 데이터베이스 실행, 사례, 반복과 선언되지 않은 결과를 거부한다. 모든 기능이 실행 가능한 coverage 명령을 제공할 때까지 기능 검사는 실패 상태로 유지한다.——[o]
G1.1.2.1scripts/features/coverage.mjs에서 AGENTS.md의 소유자·사용자 테스트 위치 규칙을 강제한다. 변경 반례 테스트는 누락된 소유자·사용자 증거와 중앙 또는 외부 테스트 경로를 RED로 재현하고 유효한 소유자·사용자 실행은 GREEN이어야 한다. make feature-check는 이 검사기에 의존하며 기능 정의에 현재 실행 증거가 없으면 RED 상태를 유지한다.——[o]
G1.1.2.2공통 집계 숫자 사례 아홉 개를 clients/typescript/tests 아래의 TypeScript 소유자 테스트에서 실행한다. coverage 변경 반례는 중앙 테스트 경로를 거부하고, 소유자 사례를 두 번 실행했을 때 사례 ID와 결과가 같아야 하며, 이전 중앙 경로는 제거한다. 기능 테스트 목록 검사는 소유자 .mjs 파일을 찾아야 하며 model_queries 검증은 소유자 테스트를 실행해야 한다. 이 항목은 G1 소유자 coverage의 선행 작업이며 네 언어 데이터베이스 대조를 완료하지 않는다.——[o]
G1.1.2.3남은 모든 TypeScript client 동작 테스트를 clients/typescript/tests 아래에 두고 이전 중앙 경로를 제거한다. 추적된 목록 테스트는 이동 전 실패하고 이동 후 통과해야 하며 모든 테스트의 명령, 기능 선언, 문서는 현재 소유 경로를 사용해야 한다. 소유자 단위 사례와 모델·스키마 도구·SQLite 사례를 필요한 데이터베이스에서 각각 제한 시간을 두고 실행한다. 중앙 conformance 대조는 tests/conformance에 유지한다.——[o]
G1.1.2.4만들어 낸 JSON 성공 보고서를 출력하는 테스트 명령을 거부한다. 선언된 각 언어의 테스트 파일과 정확한 사례 필터를 실행하고 종료 상태와 관찰한 테스트 이벤트에서 현재 결과를 만들며 데이터베이스 실행 전후 상태를 검사기에서 직접 읽는다. 추적되는 가짜 보고 변경 사례가 수정 전에는 실패하고 수정 후에는 통과해야 한다. 소유자와 사용자의 테스트는 각자 부분에 둔다. 시간제한을 둔 검사기 테스트를 실행하고 남은 기능 coverage 결함을 기록한다.——[o]
G1.1.2.5기능 coverage 명령에서 공통 사례 ID와 Go·Rust의 실제 테스트 심볼을 분리한다. 누락되거나 추가·중복되거나 존재하지 않는 심볼 연결을 거부하고 선언한 실제 테스트 통과 이벤트를 확인한 뒤 공통 ID를 기록한다. 선택한 데이터베이스와 DSN을 각 소유 테스트에 전달한다. 변경 반례의 RED와 실제 실행의 GREEN으로 기준을 검증하고 dsn_connection의 소유자 및 실제 사용 부분 사례만 MySQL, PostgreSQL, SQLite에서 같은 결과와 불변 상태로 두 번씩 등록한다. 관련 없는 기능 선언은 자체 증거가 생길 때까지 RED로 유지한다.—근거: 검사기 변경 전에 실제 심볼 사례가 실패했고 변경 후 검사기 테스트 10개가 모두 통과했다. 격리된 dsn_connection 검사에서 소유자 24회와 생성 모델 사용 부분 12회가 통과했고 각 데이터베이스에서 두 번의 결과와 상태가 동일했다. 전체 기능 검사는 선언되지 않은 coverage 종류 16개 때문에 계속 RED이며 기존 기능 목록 오류는 main에서도 재현된다.[o]
G1.1.1Rust 행 값 디코딩 테스트를 model_queries에 등록해 기능 검증에 포함한다. N5.1.2.2는 이 등록에 의존한다.——[o]
G4생성 심볼, 스키마, CI 검사를 실행한다. Go, PHP, Rust, TypeScript의 각 공개 API에 multi_statement가 없고 호출 시 인터페이스 검사에 실패하는지 검증한다.—근거: make interface-check가 Go 2,827개, PHP 1,080개, Rust 2,054개, TypeScript 3,111개 실제 심볼을 검사하고 각 언어의 공개 선언 및 호출 변경 사례를 거부했다. make schema-check가 스키마 사례 159개와 현재 변경 계획을 검증했다. CI 명령 변경 테스트가 생성 모델, 인터페이스, 스키마 검사 각각의 제거를 거부했다. PHP 생성 모델은 연결 전 실제 multi_statement(true) 호출을 CONFIG로 거부했다. 표적 검사기, 생성기, 문서, 규칙 및 체크리스트 검사가 통과했다.[o]
G4.1네 클라이언트의 getsCount가 선택한 그룹 값과 검증한 row_count를 전용 그룹 결과로 반환하게 한다. 잘못된 개수를 거부하고 일부 필드만 채운 모델을 만들지 않는다.—근거: G4.1.1, G4.1.2, G4.1.4가 MySQL, PostgreSQL, SQLite의 소유 동작을 검증했고 공통 출력 및 네 공개 서명을 현재 심볼과 대조했으며 모델 컬렉션으로 되돌리는 변경은 논리 기준 테스트에서 실패했다. make interface-check가 Go 2,827개, PHP 1,080개, Rust 2,054개, TypeScript 3,111개 심볼을 검사하고 각 언어의 소스 변경 반례 일곱 개를 거부했다.[o]
G4.1.1Go 런타임과 생성 모델에서 전용 그룹 결과를 반환한다. 선택한 값의 타입, 잘못된 개수, 조회하지 않은 필드, MySQL, PostgreSQL, SQLite의 실제 결과를 검증한다.—근거: Go 그룹 결과 단위 사례가 누락·중복·잘못된 형식·불리언·음수 개수를 거부했고 생성 모델과 SQL 예약어 소유 사례가 MySQL, PostgreSQL, SQLite에서 선택 값과 개수를 검증하며 통과했다. Go runner 컴파일, 생성기 테스트, 체크리스트 및 문서 규칙 검사도 통과했다.[o]
G4.1.2PHP와 TypeScript의 getsCount에서 전용 GroupRows를 반환한다. 각 행에는 선택한 그룹 값과 검증한 음수가 아닌 row_count만 담고 개수가 누락·중복되거나 불리언·손실·잘못된 값이면 실패한다. 불리언과 SQL NULL을 포함한 선택 값의 선언된 타입을 보존하고 일부 필드만 채운 모델은 만들지 않는다. 생성 모델은 결과 타입을 노출한다.—근거: 소유 사례가 먼저 그룹 결과 타입 누락으로 실패한 뒤 PHP 단위 사례와 TypeScript 빌드·단위·타입 사례가 통과했다. make group-rows-physical-check의 PHP·TypeScript MySQL·PostgreSQL·SQLite 사례 12/12가 통과했고 각 사례 뒤 상태 해시가 동일했다. 문서·용어·체크리스트 검사도 통과했다.[o]
G4.1.3조회하지 않은 PHP 일반 컬럼을 읽을 때 COLUMN_UNSELECTED로 실패한다. 선택한 SQL NULL과 대입한 값은 유지한다. 원인: readColumn이 로드된 행에 없는 컬럼에 null, 0, false, 빈 문자열 또는 다른 타입 영값을 반환했다.—근거: 소유 RED 사례가 기본값 반환을 재현했고 수정 뒤 PHP 모델 10개 사례가 SQLite·MySQL·PostgreSQL에서 통과했으며 생성 모델 사례가 각 데이터베이스에서 두 번 실행되어 상태 해시가 유지됐다.[o]
G4.1.4Go 또는 Rust의 GroupRow.Value가 그룹 결과에 없는 이름을 요청하면 COLUMN_UNSELECTED를 반환한다. 선택된 SQL NULL은 존재하는 값으로 구분한다.—근거: G4.1.4.1과 G4.1.4.2가 각각 이전 반환형을 RED로 재현하고 소유 테스트를 통과했으며 MySQL, PostgreSQL, SQLite에서 생성 모델 경로를 검증했다.[o]
G4.1.4.1Go GroupRow.Value가 선택하지 않은 이름에 오류를 반환하고 선택된 SQL NULL을 구분한다.—근거: 소유 테스트가 반환형 변경 전 실패하고 변경 후 통과했으며 생성 모델과 SQL 예약어 소유 사례가 MySQL, PostgreSQL, SQLite에서 COLUMN_UNSELECTED를 확인하며 통과했다.[o]
G4.1.4.2Rust GroupRow::value(name)에 요청한 이름이 없으면 COLUMN_UNSELECTED를 반환한다. SQL NULL을 포함한 선택 값은 유지한다.—근거: 소유 테스트는 기존 Option API에서 컴파일 실패하고 Result<&Val> 변경 뒤 1/1 통과했으며 생성 모델 columns_and_subqueries가 SQLite·MySQL·PostgreSQL에서 각 두 번 통과했다. 라이브러리와 integration 표적 Clippy, Rust 서식, 영한 문서 규칙, 체크리스트 검사도 통과했다.[o]
G5GitHub Actions 빌드를 검증한다. 원인: 기록된 최신 ci.yml 실행은 make test-servers로 서버를 시작했지만(G5.1, G5.2), workflow의 Node 22.16.0이 node:sqlite에 대해 출력한 ExperimentalWarning을 coverage checker가 test 출력으로 거부해 make feature-check에서 실패했다. G5.3과 G5.4가 workflow를 선언한 Node, PHP, Rust로 실행하게 하고 G5.5가 make check의 모든 target을 실행하게 하지만, 로컬 전용 checkout에서는 새 실행을 위해 현재 커밋을 게시할 수 없다.—증거: G5.24의 ci.yml 실행(ubuntu-26.04-arm의 job test)은 make test-servers로 서버를 시작하고 features/dsn_connection/dsn-php-without-mysql을 포함한 모든 target이 통과한 make check를 실행해 36분 만에 성공했다. 같은 커밋의 docs-pages 실행도 성공했고 https://polyspec.github.io/orm/ 의 HTTP 상태는 200이다.[o]
G5.1.github/workflows/ci.yml이 로컬 검사와 같은 정의인 make test-servers로 database 검사의 서버를 시작하게 한다: MySQL과 PostgreSQL primary와 replica, ProxySQL, PgBouncer, 그리고 .runtime/servers/env의 모든 DSN이다. make repo-check는 scripts/test-servers.sh가 쓰는 변수 하나라도 제공하지 않거나, 그 변수를 직접 정의하거나, 서버 시작 전에 검사를 실행하는 workflow를 거부한다.—근거: workflow 변경 전 make repo-check는 새 case the CI workflow provides every variable of make test-servers에서 실패했고, workflow가 빠뜨린 변수 여덟 개(ORM_TEST_MYSQL_REPLICA_DSN, ORM_TEST_POSTGRES_REPLICA_DSN, ORM_TEST_PROXYSQL_DSN, ORM_TEST_PGBOUNCER_DSN, ORM_TEST_PGBOUNCER_SINGLE_DSN, ORM_TEST_MYSQL_TLS_* DSN 세 개), 직접 정의한 여덟 개, 직접 쓴 환경 파일을 보고했다. 이제 workflow는 MySQL 8.4.11, PostgreSQL 17, PgBouncer, ProxySQL 3.0.9를 설치하고 make test-servers를 실행한 뒤 변수 16개를 $GITHUB_ENV에 더한다. make repo-check는 node case 7/7과 repository case를 통과하고 TestCIRequiresGeneratedChecks도 통과한다. 바뀐 path 일곱 개로 실행한 make owner-check는 feature 0개를 선택한다. 프로그램 설치와 runner에서의 서버 시작은 GitHub Actions 실행(G5)만 보일 수 있다.[o]
G5.2.github/workflows/ci.yml에서 symbolic link 없이 MySQL 8.4.11을 설치한다: Oracle Ubuntu 24.04 deb bundle의 package 다섯 개를 apt로 설치해 libaio1t64 같은 의존성을 apt가 설치하게 하고, make test-servers가 PATH에 있는 package 경로 /usr/sbin/mysqld에서 mysqld를 찾게 한다. make repo-check는 symbolic link를 만드는 workflow를 거부한다.—근거: 새 규칙을 넣자 make repo-check가 G5.1 workflow에서 the CI workflow provides every variable of make test-servers case로 실패했고 ci.yml step "programs of the database servers" creates a symbolic link를 보고했다. mutation case는 ln -s, ln -sf, ln --symbolic, symlinkSync를 거부한다. 이제 workflow는 mysql-server_8.4.11-1ubuntu24.04_amd64.deb-bundle.tar의 mysql-common, mysql-community-client-plugins, mysql-community-client-core, mysql-community-client, mysql-community-server-core 8.4.11을 설치한다. 이 package들의 control과 data는 /usr/sbin/mysqld, /usr/bin/mysql_tzinfo_to_sql, /usr/bin/mysqladmin과 의존성 libaio1t64를 선언한다. workflow는 PATH의 mysqld가 /usr/sbin/mysqld이고 version이 8.4.11인지 확인한다. make repo-check는 node case 8/8과 repository case를 통과하고, 바뀐 path로 실행한 make owner-check는 feature 0개를 선택한다. runner에서의 설치는 GitHub Actions 실행(G5)만 보일 수 있다.[o]
G5.3검사와 모든 GitHub Actions workflow를 .node-version에 한 번 선언한 Node release 하나(로컬 검사의 release인 26.8.1)로 실행한다: ci.yml과 docs-pages.yml은 22.16.0과 24 대신 node-version-file: .node-version으로 이를 읽는다. make repo-check는 workflow가 node-version을 직접 선언하거나, .node-version을 읽지 않거나, actions/setup-node 없이 Node를 실행할 때, .node-version이 정확한 x.y.z 하나가 아니거나 engines.node보다 낮을 때, 실행 중인 Node가 그와 다를 때 실패한다. engines.node >=22.16.0은 TypeScript client가 지원하는 최저 release로 남고 make ts-min-check가 이를 실행한다. coverage checker는 계속 예상하지 않은 출력을 거부한다.—증거: Node 22.16.0에서 node --test --test-name-pattern='checker reads physical database state' scripts/features/coverage.test.mjs가 그 실행처럼 sample/owner/typescript/sqlite run 1: unexpected test output: (node:70028) ExperimentalWarning: SQLite is an experimental feature로 실패했고, Node 26.8.1에서는 경고 없이 통과한다. .node-version과 이전 workflow로 make repo-check가 the checks and every workflow run the Node of .node-version에서 두 workflow의 node-version을 지목하며 실패했다. 이제 node case 13/13과 repository case를 통과하며, Node 22.16.0에서 node scripts/repo/check.mjs는 Node 22.16.0 runs the checks; .node-version declares 26.8.1로 실패한다. Node 22.16.0에서 make ts-min-check는 335 case를 통과한다. 바뀐 path 열두 개로 make owner-check는 기능 0개를 선택하고 make docs-check는 통과한다. runner에서 make feature-check의 나머지와 이후 단계는 GitHub Actions 실행(G5)만 보여 줄 수 있다.[o]
G5.4G5.3이 Node에 하듯 검사와 모든 workflow를 각각 한 번 선언한 PHP release 하나와 Rust toolchain 하나로 실행한다. .php-version은 로컬 검사의 release(8.5.10)인 PHP 8.5를 선언하고 ci.yml은 8.4 대신 php-version-file로 이를 읽는다. setup-php와 Homebrew의 php가 patch release를 고르므로 선언은 x.y다. rust-toolchain.toml은 clippy와 rustfmt를 포함한 channel 1.98.1을 선언한다. 로컬 make target은 PHYSICAL_RUST_TOOLCHAIN ?= 1.98.1로 이 toolchain을 썼다(rustup 기본값 1.98.0은 make 밖의 cargo만 실행했다). 이제 Makefile은 그 channel을 읽고, ci.yml은 dtolnay/rust-toolchain@stable 대신 rustup toolchain install로 이를 설치한다. clients/php/composer.json의 require.php >=8.4는 PHP client의 최저 release로 남는다: CHECK_TARGETS에 넣은 새 make php-min-check가 scripts/php/php-min.sh가 찾은 PHP(PATH의 php8.4나 Homebrew php@8.4 keg이며 8.4인지 확인한다)로 client-unit-check test를 실행하고, ci.yml은 이를 위해 php8.4-cli를 설치한다. Rust 최저 version은 선언되어 있지 않다(rust-version 없음). rust-150-check는 생성 model 150개를 compile하며 최저 version 검사가 아니다. make repo-check는 workflow가 php-version을 직접 선언하거나 setup-php 없이 PHP를 실행할 때, .php-version이 require.php 이상의 x.y 하나가 아닐 때, 실행 중인 PHP가 다를 때 실패한다. 또 workflow가 Rust toolchain을 직접 고르거나(dtolnay/rust-toolchain, rustup default, rustup toolchain install <name>, cargo +<name>, RUSTUP_TOOLCHAIN) rustup toolchain install 없이 cargo를 쓸 때, Makefile이 rust-toolchain.toml을 읽지 않고 toolchain을 선언할 때, channel이 clippy와 rustfmt를 포함한 x.y.z 하나가 아닐 때, 실행 중인 rustc가 다를 때 실패한다.—증거: 새 file과 이전 workflow로 node scripts/repo/check.mjs가 ci.yml declares php-version itself, does not read php-version-file .php-version, chooses a Rust toolchain with dtolnay/rust-toolchain, does not install the toolchain of rust-toolchain.toml with rustup toolchain install로 실패했고, 규칙은 이전 Makefile을 Makefile does not read PHYSICAL_RUST_TOOLCHAIN from rust-toolchain.toml과 declares a Rust toolchain itself로 거부한다. 이제 make repo-check는 node case 18/18과 repository case를 통과한다(Node 26.8.1, PHP 8.5 and rustc 1.98.1 inspected). make php-min-check는 PHP 8.4.25에서 통과하고(PASS 31, FAIL 0), make client-unit-check와 make rust-fmt-check도 통과한다. 바뀐 path 열네 개로 make owner-check는 기능 0개를 선택한다. runner에서 setup-php가 .php-version을 읽는지, apt가 php8.4-cli를 설치하는지, rustup이 1.98.1을 설치하는지는 GitHub Actions 실행(G5)만 보여 줄 수 있다.[o]
G5.5.github/workflows/ci.yml이 make test-servers 뒤에 로컬 검사의 runner이며 자기 bench와 decimal database를 쓰는 make check를 실행해 CHECK_TARGETS의 모든 target을 한 번씩 실행하게 한다. workflow는 target 43개 가운데 17개를 따로 된 step에서 실행하고 26개를 빠뜨렸다: 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와 dbspec-* target 15개다. 그 17개의 따로 된 step은 없앴다. make check 뒤의 step(fuzz, Go, interface, state contract, codec vector, hot-path regression 검사)은 if: ${{ !cancelled() }}로 실행해 한 번의 실행이 모든 결과를 보인다. make repo-check는 workflow가 CHECK_TARGETS의 target을 빠뜨리거나 make check와 함께 그 target을 다시 실행하면 실패한다.—증거: 새 규칙과 이전 workflow로 node scripts/repo/check.mjs가 빠진 target마다 하나씩 ci.yml does not run <target> of CHECK_TARGETS 오류 26개로 실패했다. 이제 통과하며 make repo-check는 node case 20/20과 repository case를 통과한다. Ruby YAML.load_file이 두 workflow를 parse한다. 바뀐 path로 make owner-check는 기능 0개를 선택한다. runner에서의 make check는 GitHub Actions 실행(G5)만 보여 줄 수 있다.[o]
G5.6Apple container 없이 한 경로로 make php-without-mysql-check를 실행한다: scripts/php-without-mysql.sh는 clients/php/tests/sqlite_without_mysql.php를 php -n과, client가 SQLite에서 쓰는 확장(pdo, pdo_sqlite, openssl, zlib, ctype, mbstring) 가운데 PHP에 compile되지 않은 것마다 -d extension=으로 실행하고, PHP가 mysqlnd, pdo_mysql, mysqli를 compile해 두면 그 이유로 실패한다. PHP case는 셋 가운데 하나라도 load되면 실패한다. 이 검사는 .github/runner의 Linux runner에 속한다: Ubuntu 26.04 arm64용 setup-php PHP 8.4 build에서 mysqlnd.so, pdo_mysql.so, mysqli.so, pdo.so, pdo_sqlite.so, ctype.so, mbstring.so는 /usr/lib/php/20240924의 shared module이고, Homebrew PHP 8.5.10과 8.4.25는 mysqlnd, pdo_mysql, mysqli를 compile해 두며(php -n -m), 이전 검사의 공식 image php:8.5.10-cli-trixie도 mysqlnd를 load한다. contracts/features.json의 그 검증은 environment: linux-runner를 선언한다: Linux가 아닌 machine의 make check는 RUNNER features/dsn_connection/dsn-php-without-mysql: … runs on the Linux runner ubuntu-26.04-arm; CI runs it there를 출력하고 통과로 세지 않으며, make repo-check는 .github/runner가 Linux runner이기를 요구한다. 지금까지의—증거: PATH에 container가 없으면 이전 script는 exec: container: not found(exit 127)로 실패했다. Homebrew PHP를 쓰는 macOS에서 새 script는 /opt/homebrew/bin/php compiles mysqlnd in, so php -n cannot leave it out; …로 실패한다. 바뀐 path 열두 개로 make owner-check는 dsn_connection을 선택해 coverage와 검증을 통과하고 RUNNER 줄을 출력한다. make repo-check는 통과한다. Linux: ubuntu-26.04-arm에서 G5.24의 ci.yml 실행은 php 8.5.11 -n -d extension=pdo -d extension=pdo_sqlite -d extension=ctype -d extension=mbstring을 실행해 php-without-mysql과 features/dsn_connection/dsn-php-without-mysql을 통과했다.[o]
G5.7.github/workflows/ci.yml에서 각 검사를 한 번만 실행한다: make check가 feature-check에서 contracts/features.json의 검증 명령으로 PATH="$HOME/.cargo/bin:$PATH" go run ./tests/interfaces/check --self-test와 ./scripts/perf-test.sh를 실행하므로, common interfaces(make interface-check)와 hot-path regression gate(make perf-check) step을 없앴다. make repo-check는 workflow의 줄이 repository root에서 실행하는 검증 명령과 같거나, 줄이 실행하는 CHECK_TARGETS 밖 make target의 모든 recipe 명령이 그런 명령일 때 실패한다(make의 $$와 $(NAME)은 shell의 $와 $NAME으로 읽고 앞의 $(WITH_TEST_ENV)는 뺀다). 규칙은 명령 전체를 비교하므로, 서로 다른 두 명령이 함께 고르는 test는 남는다: Go step의 go test ./...와 codec vectors의 Go와 Rust 줄은 feature 검증 명령이 다른 filter로도 실행하는 test를 실행한다.—증거: 새 규칙과 G5.5의 workflow로 node scripts/repo/check.mjs가 ci.yml step "common interfaces" runs make interface-check, whose commands make check runs in feature-check와 hot-path regression gate의 같은 오류로 실패했다. 이제 통과하며 make repo-check는 node case 22/22와 repository case를 통과하고, Ruby YAML.load_file이 workflow를 parse한다. 바뀐 path로 make owner-check는 기능 0개를 선택한다. runner에서의 workflow는 GitHub Actions 실행(G5)만 보여 줄 수 있다.[o]
G5.8모든 workflow job을 .github/runner에 한 번 선언한 runner ubuntu-26.04-arm에서 실행한다. 각 설치 step이 그 runner에서 유효하게 한다. ci.yml: MySQL 8.4.11은 Oracle amd64 bundle 대신 Ubuntu 26.04 package mysql-server-core와 mysql-client-core(8.4.11-0ubuntu0.26.04.1, /usr/sbin/mysqld, mysql_tzinfo_to_sql, mysqladmin 포함)에서 설치하고 Ver 8.4.11로 확인한다. PostgreSQL 17과 PgBouncer는 arm64용 postgresql-17 17.11을 가진 resolute-pgdg의 PostgreSQL apt repository에서 설치한다. ProxySQL 저장소에는 resolute가 없으므로 ProxySQL 3.0.9는 sha256 e12b2721…eb2a0f로 확인한 proxysql_3.0.9-ubuntu24_arm64.deb에서 설치한다. runner에서 그 의존성이 충족되고 풀어낸 binary가 실행된다(ProxySQL version 3.0.9-618-g7ddb3dc). PHP용 SQLite 3.46.1 build는 없앴다: image의 PHP는 system SQLite 3.46.1을 link하므로 step은 PHP가 3.46 이상을 보고하는지만 확인한다. make php-min-check의 PHP 8.4는 ubuntu-26.04-arm에서 8.4를 지원하는 두 번째 shivammathur/setup-php step이 scripts/php/php-min.sh --version에서 step php-min이 쓴 version을 읽어 설치한다. .php-version의 step이 마지막에 실행해 PHP 8.5가 php가 된다. static-php-cli common의 PHP 8.4.23 build는 쓰지 않는다: linux-aarch64와 macos-aarch64에서 모든 client가 요구하는 3.46보다 낮은 SQLite 3.45.2를 link하고(dsn.php가 CAPABILITY_UNSUPPORTED: SQLite 3.45.2 is older than 3.46로 실패한다), setup-php가 설치하는 PHP 8.4 build는 같은 release URL에서 교체되므로 checksum으로 고정할 수 없다. docs-pages.yml의 두 job도 같은 runner에서 실행한다. make repo-check는 job이 .github/runner와 다른 runner에서 실행하거나 runner를 선언하지 않을 때, setup-php step이 그것을 쓰는 step php-min 없이 steps.php-min.outputs.version을 읽을 때, 최저 release를 두 번 설치할 때, .php-version의 step이 마지막 setup-php step이 아닐 때 실패한다.—증거: .github/runner와 함께 규칙은 이전 workflow를 ci.yml runs on ubuntu-24.04; .github/runner declares ubuntu-26.04-arm과 docs-pages.yml의 두 job에 대한 같은 오류로 거부한다. make repo-check는 node case 25/25와 repository case를 통과하고, Ruby YAML.load_file이 두 workflow를 parse하며. 바뀐 path로 make owner-check는 기능 0개를 선택한다. 설치, setup-php 8.4, 서버는 ubuntu-26.04-arm에서의 실행만 보여 줄 수 있다.[o]
G5.9TestCIRequiresGeneratedChecks(tests/interfaces/check/prohibited_test.go)가 G5.5와 G5.7의 workflow를 검사하게 한다. test는 G5.5와 G5.7이 make check로 바꾼 step run: make go-model-check, run: make ts-model-check, run: make interface-check를 요구했다. G5.5와 G5.7의 commit은 이 test를 실행하지 않고 만들어졌고, go test ./tests/interfaces/check는 CI must run make go-model-check로 실패했다. 이제 test는 ci.yml이 make check를 실행하고, CHECK_TARGETS에 go-model-check와 ts-model-check가 있으며, contracts/features.json이 make interface-check의 명령인 go run ./tests/interfaces/check --self-test를 검증하는지 요구한다. 넷을 하나씩 없애는 mutation은 실패해야 한다.—증거: go test ./tests/interfaces/check -count=1은 변경 전 실패하고 변경 후 통과한다. 바뀐 path로 make owner-check는 기능 0개를 선택한다.[o]
G5.10make test-servers의 모든 MySQL 서버를 build마다 다른 compile 기본값 대신 로컬 검사의 값인 --secure-file-priv=NULL로 시작한다. Ubuntu 26.04의 mysqld는 기본값이 mysql-server package만 만드는 directory /var/lib/mysql-files인데 workflow는 mysql-server-core를 설치하고, Homebrew build의 기본값은 NULL이다.—증거: Ubuntu 26.04 ARM64 machine에서 T40의 실행이 make test-servers에서 mysqld: Error on realpath() on '/var/lib/mysql-files' (Error 2 - No such file or directory)로 실패했다. 로컬에서 mysqld --secure-file-priv=/var/lib/mysql-files는 같은 오류와 [MY-010095] Failed to access directory for --secure-file-priv로 시작하지 못하고, 그 뒤에 --secure-file-priv=NULL을 주면 시작해 @@secure_file_priv를 NULL로 보고한다. G5.10, G5.11, G5.12를 담은 작업 트리의 실행은 모든 서버를 시작하고 .runtime/servers/env를 썼다(test-servers: started). 바뀐 path로 make owner-check는 기능 0개를 선택하고 make repo-check는 통과한다.[o]
G5.11.runtime/servers 아래의 Unix socket 경로가 platform 한도(Linux 107 byte, macOS 103 byte)를 넘으면 make test-servers가 서버를 시작하기 전에 경로, 길이, 한도를 말하며 실패하게 한다: test-servers: socket path <path> is <N> bytes; <system> allows at most <limit>. socket 경로 다섯 개(MySQL, MySQL replica, ProxySQL admin interface 둘, ProxySQL PostgreSQL interface)는 한 번 선언한다. 긴 worktree 경로 같은 깊은 checkout은 서버가 시작된 뒤 MySQL log 줄 The socket file path is too long (> 107)로 실패했다.—증거: scripts/repo/check.test.mjs의 새 case test-servers.sh refuses a socket path longer than the platform limit는 script를 깊은 checkout 아래에 복사해, 모든 socket이 긴 경우와 proxysql-admin-pgsql.sock만 긴 경우를 실행한다. 이전 script로는 mysqld를 시작한 뒤 Failed to start mysqld daemon과 start failed; logs are in …으로 실패했고, 이제 정확한 메시지와 .runtime/servers 없이 85 ms에 통과한다. make repo-check는 통과하고 바뀐 path로 make owner-check는 기능 0개를 선택한다.[o]
G5.12make test-servers에서 ProxySQL과 PgBouncer의 출력을 도착하는 대로 한 줄씩 읽는다. start_logged는 출력을 awk로 복사하며 준비 줄을 기다렸는데, Ubuntu의 awk인 mawk는 pipe를 큰 block으로 읽어 서버가 끝날 때까지 줄을 넘기지 않는다. 그래서 Linux runner에서 ProxySQL 시작은 실행을 3981.5 s 뒤 멈출 때까지 기다렸고 PgBouncer도 같았다. 이제 read를 쓰는 sh loop가 각 줄을 log에 복사하고 준비 줄을 알린다. FIFO 읽기는 빨리 끝나는 서버의 종료가 open을 끊으면(macOS /bin/sh의 Interrupted system call) 다시 연다.—증거: Ubuntu 26.04 machine에서 (echo ready; sleep 5) | awk '{ print "got " $0; fflush() }'는 5 s 뒤에야 got ready를 출력하고, ProxySQL은 6 s 안에 file로 log 101줄을 쓰며, G5.10과 G5.11을 담은 T40의 실행은 .runtime/servers/proxysql.log 없이 ProxySQL에서 기다렸다. scripts/repo/check.test.mjs의 새 case start_logged reports a ready line while the server keeps running은 script의 함수를 준비 줄 뒤에도 실행하는 서버와 먼저 끝나는 서버로 실행해 36 ms에 여섯 번 연속 통과한다. G5.10, G5.11, G5.12를 담은 작업 트리의 실행은 ProxySQL과 PgBouncer를 시작하고 .runtime/servers/env를 썼다. make repo-check는 통과하고 바뀐 path로 make owner-check는 기능 0개를 선택한다.[o]
G5.13모든 서버 시작과 모든 CI step에 자기 기한을 둔다. scripts/test-servers.sh의 start_logged는 서버마다 선언한 기한을 받는다(몇 초 안에 준비 줄을 쓰는 ProxySQL과 PgBouncer는 60 s). timer가 같은 FIFO로 알리며, 기한을 넘긴 시작은 test-servers: <name> logged no line with '<line>' within its deadline of <N> s; last lines of <log>:와 log의 마지막 20줄로 실패하고, 끝난 서버도 마지막 줄을 보인다. .github/workflows/ci.yml과 docs-pages.yml의 모든 step은 timeout-minutes를 선언한다. 값은 ubuntu-26.04-arm(4 vCPU, 16 GB)에서의 작업 트리 실행을 기준으로 한다. 그 실행에서 make check는 1150.1 s, 다른 step은 모두 84 s 미만이었고, cold cache와 download를 위한 여유를 더했다: make check 90, Go와 codec vector 30, 설치 10–20, 나머지 2–15. make repo-check는 양의 timeout-minutes가 없는 workflow step에서 실패한다.—증거: G5.12 이전 실행은 기한 없이 database servers에서 3981.5 s 기다렸다. case start_logged reports a ready line, an exit or its deadline는 이제 조용한 서버를 2 s 기한 뒤 정확한 메시지로 실패시키며 2.1 s에 통과하며, 여섯 개씩 동시에 24번 실행해도 통과한다(timer는 자기 sleep만 멈추므로 shell이 멈춘 job을 보고하지 않는다). 이전 script로는 실패한다. 새 규칙과 함께 make repo-check는 <workflow> step "<step>" has no timeout-minutes of its own 오류 27개로 실패했고 이제 통과한다. Ruby YAML.load_file이 두 workflow를 parse한다. 바뀐 path로 make owner-check는 기능 0개를 선택한다.[o]
G5.14문서 build를 green으로 유지한다: docs/checklist.md와 한국어 쌍의 T40 항목이 code 밖에 <kind> <table>[.<name>]과 <nil>을 써, VitePress가 이를 닫히지 않은 HTML element로 compile했다. 그래서 make docs-check와 make docs-verify-idempotent가 로컬과 Ubuntu 26.04 ARM64 machine에서 docs/checklist.md (209:2878): Element is missing end tag로 실패했다. 이제 두 메시지는 inline code이고, make docs-rules-check는 docs/ 아래 page의 code 밖에 생성 feature page가 쓰는 <br> 외의 element가 있으면 docs.element: <file> writes <tag> outside code; put it in inline code로 실패한다.—증거: 새 규칙과 이전 checklist 쌍으로 make docs-rules-check가 docs.element 오류 여덟 개(각 언어의 <kind>, <table>, <name>, <nil>)로 실패했다. 이제 통과하고, make docs-check(48 page, 내부 링크 546개)와 make docs-verify-idempotent도 통과한다. 바뀐 path로 make owner-check는 기능 0개를 선택한다. 바뀐 문서에 대해 소유 검사가 문서 검사를 실행하게 하는 일은 T41이 한다.[o]
G5.15--offline으로 실행하는 cargo 명령보다 먼저 clients/rust와 bench/rust의 crate를 받는다. 새 target rust-fetch는 두 lockfile에 대해 cargo fetch --locked를 실행하고, recipe에서나 scripts/decimal-physical-check.mjs, tests/timing/preempted.test.mjs를 통해 cargo를 offline으로 실행하는 target 14개가 이를 선행 조건으로 둔다. target은 더 이상 앞선 target이 cargo cache를 채웠는지에 기대지 않는다.—증거: 새 Ubuntu 26.04 ARM64 machine에서 make rust-driver-check가 19 ms에 error: no matching package named mysql_async found(location searched: crates.io index, offline)로 실패했다. 로컬에서 빈 CARGO_HOME으로 bench/rust의 cargo test --locked --offline이 같은 오류로 실패하고, 이제 빈 CARGO_HOME으로 make rust-driver-check가 통과한다(PASS rust-fetch elapsed=12.701s, 이어서 0, 1, 4, 2개 test 통과). make repo-check와 TestCIRequiresGeneratedChecks는 통과하고 바뀐 path로 make owner-check는 기능 0개를 선택한다.[o]
G5.16Go, PHP, Rust, TypeScript의 primary와 replica case에서 MySQL replica가 primary의 binary log 위치에 닿기를 10 s 대신 60 s까지 기다린다. replica는 make client-db-check가 병렬로 실행하는 모든 client의 쓰기를 차례로 적용하므로, 4 vCPU Linux runner에서 PHP case가 10 s 대기 뒤 the replica did not reach binlog.000001:358010684로 실패했다. 이 기한은 다른 process가 하는 일의 제한이므로 wall-clock 시간이며, 각 대기 옆의 주석에 이유를 적었다.—증거: ubuntu-26.04-arm에서의 작업 트리 실행이 그 오류로 client-db-check에 실패했다. 바뀐 test file 네 개로 make owner-check는 기능 아홉 개(dsn_connection, model_queries, model_writes, transactions, schema_install, composite_keys, authenticated_encryption, parameter_chunking, audit_triggers)를 선택해 검증과 coverage 단계 18/18을 통과한다. gofmt, rustfmt, php -l, node --check도 통과한다.[o]
G5.17검사의 PHP를 로컬 PHP처럼 debugger와 coverage driver 없이 실행한다. ci.yml의 두 shivammathur/setup-php step은 coverage: none을 두고, make repo-check는 그것이 없는 setup-php step에서 실패한다(<workflow> sets up PHP without coverage: none, so Xdebug or pcov can load). clients/php/tests/perf_gate.php는 Xdebug나 pcov가 load되어 있으면 clients/php/tests/perf_extensions.php의 perfGateRefusedExtensions로 측정을 거부한다(perf_gate: this PHP loads <extension>; …). 그 case perf_extensions_test.php는 make client-unit-check에서 실행한다.—증거: ubuntu-26.04-arm에서의 작업 트리 실행은 PHP 8.5.4와 8.4.26에 Xdebug 3.5.3을 load했고, PHP hot-path regression 검사는 비율 1.64(pk, 한도 1.35)와 1.28(list100, 한도 1.25)을 측정했다. 새 규칙과 함께 make repo-check는 ci.yml에서 두 번 실패했고 변경 뒤에는 통과하며, 값이 없거나 xdebug인 경우도 검사한다. perf_extensions_test.php는 2/2 통과한다(로컬 PHP는 두 확장을 모두 load하지 않으므로 거부 자체는 여기서 실행하지 않았다). 바뀐 path로 make owner-check는 performance_gate를 선택해 검증과 coverage를 통과하고, make client-unit-check, make repo-check, TestCIRequiresGeneratedChecks도 통과한다.[o]
G5.18ORM_TEST_MYSQL_SERVER_DSN과 ORM_TEST_POSTGRES_SERVER_DSN을 검사가 읽는 변수의 유일한 정의인 make test-servers의 environment file에 정의한다. Makefile의 WITH_TEST_ENV만 이를 정했으므로, $GITHUB_ENV로 environment file을 읽는 CI step go test ./...가 Linux runner에서 TestRollbackFailed{MySQL,Postgres}와 TestSavepointRollbackFailed{MySQL,Postgres}를 ORM_TEST_MYSQL_SERVER_DSN is required로 실패했다. write_env가 file을 쓰고, WITH_TEST_ENV는 더 이상 두 변수를 정의하지 않으며, start는 실행 중인 서버에 대해서도 file을 다시 써서 기존 file이 정의를 따르게 한다.—증거: 이전 environment file로 go test ./clients/go/orm -run TestRollbackFailedMySQL이 그 메시지로 실패했다. make test-servers 뒤 file은 두 변수를 한 번씩 정의하고(두 번 정의한 변수 없음), file만 load해도 rollback test 네 개가 통과한다. 바뀐 path로 make owner-check는 기능 transactions와 make target 24개를 고른다. case-database-check를 빼고 모두 통과했다. case-database-check의 TypeScript case model/postgres/schemaEmpty는 idle pool connection의 처리되지 않은 terminating connection due to administrator command로 끝났고, 다시 실행하자 통과했다. 이 간헐적 실패는 이 변경 때문이 아니며 별도 항목으로 기록한다.[o]
G5.19GitHub이 실행하는 대로 build, test, smoke stage를 갖도록 docs-pages.yml에 명시적인 build step을 둔다. 이 job은 docs/.vitepress/dist를 게시하지만, 그것을 쓰는 step은 Build twice and compare every static file(make docs-verify-idempotent)뿐이었다. 이 step은 test step이다. workflow는 artifact를 만들므로 skip을 선언하지 않는다: Build the static site가 make docs-build를 실행하고, idempotence 검사가 두 번 더 build해 모든 byte를 비교하며, Smoke test of the built site in a browser가 upload 전에 결과물에 make docs-static-check를 실행한다.—증거: docs-pages.yml은 이제 build, test, smoke stage를 가진다. Ruby YAML.load_file이 workflow를 parse하고, 바뀐 path로 make owner-check는 checklist-check, version-check, repo-check, docs-rules-check, docs-check, docs-verify-idempotent를 골라 통과한다.[o]
G5.20모든 MySQL test 서버를 scripts/test-servers.sh에 한 번 선언한 lower_case_table_names=1로 실행하고, 기존 data를 반복 가능한 절차로 옮긴다. MySQL은 data directory를 초기화할 때 파일 시스템에 따라 값을 고르는데(macOS 2, Linux 0) make test-servers가 이를 정하지 않았으므로, Linux runner에서 dialect-facts-check가 got "0"; want "2"로 실패했다. 1은 두 platform이 모두 받는 유일한 값이다. make test-servers는 모든 mysqld에 --lower-case-table-names=1을 주고 .runtime/servers/mysql.settings를 쓴다. 서버가 이미 실행 중이면 scripts/test-servers-mysql.mjs가 선언한 값을 marker와, marker가 없으면 실행 중인 primary와 비교한다. 같으면 아무것도 바꾸지 않고, 다르면 migration 단계를 실행한다: preflight(그 값이 합칠 이름이 있으면 바꾸기 전에 목록과 함께 거부), dump(system이 아닌 모든 database와 routine, trigger, event, 사용자와 grant, database별 수), stop, keep(이전 data directory를 .runtime/mysql-before-<stamp>로 옮기고 지우지 않음), start, restore, verify(primary와 replica를 기록한 수와 비교), record. 각 단계는 RUN, PASS나 FAIL, 경과 시간, 기한을 가진다. 진행을 기록하므로 실패 뒤의 실행은 이어서 하며 새 data directory만 다시 만든다. tests/dialects/probes_mysql.go와 docs/spec/dialects.md의 MySQL 사실은 1을 적는다. 새 target test-servers-check(CHECK_TARGETS와 그 입력에 포함)는 scripts/test-servers-mysql.test.mjs를 실행한다: platform 기본값으로 초기화한 서버는 같은 수로 옮겨지고 두 번째 실행은 unchanged를 출력하며, 겹치는 이름은 Linux에서 migration을 거부하고 macOS에서는 서버 자체가 그런 쌍을 거부한다(1050).—증거: 로컬 서버로 make dialect-facts-check가 got "2"; want "1"로 실패했다. test-servers-check는 macOS에서 4/4 통과한다. 로컬 서버에서 make test-servers는 database 19개와 충돌 없음을 확인하고, 수 161개(그중 table 44개와 trigger 63개를 가진 database 하나, orm_bench table 12개, dbspec_sub_probe table 40개, 사용자 1명)를 기록하고, 약 33 s에 옮겨 primary와 replica에서 161개 수를 모두 확인했으며, 이전 data를 .runtime/mysql-before-20261004-121451에 보관했다. 두 번째 make test-servers는 lower_case_table_names=1 unchanged를 출력했다. 두 서버는 lower_case_table_names 1과 secure_file_priv NULL을 보고하고 make dialect-facts-check는 통과한다.[o]
G5.21PostgreSQL 서버가 pool에서 기다리거나 pool이 닫는 중인 연결을 끝내도 TypeScript client가 계속 실행되게 한다. pg pool은 그런 연결의 서버 메시지를 pool의 error event로 알리고 그 연결을 뺀다. client에 listener가 없었으므로 Node가 Unhandled 'error' event로 process를 끝냈다. 그 때문에 case-database-check가 한 번 model/postgres/schemaEmpty에서 pool의 idleListener가 낸 terminating connection due to administrator command(57P01)로 끝났다. Db.close()는 pg-pool 3.14가 idle 연결을 닫기 시작했을 때, 곧 그 socket이 닫히기 전에 끝나고, case는 이어서 자기 database를 WITH (FORCE)로 지운다. 서버가 drop을 먼저 처리하면 아직 닫히는 중인 연결이 끝난다. PostgreSQL log에는 21:32:29.621의 그 종료와 이어지는 drop의 checkpoint가 있다. 이제 PostgresPoolDriver는 pool의 error event를 받는다. 그 연결에서는 statement가 실행되지 않으므로 오류를 받을 호출자가 없고, 다음 statement는 새 연결을 연다. MySQL pool은 같은 종료를 이미 견딘다.—증거: 새 case clients/typescript/tests/idle-connection-end.mjs(npm run typescript:test와 dsn_connection test에 포함)는 새 case database에 idle pool 연결 하나를 둔 child process를 실행해, 서버가 그 연결을 끝내게 하고(PostgreSQL은 pg_terminate_backend, MySQL은 KILL), 서버가 session을 닫을 때까지(KILL은 그 전에 끝나므로 MySQL 쪽은 PROCESSLIST에 그 session이 없어질 때까지) 그리고 event loop 한 바퀴를 기다린 뒤 statement를 다시 실행하고 닫는다. 변경 전 PostgreSQL child는 매번 status 1과 Unhandled 'error' event로 끝났고 MySQL child는 통과했다. 이제 둘 다 통과하며 최소 Node 버전에서 40회 중 40회 통과한다. 기다리지 않으면 MySQL statement가 pool이 끝난 연결을 빼기 전에 그 연결에 닿아 30회 중 4회 PROTOCOL_CONNECTION_LOST나 ECONNRESET으로 실패했다.[o]
G5.22cargo가 build한 모든 program을 Makefile이 선언하고 export하는 CARGO_TARGET_DIR에서 실행한다. 검사들은 clients/rust/target/...을 직접 적었다: Makefile target dbspec-apply-pairs-check, dbspec-introspect-compare-check, dbspec-stress-bench, example-check, timing-check와 stress 문서 경로, contracts/features.json의 chunking-rust 명령, scripts/case-database-check.mjs, scripts/client-db-test.sh, tests/dbspec/compare/runners.mjs, tests/dbspec/inputs.test.mjs다. scripts/check-rust-150.sh와 scripts/package-check.sh는 선언한 값을 $ROOT/clients/rust/target으로 덮어썼고, tests/conformance/check는 값이 없으면 clients/rust/target을 썼다. 다른 target directory, 예컨대 worktree가 main checkout과 함께 쓰는 directory에서는 cargo가 그곳에 build하고 검사는 다른 곳을 찾았다(clients/rust/target/debug/integration: No such file or directory, spawn clients/rust/target/debug/examples/dbspec_compare ENOENT). 이제 모든 경로는 Makefile의 $(CARGO_TARGET_DIR), shell의 ${CARGO_TARGET_DIR:?…}, Node의 tests/cargo-target.mjs cargoTarget(), Go의 CARGO_TARGET_DIR에서 나오며, 값이 없으면 directory를 짐작하지 않고 실패한다. make repo-check는 Makefile, contracts/features.json, workflow, scripts/, tests/의 실행 명령이 clients/rust/target을("clients", "rust", "target" 형태도) 적으면 거부한다. 주석과 선언 자체는 제외하고, 문서와 예제 주석은 기본 위치를 설명할 수 있다.—증거: 함께 쓰는 target directory로 T41.1의 worktree 소유 검사가 그 오류로 chunking-rust와 schema-compare에 실패했다. 새 규칙과 함께 make repo-check는 clients/rust/target을 적은 17줄로, 이전 tests/conformance/check/main.go에서는 그 fallback 줄로 실패했다. 이제 통과하고, 바뀐 path로 make owner-check는 고른 기능과 make target을 통과한다.[o]
G5.22-1Rust test의 package directory를 cargo가 compile할 때가 아니라 test가 실행될 때 읽는다. 원인: clients/rust 아래 Rust file 24개의 31줄(integration test와 orm/src/codec.rs, orm/src/engine/mod.rs의 unit test)이 fixture와 저장소를 env!("CARGO_MANIFEST_DIR")로 찾았고, 이 값은 test를 compile한 checkout의 절대 경로를 binary에 넣는다. worktree는 main checkout의 CARGO_TARGET_DIR에 build하고(G5.22), cargo는 workspace source를 상대 경로로 기록해 checkout만 다를 때 binary를 다시 build하지 않으므로, main checkout은 지워진 worktree orm-test-design-G5.29가 build한 test binary를 실행했다. 마지막 make check는 client-db-check와 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), feature-check(schema_definition, authenticated_encryption, interface_contract의 Rust coverage)에서 실패했고, 그 worktree를 적은 줄이 56개였다. 이제 orm_testcase::manifest_dir()가 cargo test가 실행에 준 CARGO_MANIFEST_DIR를 읽고 값이 없으면 panic하며, 그런 줄은 모두 이것을 쓴다. Rust test binary를 직접 실행하는 scripts/features/coverage.mjs는 cargo test처럼 각 binary에 그 crate의 package directory를 CARGO_MANIFEST_DIR로 준다. make repo-check(scripts/repo/target.mjs의 manifestDirErrors)는 주석 밖에서 env!("CARGO_MANIFEST_DIR")를 읽는 Rust 줄을 실패시킨다. CARGO_BIN_EXE_*와 OUT_DIR는 공유 target directory 자체를 가리키므로 그대로 둔다.—증거: 새 repository case는 manifestDirErrors가 없을 때 실패했고, 그다음 node scripts/repo/check.mjs가 31줄을 보고했다. 실행 시점의 CARGO_MANIFEST_DIR를 확인하는 coverage runner test의 Rust probe는 left: Err(NotPresent)로 실패했고, runner가 값을 주기 전 owner check는 Rust coverage 단위 7개를 CARGO_MANIFEST_DIR is unset으로 실패했다. 이제 repository 실행 둘이 통과하고, coverage runner test가 통과하며, 공유 target directory로 make rust-fmt-check rust-check가 통과하고, 바뀐 path 34개로 make owner-check의 단계 22개가 통과한다.[o]
G5.23TestWithContextCancelsInsideTransaction(clients/go/orm/cancel_test.go)의 막힌 읽기를 고정된 300 ms 뒤가 아니라 서버가 그 statement의 lock 대기를 보고할 때 취소한다. MySQL과 PostgreSQL driver는 취소된 statement의 연결을 닫으므로 test는 ROLLBACK을 기대하지만, 부하가 크면 timer가 statement가 서버에 닿기 전에 울렸다. 그러면 database/sql은 연결을 건드리지 않고 취소를 돌려주고 rollback은 성공하며 결과는 CANCELED가 된다. 그 때문에 owner check 한 번에서 case database 생성이 15 s 걸리는 동안 client-db-check가 두 PostgreSQL case에서 blocked read: CANCELED: context canceled (code "CANCELED"), want ROLLBACK으로 실패했다. 이제 test는 case database의 서버 session에서 대기를 조회하고(MySQL은 LOCK_STATUS = 'WAITING'인 performance_schema.data_locks, PostgreSQL은 wait_event_type = 'Lock'인 pg_stat_activity) 대기하는 session이 있으면 취소한다. 10 s 안에 대기가 없으면 test가 실패한다.—증거: statement 앞에 1 s 지연을 넣으면 이전 test는 네 case 모두 그 message로 실패하고 새 test는 통과한다. 지연 없이 go test -run '^TestWithContextCancel' -count=3이 통과한다.[o]
G5.24signal이 start_logged(scripts/test-servers.sh)의 read를 끊어도 처음 보고를 잃지 않는다. 이 함수는 read마다 새로 여는 FIFO에서 기다렸다. background job이 끝나면 SIGCHLD가 오고 그 signal이 read를 끊을 수 있다(EINTR). 끊긴 read가 FIFO를 닫은 뒤에 쓴 보고는 읽는 쪽이 없어 사라졌다. 그러면 서버의 exited 대신 기한의 timeout을 읽었고, writer 둘이 모두 사라지면 함수가 끝없이 기다렸다. 그 때문에 make repo-check가 따로 실행한 한 번에서, case가 fake exited before it logged 'accepting connections'를 기대하는 곳에서 fake logged no line with 'accepting connections' within its deadline of 2 s로 실패했다. 이제 함수는 FIFO를 읽기와 쓰기로 한 번 열고 읽는 동안 열어 둔다. 그래서 보고는 다음 read가 가져갈 때까지 FIFO buffer에 남는다. timer는 기한에 반드시 쓰므로 반복은 끝난다.—증거: scripts/repo/check.test.mjs의 start_logged case를 병렬로 72회(24회씩 3번) 실행하면 변경 전에는 32회 실패했다. 28회는 종료 대신 기한 message였고 4회는 20 s spawn 한도까지 shell이 기다렸다. 변경 뒤에는 72회 모두 통과한다.[o]
G5.25conformance check의 PostgreSQL sequence를 pg_depend에서 읽은 소유 table로 되돌린다. tests/conformance/check/counters.go의 restoreCounters는 sequence 이름 <table>_seq_seq에서 table을 얻었지만, PostgreSQL은 sequence 이름을 63 byte로 자르므로 긴 table 이름에서는 다른 이름이 나와 그 sequence를 선언되지 않은 것으로 거부했다. 이제 readCounters는 sequence마다 pg_depend로 소유 table을 읽고(serial column은 deptype a, identity column은 i) restoreCounters는 그 table을 선언한 write table과 비교한다. 소유 table이 없는 sequence가 바뀌면 owned by no table로 거부한다.—증거: 새 case TestPhysicalPostgresCounterOwner(conformance-counter-check에 포함)는 case database에 serial sequence를 PostgreSQL이 conformance_counter_owner_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_seq_seq로 짓는 63 byte table, identity table task, 선언되지 않은 table, 소유 table 없는 sequence를 만든다. 변경 전에는 undeclared postgres counter changed: "public"."conformance_counter_owner_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_seq_seq"로 실패했다. 이제 선언한 두 counter는 이전 값으로 돌아가고, 선언되지 않은 table과 소유 table 없는 sequence는 거부된다.[o]
G5.26문서 page의 {{를 쓴 그대로 보이고, page의 server render가 실패하면 make docs-build가 실패한다. 원인: VitePress는 page마다 Vue template으로 compile하고 fenced code에만 v-pre를 붙인다. 그래서 G5.5의 inline code if: ${{ !cancelled() }}가 interpolation으로 실행됐다. checklist page의 server render는 TypeError: _ctx.cancelled is not a function을 log에 남겼고, production Vue build는 처리하지 않은 error를 log에만 남기므로 build는 통과한 채 checklist.html과 ko/checklist.html을 본문 없이 썼다. 이제 docs/.vitepress/config.mts의 markdown.config는 inline code와 indented code에 v-pre를 붙이고 {{를 담은 Markdown text를 v-pre span으로 감싼다. theme의 enhanceApp(docs/.vitepress/theme/index.ts)는 server render에서 app.config.throwUnhandledErrorInProduction을 켜므로, render error가 나면 vitepress build가 exit 1로 끝난다. Red: 변경 전 make docs-build는 TypeError를 두 번 출력하고 exit 0으로 끝났고, build한 checklist.html(21733 byte)에는 G5.5가 없었다. Green: make docs-build는 error 없이 exit 0으로 끝나고 checklist.html과 ko/checklist.html에 <code>if: ${{ !cancelled() }}</code>가 있다. theme 변경만 넣었을 때는 바꾸지 않은 G5.5 때문에 make docs-build가 build error: TypeError: _ctx.cancelled is not a function으로 실패하고 exit 2로 끝났다. scratch 사본에서는 HTML block <div class="scratch">{{ missing() }}</div>이 TypeError: _ctx.missing is not a function으로 build를 실패시켰고, Markdown text {{ missing() }}는 쓴 그대로 render됐다.——[o]
G5.27make owner-check는 바뀐 기능의 검사와 file 단위 target만 실행하고, scripts/features/coverage.mjs --feature <id>는 고를 수 있는 id를 알려 준다. 원인: contracts/check-inputs.json은 CHECK_TARGETS의 모든 make target을 똑같이 다뤘다. 그래서 네 client에 걸친 변경은 client-db-check, client-pooler-check, case-database-check, conformance-check, feature-check(모든 기능의 coverage, 약 6분) 같은 전체 suite target을 target마다 30분 기한으로 골랐다. T42 변경은 약 30분 동안 실행됐고, 이는 항목마다 전체 suite를 실행한 것이다. feature-check는 contracts/**도 맞췄으므로 fixture 하나가 바뀌어도 고른 기능의 검사 옆에서 모든 기능의 coverage가 실행됐다. coverage.mjs의 없는 --feature id는 unknown feature <id>로 실패하고 고를 수 있는 id를 적지 않았다. 이제 contracts/check-inputs.json의 모든 target은 scope를 선언한다: owner(file 단위 검사: format, checklist, 문서, 검사기 unit test, inputs와 함께)나 suite(make check에서만). scripts/features/owners.mjs는 owner target만 고르고, CHECK_TARGETS의 target에 scope가 없거나 owner target에 inputs가 없거나 suite target이 inputs를 선언하면 아무것도 실행하기 전에 실패한다. make repo-check도 같은 오류를 보고한다. contracts/features.json이 바뀌면 HEAD와 항목이 다른 기능을 고른다. make target과 기능 단계는 바깥 기한 없는 group으로 실행하고, 안의 case가 저마다 기한을 가진다. unit test와 feature 문서 검사는 feature-check에서 새 owner target feature-unit-check로 옮겼으므로 make check는 여전히 각각을 한 번 실행한다. AGENTS.md와 한국어 문서는 규칙을 적는다: 항목마다 바뀐 것의 Red/Green 테스트와 make owner-check만 실행하고, make check는 진행 중인 모든 항목이 끝났을 때 한 번, 그리고 CI에서 push마다 실행한다. Red: main에서 node --test scripts/features/owners.test.mjs는 11개 중 5개 case가 실패했다. 네 client에 걸친 T42 모양의 path 9개는 case가 repo-check, go-fmt-check, rust-fmt-check를 기대하는 곳에서 client-db-check와 feature-check를 포함한 target 30개를 골랐고, scope 없는 target은 실패하지 않고 골라졌고, manifestChanges가 없었고, 선언 모양에 대한 case 두 개가 실패했다. scripts/features/coverage.test.mjs에서 없는 id case는 unknown feature missing으로 실패했다. 기능 하나 case(statement_events의 owner spec만 세 database에서)는 --feature가 이미 기능 하나를 골랐으므로 main에서도 통과했다. Green: owners.test.mjs는 11개 모두 통과하고 coverage.test.mjs의 선택 case 세 개도 통과한다. T42에서 문서가 아닌 path 97개에 대한 owners.mjs --list는 target 38개 대신 기능 15개와 owner target 8개를 고른다. client마다 하나씩 statement_events test file 네 개를 준 make owner-check PATHS=는 repo-check, go-fmt-check, rust-fmt-check, statement_events의 검증(11.6 s)과 coverage(44.8 s)를 65 s에 실행했다. 모든 기능의 coverage는 약 6분이다.——[o]
G5.28Rust transaction option timeout_ms를 docs/spec/interfaces.md와 docs/operations/usage.md의 정의대로 만든다: PostgreSQL은 SET LOCAL statement_timeout으로 적용하고 MySQL과 SQLite는 CAPABILITY_UNSUPPORTED를 반환한다. 원인: Rust client는 timeout_ms가 있는 transaction의 callback을 모든 database에서 client timer(tokio::time::timeout) 아래 실행하고, 만료되면 session을 닫거나 SQLite에서는 rollback한 뒤 CANCELED transaction callback timed out을 반환했다. client timer는 server의 statement를 멈추지 않으므로 이 option은 MySQL과 SQLite에서 없는 제한을 있다고 했고, PostgreSQL에서는 timer가 server의 statement_timeout과 경쟁했다. Go, PHP, TypeScript는 BEGIN 전의 같은 검사로 이미 그 정의를 따른다. 이제 clients/rust/orm/src/tx.rs의 begin은 Go의 begin처럼 MySQL과 SQLite에서 양수 timeout_ms에 연결을 얻기 전에 CAPABILITY_UNSUPPORTED transaction timeoutMs is supported only by postgres를 반환한다. Transaction과 SendTransaction에서 timer와 그 cancelled 경로를 지웠고, Rust README는 이 option을 PostgreSQL statement 제한으로 설명한다. transaction option을 다루는 공유 vector는 없다: tests/events/vectors.json은 option 없는 transaction만 실행한다. Red: callback_timeout_cancels_a_statement_and_rolls_back을 대신하는 case tx::tests::transaction_timeout_is_the_postgres_statement_timeout은 이전 code에서 sqlite: got "CANCELED: transaction callback timed out" with callback run true로, MySQL도 같은 결과로 실패했다. 기대는 callback을 실행하지 않은 CAPABILITY_UNSUPPORTED: transaction timeoutMs is supported only by postgres다. 두 번 가운데 한 번은 PostgreSQL도 server의 canceling statement due to statement timeout (SQLSTATE 57014) 대신 timer의 CANCELED: transaction callback timed out을 반환했다. MySQL과 SQLite의 timeout_ms(500)에 이제 CAPABILITY_UNSUPPORTED를 기대하는 Rust integration program의 transactions case는 SQLite에서 left: "ok"로 실패했다. Green: lib case는 네 번 모두 세 database에서 통과한다. PostgreSQL은 server의 57014 오류와 함께 callback을 실행하고, MySQL과 SQLite는 callback을 실행하지 않고 CAPABILITY_UNSUPPORTED를 반환하며, timeout_ms(0)은 세 database 모두에서 commit한다. integration schema/bench.dbs --case transactions는 SQLite, MySQL, PostgreSQL에서 통과한다.——[o]
G5.28-1Rust symbol snapshot에서 clients/rust/orm/src/tx.rs::cancelled를 지운다. 원인: G5.28은 client timer와 그 함수 cancelled를 clients/rust/orm/src/tx.rs에서 지웠지만 contracts/symbols/rust.json에는 그 함수를 남겨, go run ./tests/interfaces/check가 rust: missing clients/rust/orm/src/tx.rs::cancelled로 실패했다. 마지막 make check는 이를 보이지 않았다: Rust coverage test coverage_interface_symbols의 binary가 지워진 worktree에서 저장소를 찾아(G5.22-1) check를 실행하기 전에 실패했고, 그 실패가 feature-check를 검증 명령 interface-self-test 전에 멈췄다. 이제 snapshot에 cancelled 항목이 없고 contracts/interfaces.json의 rust hash는 새 snapshot의 것이다.—증거: 전에는 main checkout에서 go run ./tests/interfaces/check가 interfaces/rust를 그 줄로 실패하고 Go, PHP, TypeScript는 통과했다. go run ./tests/interfaces/check -language rust -record는 그 줄 하나만 지웠다. 이제 go run ./tests/interfaces/check --self-test의 case 여섯이 모두 통과한다.[o]
G5.29모든 check가 AGENTS.md의 test 규칙을 따르게 한다: case마다 자기 기한 아래에서 실행, 단계, 결과와 경과 시간을 보고하고, CI step이나 전체 suite에는 자기 기한이 없으며, 전체 suite는 push마다 한 번 실행하고, make owner-check는 바뀐 file이 소유하는 명령과 coverage만 실행한다. 하위 항목 G5.29-1에서 G5.29-13이 결함 하나씩을 고친다.—증거: branch가 바꾼 path 43개로 make owner-check가 owner make target 11개, helper check 3개, 기능 4개의 검증 명령 18개를 골랐고 단계 18개가 모두 통과했다(가장 긴 것은 testcase-check 1 m 9 s와 perf-gates 검증 52 s).[o]
G5.29-4AGENTS.md의 timeout 규칙을 모순 없이 적는다. 원인: testing 규칙은 모든 test가 자기 timeout을 가진다고 한 뒤 다음 문장에서 장기 작업은 timeout 대신 상세 단계 로그를 둔다고 해, 기한 없는 장기 작업을 허용했다. 이제 장기 작업은 자기 timeout에 더해 상세 단계 로그를 둔다(AGENTS.md와 AGENTS.ko.md). Red: grep -n 'instead of a timeout' AGENTS.md가 53번 줄을, grep -n '타임아웃 대신' AGENTS.ko.md가 44번 줄을 출력했다. Green: 둘 다 아무것도 출력하지 않고 새 문장이 두 file에 있다.——[o]
G5.29-6scripts/version/check.test.mjs의 case를 caseTest로 선언하고, node:test로 test를 직접 선언하는 JavaScript test를 거부한다. 원인: version check test는 node:test의 test를 import해, 다른 *.test.mjs가 tests/testcase.mjs의 caseTest를 쓰는 것과 달리 case 세 개가 자기 기한도, 경과 시간을 담은 RUN, PASS, FAIL 줄도 갖지 않았고, 그런 file을 찾는 check가 없었다. 이제 세 case는 COMPUTE 기한(1분)의 caseTest로 실행한다. 새 scripts/repo/testcases.mjs의 nodeTestErrors는 docs/ 밖의 추적되는 JavaScript file이 node:test의 test, it, describe, suite나 default export를 import하면 실패한다. after 같은 hook은 쓸 수 있고 caseTest를 정의하는 tests/testcase.mjs는 검사하지 않는다. make repo-check가 그것을 실행한다. Red: scripts/repo/check.test.mjs의 새 case every JavaScript test declares its cases with caseTest가 main에서 scripts/version/check.test.mjs declares tests with node:test directly로 실패했다. Green: 그 case와 a JavaScript test that declares tests with node:test directly fails가 통과하고, node --test scripts/version/check.test.mjs는 case 3개마다 RUN과 경과 시간을 담은 PASS를 출력하며, node scripts/repo/check.mjs가 통과한다. check 자신인 make version-check의 node scripts/version/check.mjs는 RUN 줄도 기한도 없이 version: 25 declarations agree on 0.0.2만 출력했다. 이제 COMPUTE 기한의 case version으로 실행해 RUN version deadline=1m0s와 경과 시간을 담은 PASS version을 출력한다.——[o]
G5.29-5네 client의 MySQL TLS test를 자기 기한을 가진 case로 보고한다. 원인: clients/go/orm/mysql_tls_test.go는 testcase.Start를 부르지 않아 TestParseDSNMySQLTLS와 TestMySQLTLSConnection이 RUN 줄도 기한도 없었고, 연결 test는 context 없이 QueryRow로 Ssl_version을 읽었다. Rust mysql_tls.rs test는 orm_testcase case를 시작하지 않았고, clients/php/tests/mysql_tls.php와 clients/typescript/tests/mysql_tls.mjs는 공유 RUN, STEP, PASS, FAIL 줄 대신 자기 START와 PASS 줄을 file 전체에 하나인 60 s alarm과 함께 출력했다. case 없는 Go나 Rust test를 찾는 check가 없었다. 다른 Go와 Rust test는 모두 case를 시작한다. 이제 Go test는 testcase.Start(DSN parse는 Compute, 연결은 Database)를 부르고 case context로 QueryRowContext를 쓴다. Rust test는 orm_testcase::case!(orm_testcase::DATABASE)를 시작하고, PHP와 TypeScript test는 tests/testcase.php와 tests/testcase.mjs로 case mysql_tls/parameters(COMPUTE)와 mysql_tls/connections(DATABASE)를 실행한다. make repo-check가 실행하는 scripts/repo/testcases.mjs의 goTestCaseErrors와 rustTestCaseErrors는 testcase.Start, Group, Run, Of를 부르지 않는 Go Test나 Fuzz 함수(생성기 test가 쓰는 raw string literal 안의 Go source는 건너뛴다)와 case!를 시작하지 않는 Rust #[test]나 #[tokio::test] 함수를 실패시킨다. Red: main의 file로 scripts/repo/check.test.mjs의 새 case every Go and Rust test starts its case with the shared testcase package가 clients/go/orm/mysql_tls_test.go: TestParseDSNMySQLTLS does not start its case와 TestMySQLTLSConnection의 같은 오류로 실패했고, main의 mysql_tls.rs에 rustTestCaseErrors는 Rust test 둘을 돌려주었다. main의 PHP와 TypeScript file은 START php mysql tls: parameters, START php mysql tls: connections, PASS php mysql tls 19.979ms만 출력했다(TypeScript도 같다). Green: 두 repository case가 통과한다. go test -v -timeout 0 ./clients/go/orm -run 'MySQLTLS' -count=1은 RUN TestParseDSNMySQLTLS deadline=1m0s와 RUN TestMySQLTLSConnection deadline=2m0s를 PASS 줄과 함께 출력하고, run-case build 뒤 cargo test --test mysql_tls는 test 2개마다 RUN과 PASS를 출력하며, PHP와 TypeScript file은 RUN mysql_tls/parameters deadline=1m0s와 RUN mysql_tls/connections deadline=2m0s를 PASS와 경과 시간과 함께 출력한다. 모두 make test-servers의 TLS server에 실행했다.——[o]
G5.29-7PHP와 TypeScript의 decimal database test를 자기 기한을 가진 case로 실행한다. 원인: contracts/features.json(기능 model_queries와 model_writes)의 검증 명령 decimal-php-mysql, -postgres, -sqlite와 decimal-typescript-mysql, -postgres, -sqlite 여섯 개는 clients/php/tests/decimal_model_db.php와 clients/typescript/tests/decimal_model_db.mjs를 실행했고, 두 file은 끝에 CASE decimal_<dialect> PASS만 출력했다. RUN 줄도 단계도 경과 시간도 기한도 없어 멈춘 연결은 끝없이 기다렸다. check가 실행하는 PHP나 TypeScript test가 공유 case 보고를 쓰지 않는지 찾는 check가 없었다. 이제 PHP test는 tests/testcase.php로 case decimal_db/<dialect>/generate와 setters(COMPUTE), round_trip(DATABASE)을 실행하고, TypeScript test는 tests/testcase.mjs의 cases()로 setters와 round_trip을 실행한다. 두 file을 함께 실행하는 scripts/decimal-physical-check.mjs는 옛 CASE 줄 대신 이 case마다 PASS 줄이 있고 다른 결과 줄이 없기를 요구한다. make repo-check가 실행하는 scripts/repo/testcases.mjs의 reportingScriptErrors는 Makefile recipe, 검증 명령, scripts/ 아래 script나 root package.json script에서 php나 node가 실행하는 clients/<language>/tests/ 아래 file이 직접이나 require, import한 file을 거쳐 tests/testcase.php나 tests/testcase.mjs에 닿지 않으면 실패시킨다. Red: main의 file로 scripts/repo/check.test.mjs의 새 case every PHP and TypeScript test that a check runs reports its cases가 contracts/features.json runs clients/php/tests/decimal_model_db.php, which reports no case와 decimal_model_db.mjs의 같은 오류로 실패했다. Green: 그 case와 a PHP or TypeScript test without the shared case report fails가 통과한다. .runtime/decimal-env의 decimal database에서 두 file은 MySQL, PostgreSQL, SQLite의 case마다 기한을 담은 RUN과 경과 시간을 담은 PASS를 출력하고(PostgreSQL PHP round trip 1.084 s, 나머지 case는 200 ms 미만), decimal-physical-check.mjs의 observed 함수는 그 출력을 받아들이고 TypeScript 출력을 PHP 출력으로는 거부한다.——[o]
G5.29-8검증 명령의 build와 lint 도구를 tests/run-case.mjs 아래에서 실행한다. 원인: contracts/features.json의 검증 명령 다섯 개가 case를 보고하지 않는 도구를 기한도 RUN 줄도 없이 실행했다: model_queries와 model_writes의 decimal-typescript-types(tsc --noEmit), generation-go(go generate와 git diff), chunking-rust(integration program의 cargo build), conformance-build(go vet). Makefile은 같은 도구를 이미 RUN_CASE로 감싼다. 이제 이 명령은 TOOL_DEADLINE과 BUILD_DEADLINE의 기한으로 node tests/run-case.mjs typescript-types 5m, go-generate/model 5m --cwd clients/go/model, rust-build/integration 8m, go-vet/conformance 5m 아래에서 실행한다. make repo-check가 검증 명령에 실행하는 scripts/repo/testcases.mjs의 unwrappedToolErrors는 따옴표 밖의 &&, ||, ;, |, 괄호에서 나눈 명령 조각이 tsc, TypeScript build, go generate, go vet, go build, cargo build, cargo check, cargo clippy, cargo test --no-run을 그 앞의 run-case.mjs, $(RUN_CASE), $(TSC_BUILD) 없이 실행하면 실패시킨다. Red: scripts/repo/check.test.mjs의 새 case every build tool of a verification command runs under run-case가 main에서 이 다섯 명령으로 실패했다. Green: 그 case와 a build tool outside run-case fails가 통과하고, 감싼 도구 네 개가 기한을 담은 RUN과 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모든 cargo test 실행의 test binary를 실행 전에 기한을 가진 case로 build한다. 원인: cargo test는 test를 실행하기 전에 compile하고, 그 compile은 case를 보고하지 않으며 기한도 없다. Makefile의 testcase-check, dbspec-introspect-rust-check, dbspec-apply-rust-check, dbspec-plan-rust-check, dbspec-rust-check, rust-send-savepoint-check, rust-driver-check, 검증 명령 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, catalog-rust, 그리고 scripts/client-db-test.sh의 Rust lane이 그렇게 실행했다. 이제 실행마다 그 앞에서 같은 인자의 cargo test --no-run을 $(RUN_CASE) rust-build/<target> $(BUILD_DEADLINE)나 node tests/run-case.mjs rust-build/<id> 8m 아래에서 실행한다. catalog-rust는 cd clients/rust 대신 --manifest-path로 workspace를 정한다. make repo-check가 Makefile recipe(makeRecipes), 검증 명령, 그 명령이 run-case 밖에서 실행하는 shell script(reachedScripts)에 실행하는 scripts/repo/testcases.mjs의 unbuiltCargoTestErrors는 -- 앞의 인자(--no-run과 toolchain 제외)를 같은 단위의 앞선 cargo test --no-run이 build하지 않은 cargo test 실행을 실패시킨다. segments는 $(...)를 나누지 않는다. Red: scripts/repo/check.test.mjs의 새 case every cargo test run follows a run-case build of the same test binaries가 main에서 실행 26개(Makefile recipe 줄 11개, 검증 명령 조각 14개, scripts/client-db-test.sh)로 실패했다. Green: 그 case와 a cargo test run without its build fails가 통과한다. dsn-rust는 test case 13개 앞에 RUN rust-build/dsn-rust deadline=8m0s와 PASS(3.6 s)를, make rust-driver-check는 RUN rust-build/rust-driver-check deadline=8m0s와 PASS(29.1 s)를 출력하고, make -n dbspec-rust-check에 --no-run build 두 개가 있다.——[o]
G5.29-10check의 모든 TypeScript build를 tests/run-case.mjs 아래에서 실행한다. 원인: scripts/typescript/sqlite-test.sh(client-db-check의 TypeScript lane이자 npm run typescript:test의 끝)는 기한 없는 build npm run typescript:build >/dev/null을 실행하고 출력을 버렸다. ts-check와 ts-min-check가 실행하는 root package.json script typescript:test는 감싸지 않은 npm run typescript:build로 시작했고, Makefile target conformance-result-check는 RUN_CASE 없이 npm --prefix clients/typescript run build를 실행했다. ts-check, ts-min-check, dbspec target의 다른 TypeScript build는 이미 RUN_CASE나 TSC_BUILD로 실행했다. 이제 세 build는 5분 TOOL_DEADLINE의 typescript-build로 실행하고 build 출력은 STEP 줄로 보인다. make repo-check는 unwrappedToolErrors를 Makefile recipe, 명령이 둘 이상인 root package.json script(명령 하나인 script는 도구를 정의하고 그 호출을 검사한다), check가 run-case 밖에서 실행하는 shell script에도 적용한다. Red: scripts/repo/check.test.mjs의 새 case every build tool of the Makefile, package.json and the scripts runs under run-case가 main에서 이 세 build로 실패했다. Green: 그 case가 통과하고, node tests/run-case.mjs typescript-build 5m -- npm run typescript:build는 RUN typescript-build deadline=5m0s와 PASS(8.5 s)를 출력하며, make -n conformance-result-check는 감싼 build로 시작한다.——[o]
G5.29-1case를 보고하는 suite를 실행하는 CI step에는 자기 기한이 없다. 원인: .github/workflows/ci.yml은 make check를 timeout-minutes: 90 하나 아래에서, Go와 codec step을 각각 30분, make test-servers를 15분, make fuzz-check를 10분 아래에서 실행했고, docs-pages.yml은 make docs-build, make docs-verify-idempotent, make docs-static-check를 10, 20, 15분 아래에서 실행했다. 모두 case마다 이미 기한을 가진 suite 전체에 기한 하나를 거는 것이다. scripts/repo/ci.mjs의 stepTimeoutErrors는 모든 step에 timeout-minutes를 요구해 그 suite 기한을 강제했다. 이제 규칙을 뒤집는다: run text의 명령 조각이 make, go test, cargo test로 시작하는 step(reportsCases)에는 timeout-minutes가 없고, 다른 step(action, 설치, 도구 하나)은 자기 기한을 유지한다. suite step 여덟 개에서 timeout-minutes를 뺐고, ci.yml의 머리 주석이 그 규칙을 적는다. Red: scripts/repo/check.test.mjs의 새 case a suite step has no timeout-minutes and every other step has its own이 main에서 그 여덟 step으로 실패했다. Green: timeout-minutes를 둔 make check, go test, (cd clients/rust && cargo test ...) step과 그것이 없는 도구 step이 실패하는 최소 workflow를 포함해 그 case가 통과하고, node scripts/repo/check.mjs가 통과한다.——[o]
G5.29-2push마다 test를 한 번씩 실행한다: make check 뒤에 test를 다시 실행하던 CI step을 없애고 make check에 없던 것을 CHECK_TARGETS의 target으로 옮긴다. 원인: ci.yml은 make check 뒤에 step Go(go vet ./... && go test -v -timeout 0 ./...)를 실행했고 그 go test는 go-test-check, client-db-check, 기능 명령이 실행하는 것을 되풀이했다. step codec vectors의 go test ./clients/go/orm -run TestCodec과 cargo test -p orm codec도 client-db-check가 실행한다. go vet ./...와 php tests/codec/check.php만 다른 곳에서 실행하지 않았다. make repo-check는 step 줄을 기능 명령과 Makefile recipe의 정확한 text와 비교했으므로 이 재실행이 통과했다. tests/codec/check.php도 다른 codec test가 출력을 쓰지 않았을 때 0 other-language outputs로 통과했다. 이제 go-vet-check(scope owner, 입력 **/*.go, go.mod, go.sum, $(RUN_CASE) go-vet $(BUILD_DEADLINE) 아래의 go vet ./...)와 codec-check(scope suite, php tests/codec/check.php, CHECK_TARGETS에서 ts-check와 client-db-check 뒤)가 make check에서 실행한다. check.php는 tests/codec/out/go.json, rust.json, typescript.json이 없으면 실패하고, CI step 둘은 없앴다. scripts/repo/ci.mjs의 ciRerunErrors는 make check를 실행하는 workflow가 step이나 CHECK_TARGETS 밖 make target의 recipe(변수를 풀어서)에서 test runner를 실행하면 실패시킨다. 판단은 runnerIdentity가 한다: 환경 변수 대입 뒤의 go test(-fuzz로 fuzzing만 하는 것은 제외), go vet, cargo test, php <file>, node --test나 test file을 실행하는 node. Red: scripts/repo/check.test.mjs의 새 case the CI workflow runs no test runner outside make check가 main에서 step Go의 go vet과 go test, step codec vectors의 go test, cargo test, php tests/codec/check.php로 실패했다. tests/codec/out 없이 main의 check.php는 0 other-language outputs of 80 vectors와 PASS를 출력했다. Green: 그 case와 a workflow that runs a test runner after make check fails by identity가 통과한다. 출력 없이 make codec-check는 missing tests/codec/out/go.json(그리고 rust.json, typescript.json)으로 실패하고, TestCodecVectors, Rust codec lib test, codec-vector.mjs 뒤에는 3 other-language outputs of 80 vectors로 통과하며, make go-vet-check가 통과한다.——[o]
G5.29-3make owner-check가 바뀐 file이 입력인 검증 명령과 coverage 단위만 고르게 하고, 공유 helper에 자기 check를 준다. 원인: scripts/features/owners.mjs는 기능의 fixtures나 tests 하나가 바뀌면 기능 전체, 곧 모든 검증 명령과 coverage 전체를 골랐다. 공유 coverage 실행기 clients/php/tests/coverage_cases.php와 clients/typescript/tests/coverage_case.mjs는 기능 18개 가운데 17개와 15개가 적었으므로 그 변경은 거의 모든 기능을 실행했고, 명령 자체가 전체 suite인 것도 있었다: planner-go는 go test ./engine/...(schema-go도 실행하는 engine/dbspec을 포함한 모든 engine package)를 실행했고, schema_definition은 자기 file 하나에도 모든 make dbspec-*-check를 실행했다. 이제 모든 검증 명령은 inputs(실행하거나 읽는 test와 fixture file)를 선언하고, coverage 단위(owner/<language>나 dependent/<id>)는 tests와 선택적 inputs로 고른다. 선언한 JSON이나 dbspec 입력은 그것이 적는 file도 한 단계만 고른다(contracts/symbols/rust.json은 Rust source에 Rust coverage 단위를 고르고, source 주석은 아무것도 고르지 않는다). contracts/features.json의 새 최상위 helpers는 자기 check를 가진 helper 다섯 개를 선언하고, 어떤 명령이나 단위도 helper path를 선언할 수 없다: coverage-runner-php(새 clients/php/tests/coverage_cases_test.php), coverage-runner-typescript(새 clients/typescript/tests/coverage-case-unit.mjs), php-test-autoload(새 clients/php/tests/autoload_test.php), go-test-helpers(run-case 아래 두 Go package의 go vet -tags featurecoverage,ormtest), rust-test-helpers(run-case 아래 workspace의 cargo test --no-run). make check는 feature-check에서 그것을 실행한다. planner-go는 planner-go-engine, -planner, -ir, -plan, -dialect, -runtimemodel로 나누고 dbspec-go-check(schema-go)는 engine/dbspec package 전체를 실행하므로 engine package마다 한 번 실행한다. scripts/features/check.mjs는 --feature 뒤의 --command <id>와 --helper <id>를 받고, 입력이 없는 검증 명령, 추적되는 file을 맞추지 않는 입력, 입력으로 쓰인 helper를 실패시킨다(manifestInputErrors). scripts/features/coverage.mjs는 --part를 받는다. owners.mjs는 고른 명령, 단위, helper만 실행하고 아무것도 고르지 않은 바뀐 path를 모두 출력한다. AGENTS.md와 docs/spec/feature-coverage.md, 그 한국어 쌍이 규칙을 적는다. Red: main의 선택 code와 manifest로 다시 쓴 scripts/features/owners.test.mjs의 case 13개 가운데 8개가 실패했고(예: clients/php/tests/coverage_cases.php가 기능 17개, clients/typescript/tests/coverage_case.mjs가 15개, clients/go/orm/mysql_tls_test.go가 dsn_connection 전체를 골랐다), scripts/features/coverage.test.mjs의 새 case selecting coverage parts requires only those parts가 실패했다. Green: owners.test.mjs가 13개 모두, coverage.test.mjs가 15개 모두 통과한다. mysql_tls_test.go는 dsn-go와 dsn-mysql-tls-go만, engine/ir/operators_test.go는 planner-go-ir만, helper path는 자기 helper만 고른다. 다섯 helper로 check.mjs --run이 통과하고(Rust test build 1 m 20 s, 나머지는 9 s 미만), make dbspec-go-check와 --command planner-go-ir이 통과하며, node scripts/features/check.mjs는 기능 18개의 선언 검사 통과를 보고한다.——[o]
G5.29-11들여 쓴 checklist 하위 항목의 id와 상태도 docs/checklist.md와 한국어 쌍 사이에서 비교한다. 원인: scripts/checklist/check.mjs는 최상위 - [state] ID 줄만 읽고 항목 뒤의 들여 쓴 줄을 모두 이어지는 글로 다루었다. 그래서 완료로 표시된 T27.1이나 G5.29-4 같은 하위 항목은 비교하지 않았고, 한 언어에서 빠지거나 더해진 하위 항목, 다른 상태, 잘못된 상태가 make checklist-check를 통과했다. id 형식도 하위 항목의 -n id를 받지 않았다. 이제 들여 쓴 - [state] ID 줄은 최상위 항목과 같은 id, 상태, 중복, 우회 검사를 받는 항목이고, id에는 -가 들어갈 수 있으며, 다른 들여 쓴 줄은 그대로 이어지는 글이다. Red: scripts/checklist/check.test.mjs의 새 case compares the ids and states of indented sub-items too가 실패했다: 진행 중으로 표시된 영어 A3-2에 대해 완료로 표시된 한국어 하위 항목 A3-2가 오류를 내지 않았다. Green: 그 case가 통과하고(하위 항목의 상태, id, 잘못된 상태, 중복 id를 보고한다) 다른 case 5개도 통과하며, node scripts/checklist/check.mjs가 file마다 들여 쓴 하위 항목 48개를 비교하고 통과한다.——[o]
G5.29-12make check에서 clients/go/model의 생성 검사를 한 번만 실행한다. 원인: make check는 clients/go/model에서 go generate ./와 git diff --exit-code를 두 번 실행했다: CHECK_TARGETS의 target go-model-check, 그리고 exclusive로 표시되어 feature-check가 먼저 혼자 실행하던 model_generation의 검증 명령 generation-go에서다. 이제 generation-go는 go test -v -timeout 0 ./generator -count=1만 실행하고, go-model-check가 하나뿐인 생성 검사로 남는다(contracts/rules.json의 규칙 schema.generated와 TestCIRequiresGeneratedChecks가 그것을 적는다). exclusive field와 그 순서 처리는 scripts/features/check.mjs에서 없앴고, 그 file은 이제 id, command, inputs, cwd, environment 밖의 검증 field를 실패시킨다. make repo-check가 CHECK_TARGETS의 recipe와 검증 명령에 실행하는 scripts/repo/testcases.mjs의 repeatedGenerateErrors는 같은 directory의 go generate가 두 번 이상 실행되면 실패시킨다. generateRuns는 sh -c의 내용, cd, --cwd를 읽는다. Red: scripts/repo/check.test.mjs의 새 case make check runs each go generate once가 이전 tree에서 go generate of clients/go/model runs 2 times in make check: Makefile go-model-check, contracts/features.json model_generation/generation-go로 실패했다. Green: 그 case와 a go generate that make check runs twice fails가 통과하고, check.mjs --run --feature model_generation --command generation-go가 1 m 30 s에 통과하며, node scripts/features/check.mjs가 통과한다.——[o]
G5.29-13모든 go test 실행의 Go test binary를 실행 전에 기한을 가진 case로 build한다. 원인: go test는 test를 실행하기 전에 package와 test binary를 compile하고, 그 compile은 case를 보고하지 않으며 기한도 없다. Makefile recipe 줄 21개(GO_TEST가 go test -v -timeout 0이었고 fuzz-check의 fuzzing 명령 둘도 포함), contracts/features.json의 검증 명령 조각 29개, scripts/client-db-test.sh의 Go lane과 scripts/perf-test.sh가 그렇게 실행했다. 새 tests/go-test.mjs는 같은 package, build tag, build flag에 -run '^$' -count=1을 준 go test를 8분 BUILD_DEADLINE의 case go-build/<packages>로 실행하고(compiler 출력은 STEP 줄, 기한에는 process group을 종료한다), 그 뒤 받은 인자로 go test를 실행한다. GO_TEST는 이제 node tests/go-test.mjs -v -timeout 0이고 검증 명령과 두 script가 그것을 부른다. instrument한 build가 fuzzing 실행에 속하는 fuzz-check는 명령마다 $(RUN_CASE) fuzz/<package> $(BUILD_DEADLINE) 아래에서 실행한다. make repo-check가 변수를 푼 Makefile recipe, 검증 명령, 그 명령이 run-case 밖에서 실행하는 script에 실행하는 scripts/repo/testcases.mjs의 rawGoTestErrors는 tests/go-test.mjs나 run-case 밖에서 실행하는 go test를 실패시킨다. G5.29-2의 runnerIdentity는 tests/run-case.mjs와 tests/go-test.mjs를 거쳐 읽는다. testcase-check의 tests/go-test.test.mjs가 build 인자를 검사한다. Red: scripts/repo/check.test.mjs의 새 case every go test runs through go-test.mjs or run-case가 이전 tree에서 go test 실행 54개로 실패했다. Green: 그 case, a go test without its build case fails, tests/go-test.test.mjs가 통과한다. make dbspec-go-check는 test 앞에 RUN go-build/engine/dbspec deadline=8m0s와 PASS(1.7 s)를, 검증 명령 planner-go-ir은 go-build/engine/ir를 출력하고, make fuzz-check는 RUN fuzz/engine-ir deadline=8m0s와 RUN fuzz/clients-go-orm deadline=8m0s를 PASS(12.7 s, 42.8 s)와 함께 출력한다.——[o]
G5.31PostgreSQL에서 PHP의 모든 statement를 round trip 하나로 보내고, PHP session의 time zone을 startup parameter로 둔다. Cause: clients/php/src/Db.php는 PDO::ATTR_EMULATE_PREPARES를 MySQL에만 켰으므로 PostgreSQL에서 pdo_pgsql은 새 statement text마다 Bind와 Execute 전에 자기 round trip(Parse와 Sync)으로 named statement를 prepare했고, PostgreSQL은 그 Parse를 자기 implicit transaction에서 실행한다. PHP 연결은 요청 하나 동안만 살므로 모든 요청의 모든 statement text가 그 round trip과 transaction을 치렀다. Orm::connect는 또 PostgreSQL 연결마다 event 없는 statement SET TIME ZONE 'UTC'를 보냈다. Red: tests/events/vectors.json의 새 server_transactions는 PostgreSQL case database에서 Go(TestStatementEventsServerTransactions), PHP와 TypeScript(statement events test의 case server_transactions), Rust(statement_events_server_transactions)로 실행된다. 연결의 TimeZone이 startup parameter의 UTC(pg_settings source client)인지 확인하고, 한 연결에서 model_statements의 step(statement event 8개, statement text 5개)을 두 번 실행하는 앞뒤로 pg_locks에서 backend의 local transaction 번호를 읽어 probe 비용을 빼고, 실행 한 번의 transaction을 event 수와 비교한다. 고치기 전 PHP는 실패했다: TimeZone source session, 첫 실행 event 8개에 transaction 13개, 두 번째 실행 8개. TypeScript는 8과 8, Go는 13과 8, Rust는 15와 8이었다. 수정: PostgreSQL에서 PHP client는 Pdo\Pgsql::ATTR_DISABLE_PREPARES를 두므로 pdo_pgsql은 statement와 bind를 unnamed statement 하나(PQexecParams)로 round trip 하나에 보내고, Orm::connect는 -c TimeZone=UTC를 statement_timeout 옆의 startup options에 둔다. Go(pgx runtime parameter timezone), Rust와 TypeScript(options)가 이미 그렇게 한다. emulated prepare는 택하지 않았다: bind를 SQL text에 literal로 써 넣으므로 server의 type 추론이 바뀌고(따옴표 literal은 unknown, 숫자는 integer나 numeric) bytea는 escape된 text로 간다. prepare를 끄면 bind는 server-side parameter로 남는다: pdo_pgsql은 PQprepare에 주던 parameter type과 format을 PQexecParams에 그대로 주므로 server가 같은 type을 추론한다. bytea(NUL byte 포함, PARAM_LOB), boolean(PARAM_BOOL), numeric text, null, bigint, jsonb bind를 가진 statement 하나는 prepare를 켜고 끈 두 모드에서 같은 값을 돌려주었고, cast 없는 bytea bind는 두 모드 모두 type을 추론하지 못해 실패했다. Go와 Rust는 첫 사용의 prepare를 유지한다: pgx와 sqlx는 bind를 describe한 parameter type으로 binary encode하려고 새 statement text를 자기 round trip으로 describe하고, sqlx는 새 결과 column의 origin도 catalog에서 읽는다(Rust 15 = event 8 + prepare 5 + catalog 읽기 2). 그 pool은 연결과 statement를 다음 page를 위해 유지하므로 server_transactions의 first_run_clients는 PHP와 TypeScript만 두고, Go와 Rust의 첫 실행은 비교하지 않고 보고한다. docs/operations/perf.md(F4)와 docs/spec/config.md(+ko)가 PostgreSQL 동작을 적는다. Green: server_transactions가 네 client에서 통과하고 PHP는 TimeZone UTC/client, 두 실행 모두 event 8개에 transaction 8개다. php clients/php/tests/statement_events_test.php는 25 case, php clients/php/tests/model_test.php는 66, php clients/php/tests/dsn.php는 13 case가 통과한다. go run ./tests/conformance/check run -driver postgres는 기록된 PostgreSQL 기대값에 대해 26 vectors × 4 languages identical을 보고한다. php tests/codec/check.php는 Go, Rust, TypeScript codec 출력의 80 vector로 통과한다. PgBouncer를 거친 PHP 연결은 TimeZone UTC와 statement_timeout 5s를 읽는다. 바뀐 path 19개로 make owner-check는 owner make target 10개와 statement_events, interface_contract의 검증 명령과 coverage를 골랐고 14 step이 모두 통과했다.——[o]
G5.30모든 장기 작업에 상세 단계 로그를 두고 기한 없이 실행하며, 기한은 test case에만 있다. 장기 작업은 build, 설치, tsc, go build, go generate, go vet, cargo build, cargo test --no-run, 전체 suite, server나 도구 실행이고, 그 성공과 실패는 관측한 결과와 오류로 판정한다. 하위 항목 G5.30-1에서 G5.30-16이 server 시작과 test case 안의 장기 작업까지 포함해 G5.29와 그 전 항목이 장기 작업에 둔 기한을 되돌린다.—증거: branch와 그 실행 시점 main 사이의 path 44개(branch의 path 30개와 항목 중에 main에 들어온 G5.31의 14개)로 make owner-check가 기능 11개의 검증 명령 75개와 coverage 단위 43개, helper check 2개, make target 12개를 골랐고 단계 36개가 모두 통과했다(가장 긴 것은 owners/make/test-servers-check 1 m 41 s와 owners/model_generation/verification 1 m 37 s). G5.31 위로 rebase한 뒤 node scripts/repo/check.mjs, checklist/check.mjs, docs/rules.mjs, features/check.mjs, git/check.mjs가 통과하고, 두 항목이 함께 바꾼 기능 하나인 statement_events의 check.mjs --run --feature statement_events와 coverage.mjs --feature statement_events가 통과한다. G5.32-1 위로 rebase한 G5.30-11과 G5.30-12의 path 8개로 make owner-check가 make target 7개를 골랐고 7개가 모두 통과했다(test-servers-check 52.9 s, 새 page case를 포함한 docs-check 24.0 s, docs-verify-idempotent 21.9 s). 전체 suite의 ts-check만 실행하는 package-declarations.mjs는 직접 실행해 통과했다. G5.30-14는 MySQL migration의 기한을 없애고, 그 증거는 G5.33에 있다. G5.30-15는 Go checker의 build 기한을 없애고, 그 증거는 G5.33에 있다. G5.30-16에서는 path 14개로 make owner-check가 자기 실행의 database와 함께 기능 1개의 검증 명령 1개와 coverage 단위 4개, make target 7개를 골랐고, 이제 tests/go-run.mjs로 build하는(go-build/bench-install, go-build/bench-seedaes) database 생성을 포함해 단계 11개가 모두 통과했다.[o]
G5.30-1AGENTS.md의 timeout 규칙을 적는다: test case는 자기 timeout을 가지고, 장기 작업은 timeout 대신 상세 단계 로그를 둔다. 원인: G5.29-4는 "모든 test는 자기 timeout을 가진다"와 "장기 작업은 timeout 대신 상세 단계 로그를 둔다"를 모순으로 보았지만 두 문장은 서로 다른 대상인 test case와 장기 작업에 관한 것이다. G5.29-4는 둘째 문장을 "자기 timeout에 더해 상세 단계 로그"로 고쳤고, 그 문장은 틀렸다. 그 뒤 G5.29-8, -9, -10, -13이 build와 도구에 기한을 두어 느리지만 정상인 build가 시계 때문에 실패하게 되었다. 이제 AGENTS.md와 AGENTS.ko.md는 짧은 검증 단위인 모든 test case가 자기 timeout을 가지고, 장기 작업(build, 설치, tsc, go build, go generate, go vet, cargo build, cargo test --no-run, 전체 suite, server나 도구 실행)은 timeout 대신 상세 단계 로그를 두며 출력 없음 기한을 포함해 어떤 기한도 없고, 관측한 결과와 오류로 판정한다고 적는다. Red: main에서 grep -c 'instead of a timeout' AGENTS.md와 grep -c '타임아웃 대신' AGENTS.ko.md가 0을 출력했다. Green: 둘 다 1을 출력하고, 두 file 어디에도 in addition to its own timeout이나 자기 타임아웃에 더해가 없다.——[o]
G5.30-2장기 작업을 기한 없는 단계 실행기로 실행한다. 원인: 자기 case를 보고하지 않는 명령의 실행기는 tests/run-case.mjs 하나였고, 그것은 기한 인자를 요구하며 기한에 명령을 끝냈다. 그래서 check가 감싸는 모든 build, 설치, 도구 실행이 기한을 가져야 했고, 느리지만 정상인 build가 시계 때문에 실패했다. 이제 tests/testcase.mjs의 runLong은 timer 없이 장기 작업을 실행하고 RUN <name> no-deadline, 단계 줄, 경과 시간과 함께 PASS나 FAIL을 출력하며, runGroup은 그 code를 함께 쓴다. tests/run-long.mjs <name> [--cwd <dir>] -- <program> [args...]는 명령 하나를 그것으로 실행한다: 명령의 출력 줄을 오는 대로 STEP 줄로, PASS 앞에 exit 0을 STEP 줄로, 실패면 종료 코드나 signal을 담은 FAIL을 출력한다. 기한 인자는 받지 않고, 명령은 호출자의 process group에 남아 interrupt가 닿는다. Makefile은 그것을 RUN_LONG으로 선언하고, make repo-check는 run-case를 받는 곳에서 그것도 받는다. tests/testcase.test.mjs의 새 case testcase/run-long(testcase-check)은 출력 줄 사이에 1.5 s를 쉬는 느리지만 정상인 build, 실패하는 build, 기한 인자를 실행한다. Red: 같은 느리지만 정상인 build를 main의 실행기로 node tests/run-case.mjs fixture/slow-build 1s -- node -e ...처럼 실행하면 STEP fixture/slow-build elapsed=37ms: compiling one과 FAIL fixture/slow-build elapsed=1.011s: deadline 1s exceeded를 출력하고 1로 끝났다. Green: node tests/run-long.mjs fixture/slow-build -- node -e ...는 RUN fixture/slow-build no-deadline, 출력 줄 세 개, STEP fixture/slow-build elapsed=1.599s: exit 0, PASS fixture/slow-build elapsed=1.6s를 출력하고 0으로 끝난다. node --test tests/testcase.test.mjs는 case 7개를 통과하고(testcase/run-long 1.8 s, 실패하는 build는 오류 줄 뒤에 FAIL fixture/broken-build ...: ... exited with 3으로 보고하고 5m은 usage 줄과 함께 거부한다), node scripts/repo/check.mjs가 통과한다.——[o]
G5.30-3검증 명령의 build와 lint 도구를 기한 없이 실행한다. 원인: G5.29-4의 규칙이 장기 작업에 timeout을 요구했기 때문에 G5.29-8은 contracts/features.json 검증 명령의 도구를 TOOL_DEADLINE과 BUILD_DEADLINE의 기한으로 tests/run-case.mjs에 감쌌다: model_queries와 model_writes의 decimal-typescript-types(typescript-types 5m, tsc --noEmit), chunking-rust(rust-build/integration 8m, cargo build), conformance-build(go-vet/conformance 5m)이다. G5.29-3은 helper go-test-helpers에 같은 형식(go-vet/test-helpers 8m)을 주었다. 빈 cache나 바쁜 machine에서 그 수보다 오래 걸리는 도구는 동작해도 실패한다. 이제 이 다섯 명령은 node tests/run-long.mjs <name> -- ...로 기한 없이 실행하고 출력과 종료 코드를 보인다. Red: main의 형식에 기한만 1 s로 낮춘 실제 도구 node tests/run-case.mjs typescript-types 1s -- node clients/typescript/node_modules/typescript/bin/tsc -p clients/typescript/tsconfig.json --noEmit과 node tests/run-case.mjs go-vet/conformance 1s -- go vet ./tests/conformance/check ./tests/conformance/runner_go가 FAIL typescript-types elapsed=1.017s: deadline 1s exceeded와 FAIL go-vet/conformance elapsed=1.018s: deadline 1s exceeded를 출력하고 1로 끝났다. Green: 같은 명령을 tests/run-long.mjs로 실행하면 RUN ... no-deadline, STEP typescript-types elapsed=1.459s: exit 0과 PASS typescript-types elapsed=1.46s, STEP go-vet/conformance elapsed=3.168s: exit 0과 PASS go-vet/conformance elapsed=3.168s를 출력하고 0으로 끝난다. node scripts/features/build.mjs --check가 통과한다.——[o]
G5.30-4모든 cargo test 실행의 test binary를 기한 없이 build한다. 원인: G5.29-9는 G5.29-4의 틀린 timeout 규칙을 따라 각 cargo test 실행 앞의 cargo test --no-run build를 $(RUN_CASE) rust-build/<target> $(BUILD_DEADLINE)이나 node tests/run-case.mjs rust-build/<id> 8m으로 실행했다: Makefile target testcase-check, dbspec-introspect-rust-check, dbspec-apply-rust-check, dbspec-plan-rust-check, dbspec-rust-check(build 두 개), rust-send-savepoint-check, rust-driver-check, contracts/features.json의 검증 명령 11개(build 14개), helper rust-test-helpers, scripts/client-db-test.sh의 Rust lane이다. timing target도 orm-schema-tests를 같은 방식으로 build했다. 바쁜 machine에서 의존성 전체를 compile하는 clean build처럼 8분보다 오래 걸리는 Rust build는 compile에 성공해도 실패했다. 이제 이 build는 저마다 $(RUN_LONG) rust-build/<target>이나 node tests/run-long.mjs rust-build/<id>로 compiler 출력을 단계 줄로 보이며 기한 없이 실행한다. scripts/client-db-test.sh는 integration program의 cargo build도 같은 방식으로 실행한다. 순서는 그대로다: build 뒤에, case마다 자기 기한을 가진 cargo test 실행이 온다. Red: 느리지만 정상인 build(1.5 s 동안 compiler 줄을 출력하고 0으로 끝나는 PATH의 cargo)를 main의 형식에 기한만 1 s로 낮춰 node tests/run-case.mjs rust-build/dsn-rust 1s -- cargo test --no-run ...으로 실행하면 첫 compiler 줄 뒤에 FAIL rust-build/dsn-rust elapsed=1.003s: deadline 1s exceeded를 출력하고 1로 끝났다. Green: 같은 fixture로 node tests/run-long.mjs rust-build/dsn-rust -- cargo test --no-run ...은 compiler 줄 세 개, STEP rust-build/dsn-rust elapsed=1.514s: exit 0, PASS rust-build/dsn-rust elapsed=1.514s를 출력하고 0으로 끝난다. make -n testcase-check는 node tests/run-long.mjs rust-build/testcase-check --cwd clients/rust -- cargo +1.98.1 test --no-run ...을 보이고, node scripts/repo/check.mjs가 통과한다.——[o]
G5.30-5check의 모든 TypeScript build와 type check를 기한 없이 실행한다. 원인: G5.29-10은 G5.29-4의 틀린 timeout 규칙을 따라 TypeScript build 세 개를 5분 TOOL_DEADLINE으로 tests/run-case.mjs에 감쌌다: scripts/typescript/sqlite-test.sh, root package.json script typescript:test, Makefile target conformance-result-check이다. Makefile의 다른 TypeScript build는 이미 RUN_CASE 아래에서 기한과 함께 실행했다: TSC_BUILD(dbspec target), feature-check, case-database-check(BUILD_DEADLINE), timing-check의 build, target typescript-build, ts-check의 type check(typescript-check)이다. 그 수보다 느린 build는 compile에 성공해도 실패했다. 이제 모두 $(RUN_LONG) typescript-build나 typescript-check, 또는 node tests/run-long.mjs typescript-build로 build 출력과 종료 코드를 단계 줄로 보이며 기한 없이 실행한다. Red: 느리지만 정상인 build(npm과 tsc 줄을 출력하고 1.5 s 뒤 0으로 끝나는 PATH의 npm)를 main의 형식에 기한만 1 s로 낮춰 node tests/run-case.mjs typescript-build 1s -- npm run typescript:build로 실행하면 두 줄 뒤에 FAIL typescript-build elapsed=1.006s: deadline 1s exceeded를 출력하고 1로 끝났다. Green: 같은 fixture로 node tests/run-long.mjs typescript-build -- npm run typescript:build는 build finished, STEP typescript-build elapsed=1.533s: exit 0, PASS typescript-build elapsed=1.534s를 출력하고 0으로 끝난다. make -n conformance-result-check는 node tests/run-long.mjs typescript-build -- npm run typescript:build로 시작하고, node scripts/repo/check.mjs가 통과한다.——[o]
G5.30-6모든 go test 실행의 Go test binary를 기한 없이 build한다. 원인: G5.29-13은 G5.29-4의 틀린 timeout 규칙을 따라 tests/go-test.mjs가 각 go test 앞의 build를 8분 BUILD_DEADLINE 아래의 case go-build/<packages>로 실행해 기한에 build의 process group을 끝내게 했고, fuzz-check의 각 명령을 $(RUN_CASE) fuzz/<package> $(BUILD_DEADLINE)로 실행했다. timing-check도 Go test binary를 같은 방식(go-build/dbspec-test)으로 build했다. 바쁜 machine에서 빈 cache로 하는 build처럼 8분보다 느린 Go build는 compile에 성공해도 실패했다. 이제 tests/go-test.mjs의 build는 runLong으로 RUN go-build/<packages> no-deadline과 함께 compiler 출력과 exit 0을 단계 줄로 보이며 실행한다. 실패한 build는 오류 줄과 종료 코드를 보고하고, case마다 자기 기한을 유지하는 test 실행은 build가 통과한 뒤에만 시작한다. 길이를 -fuzztime=1s가 정하는 fuzz-check의 명령과 timing-check의 build는 $(RUN_LONG) 아래에서 실행한다. tests/go-test.test.mjs의 새 case a slow but healthy go test build runs to its end without a deadline(testcase-check)은 build 단계에서 줄 사이에 1.5 s를 쉬는 go를 PATH 앞에 둔다. Red: 그 go로 BUILD_DEADLINE만 1 s로 낮춘 main의 tests/go-test.mjs는 STEP go-build/engine elapsed=342ms: # orm/engine과 FAIL go-build/engine elapsed=1.005s: deadline 1s exceeded를 출력하고 test를 실행하지 않은 채 1로 끝났다. Green: 같은 go로 node tests/go-test.mjs -v -timeout 0 ./engine -count=1은 RUN go-build/engine no-deadline, build 줄 두 개, STEP go-build/engine elapsed=1.517s: exit 0, PASS go-build/engine elapsed=1.517s, 그 뒤 test 줄을 출력하고 0으로 끝난다. node --test tests/go-test.test.mjs는 case 2개를 통과하고(새 case 2.5 s, 실패하는 build는 오류 줄 뒤에 FAIL go-build/engine ...: go test build exited with 2로 보고하고 test를 실행하지 않는다), make -n fuzz-check는 두 명령을 tests/run-long.mjs 아래에 보이며, node scripts/repo/check.mjs가 통과한다.——[o]
G5.30-7Makefile에서 RUN_CASE, BUILD_DEADLINE, TOOL_DEADLINE을 없애고, make repo-check가 장기 작업의 모든 기한을 실패시킨다. 원인: Makefile은 RUN_CASE(tests/run-case.mjs)를 BUILD_DEADLINE = 8m, TOOL_DEADLINE = 5m과 함께 정의하고 recipe 21줄을 더 그 아래에서 실행했다: dbspec target, example-check, timing-check의 Rust example, release, stress build, case-database-check의 integration build, rust-check와 rust-send-savepoint-check의 cargo check와 cargo clippy 네 번, cargo fmt, cargo fetch(rust-fetch), gofmt(go-fmt-check), go vet ./...(go-vet-check), go-model-check의 생성, package-check, rust-150-check, php-without-mysql-check이다. 모두 build, 설치, 도구 실행이고, 느리지만 정상인 실행이 기한에 실패한다. scripts/package-check.sh는 test binary의 기본 10분 기한으로 go test ./...를 실행했다. repository 규칙이 그 기한을 강제했다: scripts/repo/testcases.mjs의 unwrappedToolErrors, unbuiltCargoTestErrors, rawGoTestErrors는 기한을 가진 실행기인 run-case를 요구했다. 이제 21줄은 $(RUN_LONG) 아래에서 실행하고, RUN_CASE, BUILD_DEADLINE, TOOL_DEADLINE, tests/run-case.mjs(와 tests/testcase.test.mjs의 그 case)는 없어졌으며, package-check.sh는 go test -timeout 0 ./...를 실행한다. 규칙은 뒤집혔다: unwrappedToolErrors는 tests/run-long.mjs 밖이나 기한 형식 아래에서 실행하는 build나 lint 도구를 실패시키고, unbuiltCargoTestErrors는 run-long 밖이나 기한 아래의 cargo test --no-run build와 그 build 없는 cargo test 실행을 이제 root package.json script에서도 실패시키며, rawGoTestErrors는 tests/go-test.mjs나 run-long 밖, 또는 기한 아래의 go test를 실패시킨다. 새 longDeadlineErrors는 변수를 푼 Makefile recipe, 검증 명령, root package.json script, 그리고 run-long 아래의 것까지 그것들이 실행하는 모든 script에서 장기 작업에 기한을 두는 segment마다 실패한다: tests/run-case.mjs, $(RUN_CASE), $(BUILD_DEADLINE), $(TOOL_DEADLINE), timeout <n>, 그리고 0이 아닌 -timeout이나 -timeout 없이 실행하는 go test이다(-c build는 실행이 아니다). runnerIdentity는 run-long만 풀어 읽는다. Red: 이 변경 전 tree에 새 규칙을 적용하면 node scripts/repo/check.mjs가 Makefile 21줄과 scripts/package-check.sh로 오류 40개를 내며 실패했다. main checkout에 복사하면 장기 작업에 기한을 두는 segment 73개에서 오류 130개로 실패했고, 새 repository case 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, no check puts a deadline on a long operation이 그곳에서 실패했다. Green: 그 case들과 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, a deadline on a long operation fails가 통과한다. node --test scripts/repo/check.test.mjs는 case 50개를, node --test tests/testcase.test.mjs는 case 6개를 통과하고 node scripts/repo/check.mjs가 통과하며, make go-fmt-check는 RUN go-fmt no-deadline, STEP go-fmt elapsed=266ms: exit 0, PASS go-fmt elapsed=266ms를 출력한다.——[o]
G5.30-8어떤 CI step이나 job에도 기한이 없다. 원인: G5.29-1은 make나 test runner를 실행하는 step에서 timeout-minutes를 없앴지만 다른 모든 step에는 그것을 필수로 했다: scripts/repo/ci.mjs의 stepTimeoutErrors는 자기 timeout-minutes가 없는 step을 실패시켰고, 그래서 ci.yml에는 step 기한 14개(checkout과 setup action, Node, Rust, PHP, database server 설치, PHP와 환경 step, interface 검사)가, docs-pages.yml에는 6개(checkout, setup-node, npm ci, Playwright 설치, upload, deploy)가 남았다. 이 step은 모두 장기 작업인 설치, server program 준비, 도구 실행이고, 느린 mirror나 바쁜 runner에서는 성공할 것이 기한에 실패한다. 이제 두 workflow의 어떤 step이나 job에도 timeout-minutes가 없고, runner가 step마다 줄 단위로 log를 남기고 종료 코드로 판정한다. stepTimeoutErrors는 뒤집혔다: timeout-minutes를 가진 모든 step과 step 밖에 timeout-minutes(job 기한)를 가진 workflow를 실패시킨다. 옛 구분에만 쓰던 reportsCases는 없어졌고, ci.yml의 머리말이 규칙을 적는다. Red: 이전 tree의 workflow에 새 규칙을 적용하면 node scripts/repo/check.mjs가 오류 20개(ci.yml의 step 14개, docs-pages.yml의 6개)로 실패했고, scripts/repo/check.test.mjs의 새 case no workflow step or job has timeout-minutes가 실패했다. Green: 그 case가 통과하고, timeout-minutes를 가진 checkout, 설치, make check step과 job 기한을 가진 최소 workflow는 실패한다. grep -c timeout-minutes는 ci.yml의 주석 두 줄만 찾고 docs-pages.yml에서는 아무것도 찾지 않으며, node --test scripts/repo/check.test.mjs는 case 50개를 통과하고 node scripts/repo/check.mjs가 통과한다.——[o]
G5.30-9기능 coverage checker의 build를 기한 없이 실행한다. 원인: scripts/features/coverage.mjs의 buildNative는 coverage 실행 전의 각 build(state reader, test2json, Go package마다 go test -c, Rust workspace마다 cargo locate-project와 cargo test --no-run)를 Makefile BUILD_DEADLINE의 수인 8분 COVERAGE_BUILD_DEADLINE 아래의 case로 실행했고, 그 run은 그 시간에 build process를 timeout after <n> ms로 끝냈다. 그보다 느린 build는 compile에 성공해도 그 build를 쓰는 모든 coverage 실행을 실패시켰다. 이제 각 build는 runLong으로 RUN features/coverage/build/<name> no-deadline과 함께 compiler 출력과 exit 0을 단계 줄로 보이며 실행하고, run은 timeoutMs가 null이면 timer 없이 실행하며, executeCoverage는 build 기한을 받지 않는다. COVERAGE_BUILD_DEADLINE은 없어졌다. coverage 실행은 process마다 자기 기한(DATABASE)을 유지한다. scripts/features/coverage.test.mjs의 새 case a slow but healthy coverage build runs to its end without a deadline(feature-unit-check)은 1.5 s를 기다린 뒤 실제 go를 실행하는 go를 PATH 앞에 둔다. Red: 그 go로 이 변경 전 code의 build 기한을 1 s로 낮추면 executeCoverage가 FAIL features/coverage/build/go/test2json elapsed=1.103s: timeout after 1000 ms와 FAIL features/coverage/build/go/clients/go/sample elapsed=1.001s: timeout after 1000 ms를 출력하고 go test2json build failed: timeout after 1000 ms와 exactly two executions required를 돌려주었다. Green: 같은 probe는 RUN features/coverage/build/go/test2json no-deadline, STEP ... elapsed=2.605s: exit 0, PASS features/coverage/build/go/test2json elapsed=2.605s와 sample package의 같은 줄(2.33 s)을 출력하고 두 실행이 오류 없이 PASS한다. 새 case는 5.1 s에 통과하고 every Go execution runs the test instead of reading the test cache도 통과한다.——[o]
G5.30-10문서를 기한 없이 build한다. 원인: scripts/docs/build.mjs(make docs-build)는 prepare.mjs의 Mermaid 렌더링과 VitePress build를 5분 PROCESS 기한 아래의 case로 실행해 그 시간에 두 process를 중단했고, scripts/docs/idempotent.mjs(make docs-verify-idempotent)는 두 번의 build를 10분 아래에서 실행했다. 둘 다 build이고, 느리지만 정상인 실행이 기한에 실패한다. 이제 둘은 runLong으로 RUN docs-build no-deadline과 RUN docs-idempotent no-deadline과 함께 각 process의 출력과 exit 0을 단계 줄로 보이며 실행하고, 결과는 종료 코드와 byte 비교로 정한다. Red: 기한을 1 s로 낮춘 main의 scripts/docs/build.mjs와 scripts/docs/idempotent.mjs가 FAIL docs-build elapsed=1.002s: The operation was aborted와 FAIL docs-idempotent elapsed=1.004s: The operation was aborted를 출력하고 1로 끝났다. Green: node scripts/docs/build.mjs는 RUN docs-build no-deadline, build 줄, 두 process의 exit 0, PASS docs-build elapsed=22.327s를 출력하고 0으로 끝나며, node scripts/docs/idempotent.mjs는 RUN docs-idempotent no-deadline, two builds produced identical bytes in 179 files, PASS docs-idempotent elapsed=24.818s를 출력하고 0으로 끝난다.——[o]
G5.30-11test server를 기한 없이 시작하고 준비 여부는 server의 준비 줄로 판정한다. 원인: ProxySQL과 PgBouncer를 시작하는 scripts/test-servers.sh의 start_logged는 server마다 기한(G5.13의 60 s)을 받았고, 그 안에 준비 줄이 오지 않으면 timer가 시작을 실패시켰다. PostgreSQL과 그 replica는 pg_ctl start -w로 시작하고 pg_ctl stop -w로 멈췄으며, 둘은 최대 60 s(PGCTLTIMEOUT)만 기다리고 제한 없는 설정이 없다. 바쁜 machine이나 큰 data directory에서 느리지만 정상으로 시작하는 server가 make test-servers 전체를 실패시켰다. 이제 start_logged <name> <line> <command>...에는 기한이 없다: server를 foreground에서 실행하고, 출력 줄마다 $DIR/<name>.log에 복사하며, 준비 줄까지의 줄을 stderr에 test-servers: <name>: <line>으로 보이고, server가 준비 줄을 쓰면 돌아오며, server가 먼저 끝나면 log의 마지막 줄과 함께 실패한다. 기다림은 이 두 관측 가운데 하나로 끝난다. PostgreSQL과 replica는 준비 줄 database system is ready to accept connections와 database system is ready to accept read-only connections로 start_logged를 거쳐 postgres -D ...로 실행하고, fast shutdown인 INT로 stop_pid를 거쳐 process가 끝날 때까지 기다려 멈춘다. postgres는 확인하는 도구에 들어갔다. 실행 중인 test server는 다시 시작하지 않았다. scripts/repo/check.test.mjs의 repository case start_logged waits for a ready line or an exit, with no deadline(test-servers-check와 repo-check)이 기한 case를 대신한다: start_logged의 timer와 주석 밖의 pg_ctl ... -w를 거부하고, 바로 준비되는 fixture server, 2.5 s 뒤에야 준비 줄을 쓰는 server, 먼저 끝나는 server를 실행한다. Red: 그 case는 main의 script에서 start_logged has a timer로 실패했고, 기한 1 s를 준 main의 start_logged는 1.5 s 뒤에 now accepting connections를 쓰는 fixture server를 test-servers: fake logged no line with 'accepting connections' within its deadline of 1 s와 종료 코드 1로 실패시켰다. Green: case가 통과한다(2.8 s). 새 start_logged는 같은 fixture를 시작해 test-servers: fake: starting과 test-servers: fake: now accepting connections를 출력하고 0을 돌려준다. port 55432와 55433의 일회용 PostgreSQL 17을 script와 같은 인자로 새 start_logged로 시작하면 primary는 database system is ready to accept connections까지, pg_basebackup replica는 database system is ready to accept read-only connections까지 log 줄을 출력했고, primary에 쓴 행을 replica에서 읽었으며, 둘 다 stop_pid ... INT로 멈췄다.——[o]
G5.30-12test case 두 곳에서 장기 작업을 분리한다. 원인: scripts/docs/check.mjs(make docs-static-check, make docs-check의 끝)는 local server 시작, Chromium 실행, 모든 page, link, 검색, theme와 mobile 탐색까지 smoke test 전체를 5분 PROCESS 기한의 case docs-static 하나로 실행했다. 그래서 느린 server나 browser 시작이 page 검사와 같은 시계를 썼고, 멈춘 page는 case 전체로만 보고되었다. clients/typescript/tests/package-declarations.mjs는 npm pack, 임시 project로의 설치, tsc를 120 s 기한의 case package-declarations/type-check-without-driver-types 안에서 실행했다. 그 호출은 동기 호출이라 기한이 멈출 수 없었고(1 s로 낮춘 기한 아래에서 2.41 s 뒤에 PASS했다), file은 runCase의 결과를 버려 실패한 case도 종료 코드 0으로 끝났다. 이제 docs-static/server와 docs-static/browser는 runLong으로 단계 줄 하나씩과 함께 기한 없이 실행하고, page마다 자기 case docs-static/page/<path>(30 s)가 있으며, 그 뒤에 docs-static/links(1분), docs-static/search와 docs-static/theme-and-mobile(각 2분)이 온다. 각 case는 그 동안 생긴 browser 오류로도 실패한다. package-declarations.mjs에서는 package-declarations/install(npm pack, tar, 복사)과 package-declarations/tsc가 runLong으로 출력과 종료 코드를 단계 줄로 보이며 실행하고, case package-declarations/type-check-without-driver-types(1분)가 tsc의 종료 코드와 출력을 검사하며, 실패한 case는 종료 코드 1을 둔다. Red: 기한을 1 s로 낮춘 main의 scripts/docs/check.mjs가 FAIL docs-static elapsed=6.001s: deadline 1s exceeded를 출력했다. main의 package-declarations.mjs는 1 s 기한 아래에서 PASS package-declarations/type-check-without-driver-types elapsed=2.41s를 출력했고, tsc 검사를 뒤집으면 그 case에 FAIL을 출력하고도 0으로 끝났다. Green: 새 scripts/docs/check.mjs는 build한 site에서 RUN docs-static/server no-deadline과 RUN docs-static/browser no-deadline을 단계 줄과 함께 출력하고, page case 48개(가장 긴 것 268 ms)와 links, search(579 ms), theme와 mobile(1.211 s) case를 통과하고 0으로 끝난다. 새 package-declarations.mjs는 RUN package-declarations/install no-deadline과 RUN package-declarations/tsc no-deadline을 exit 0 단계 줄과 함께 출력하고 두 case를 통과해 0으로 끝나며, tsc 검사를 뒤집으면 FAIL과 1 case(s) failed를 출력하고 1로 끝난다. node scripts/repo/check.mjs가 통과한다.——[o]
G5.30-13test server가 멈추기를 polling이 아니라 process의 종료 신호로 기다린다. 원인: PgBouncer, ProxySQL, 그리고 G5.30-11부터 두 PostgreSQL server를 멈추는 scripts/test-servers.sh의 stop_pid는 signal을 보낸 뒤 process가 없어질 때까지 0.1 s마다 kill -0으로 확인했다. 운영체제가 process의 종료를 알리는데도 그 반복은 polling이다. 이제 stop_pid는 새 scripts/stop-process.py <signal> <pid>를 실행한다. 그것은 먼저 process에 감시를 등록하고(macOS는 kqueue EVFILT_PROC NOTE_EXIT, Linux는 os.pidfd_open의 pidfd를 poll), 그 뒤 signal을 보내며, 종료 알림이 오면 돌아온다. 등록할 때 이미 없는 process는 그 사실을 출력하고 멈춘 것으로 본다. 기다림에는 기한도 반복도 없다. python3는 make test-servers가 확인하는 도구에 들어갔다. 실행 중인 test server는 다시 시작하지 않았다. scripts/repo/check.test.mjs의 새 repository case stop_pid waits for the exit event of the server, with no polling은 stop_pid의 sleep, while, until, kill -0을 거부하고, TERM trap을 둔 뒤 FIFO로 시작을 알리는 fixture server 두 개, 곧바로 끝나는 것과 TERM 뒤 1.5 s가 걸리는 것을 멈춘다. Red: main의 script에서 그 case가 stop_pid polls for the exit로 실패했다. Green: case가 통과한다(1.6 s). stop_pid가 돌아올 때 두 fixture는 모두 없고, 느린 것은 1.5 s 전에 돌아오지 않는다. node --test scripts/repo/check.test.mjs는 case 51개를 통과하고 node scripts/repo/check.mjs가 통과한다. port 55432의 일회용 PostgreSQL 17을 start_logged로 시작하고 stop_pid ... INT로 멈추면 log의 마지막 줄이 database system is shut down이고 process는 없다. Linux 경로(pidfd)는 개발 machine에서 실행하지 않았고, Linux runner의 make test-servers와 repo-check에서 실행된다.——[o]
G5.30-13-1test server의 process 종료 감시기를 Go로 쓴다. 원인: G5.30-13은 scripts/stop-process.py를 저장소의 도구 언어인 Go, PHP, Rust, TypeScript 밖의 Python으로 썼다. 그래서 test server를 멈추는 모든 machine과 runner가 Python runtime에 의존했고, make test-servers는 python3를 확인했다. 다른 언어의 도구를 거부하는 검사가 없었고, helper는 어떤 owner check의 입력도 아니어서 그 변경은 **를 통해 repo-check만 골랐다. 이제 tests/stop-process는 go.mod의 직접 의존인 golang.org/x/sys/unix를 쓰는 Go 명령이다: main.go는 감시를 등록하고 signal을 보낸 뒤 기다리고, watch_darwin.go는 kqueue EVFILT_PROC NOTE_EXIT, watch_linux.go는 unix.PidfdOpen과 unix.Poll을 쓰며, 둘 다 timeout이 없다. Makefile은 test-servers, test-servers-stop, test-servers-check 앞에서 그것을 $(RUN_LONG) go-build/stop-process로 .runtime/bin/stop-process에 build하고 그 경로를 STOP_PROCESS로 넘기며, stop_pid는 그것을 요구하고 실행한다. python3는 확인하는 도구에서 빠졌고 scripts/stop-process.py는 없어졌다. stop_pid case는 test-servers-check가 실행하는 새 scripts/test-servers.test.mjs로 옮겼고, stop_pid가 STOP_PROCESS를 실행하는지도 검사한다. contracts/check-inputs.json에서 tests/stop-process/**와 scripts/test-servers.test.mjs는 test-servers-check의 입력이다. make repo-check가 실행하는 scripts/repo/scripts.mjs의 toolingLanguageErrors는 Python, Ruby, Perl, Lua, Java, Kotlin, C#, Swift, Scala, R로 쓴 tracked program을 실패시킨다. Red: main의 tree에서 toolingLanguageErrors가 scripts/stop-process.py is a program in a language outside Go, PHP, Rust and TypeScript를 돌려주었고, main의 script에 대한 stop_pid case가 stop_pid does not run the Go program STOP_PROCESS of tests/stop-process로 실패했다. Green: make test-servers-check는 RUN go-build/stop-process no-deadline, STEP go-build/stop-process elapsed=713ms: exit 0과 PASS를 출력하고 case 6개를 통과한다(빠른 fixture와 1.5 s fixture server의 stop_pid case 2.1 s). run-long 아래의 GOOS=linux GOARCH=arm64 go build ./tests/stop-process가 통과하고, GOOS=linux go vet과 gofmt -l은 아무것도 보고하지 않는다. node --test scripts/repo/check.test.mjs는 case 51개를 통과하고 node scripts/repo/check.mjs가 통과한다. port 55432의 일회용 PostgreSQL 17은 Go program으로 stop_pid ... INT를 거쳐 멈췄고, log는 database system is shut down으로 끝나며 process는 없다. Linux 감시기는 push 때 GitHub CI의 make test-servers와 test-servers-check에서 실제로 실행된다.——[o]
G5.30-14test server의 MySQL migration을 기한 없이 실행한다. 원인: scripts/test-servers-mysql.mjs는 migration의 각 단계를 기한 아래의 case로 실행했고(preflight 1분, dump 30분, stop 5분, keep 1분, start 5분, restore 30분, verify 10분, record 1분), verify는 SOURCE_POS_WAIT(file, position, 600)으로 replica를 기다렸다. 그래서 바쁜 machine에서 큰 data directory의 migration은 data를 옮기는 도중에 시계 때문에 실패했다. 이제 각 단계는 runLong으로 RUN mysql-migration/<step> no-deadline과 단계 줄을 출력하며 실행하고, 명령의 종료 코드와 비교한 수로 판정한다. SOURCE_POS_WAIT에는 timeout이 없고, 함수가 NULL이나 음수를 돌려주면 여전히 실패한다. Red: 이제 단계마다 RUN mysql-migration/<step> no-deadline 줄을 요구하는 scripts/test-servers-mysql.test.mjs의 migration case a MySQL setting other than the declared one migrates the data with equal counts, then nothing이 이전 commit의 script에서 실패했다. 그 출력에는 RUN mysql-migration/dump deadline=30m0s와 RUN mysql-migration/verify deadline=10m0s가 있었다. Green: 이 script로 같은 case가 통과한다(port 39301과 39302의 일회용 MySQL primary와 replica에서 47.4 s).——[o]
G5.30-15Go conformance와 interface checker의 runner를 기한 없이 build한다. 원인: tests/conformance/check는 Rust runner, TypeScript client, Go runner를 rustBuildDeadline(8분), typescriptBuildDeadline(2분), goBuildDeadline(3분)의 합 아래의 case conformance/build 하나로, 명령마다 자기 timeout과 함께 build했고, tests/interfaces/check는 Rust symbol 추출기를 rustBuildDeadline(testcase.Process, 5분) 아래의 case interfaces/rust-build로 cargo build --quiet로 build해 진행이 로그에 남지 않았다. 느리지만 정상인 build가 시계 때문에 실패했다. 이제 internal/testcase에는 RUN <name> no-deadline, 단계, 경과 시간과 함께 PASS나 FAIL을 보고하며 timer 없이 실행하는 RunLong이 있다. conformance/build와 interfaces/rust-build는 그것으로 실행하고, runCommand는 build를 timeout 없이 실행하며(timeout 0), build 기한은 없어졌고, 추출기 build는 cargo 출력을 단계 줄로 보인다. runner와 언어 case는 자기 기한을 유지한다. internal/testcase의 report fixture(testcase-check)는 1.5 s 걸리는 장기 작업과 실패하는 장기 작업을 더한다. Red: main의 go run ./tests/interfaces/check --self-test는 RUN interfaces/rust-build deadline=5m0s를 출력했다. Green: 여기서 같은 명령은 RUN interfaces/rust-build no-deadline, run cargo build ..., Compiling orm-interface-symbols, Finished 단계 줄, PASS interfaces/rust-build elapsed=1.645s를 출력했다(PHP vendor directory와 TypeScript build가 없는 이 worktree에서 PHP와 TypeScript case는 실패했다). go test -run TestReportForm ./internal/testcase가 RUN runner/long no-deadline과 PASS runner/long elapsed=1.5...s와 함께 통과한다.——[o]
G5.30-16모든 go run의 build를 로그로 남긴다. 원인: go run은 program을 실행하기 전에 compile하고 그동안 아무것도 출력하지 않아, go run 14개가 단계 로그 없이 build했다: Makefile의 conformance-check와 interface-check, 검증 명령 interface-self-test, 모든 실행의 database 생성이 실행하는 scripts/bench-db.sh의 6개, scripts/decimal-bench-sqlite.sh의 2개, CI step common state and execution contracts의 3개다. 이제 tests/go-run.mjs <name> <package> [args...]는 go build -o <임시 file>로 package를 장기 작업 go-build/<name>(runLong: 기한 없음, compiler 출력과 exit 0을 단계 줄로)으로 build하고, 인자와 함께 program을 실행해 그 종료 코드로 끝나며, file을 지운다. 14개 모두 그것을 쓰고, runnerIdentity는 그것을 test runner로 세지 않는다. make repo-check의 goRunErrors는 Makefile recipe, 검증 명령, root package.json script, 그것들이 실행하는 모든 script, workflow step의 go run을 실패시킨다. Red: 이전 commit의 file에 goRunErrors는 14개를 보고했다. Green: scripts/repo/check.test.mjs의 새 case a go run, whose build has no step log, fails와 고친 기능 명령 case가 통과하고(case 58개), node scripts/repo/check.mjs가 통과하며, make interface-check는 case 앞에 RUN go-build/interfaces-check no-deadline, STEP go-build/interfaces-check elapsed=659ms: exit 0, PASS go-build/interfaces-check elapsed=659ms를 출력하고 case가 모두 통과한다.——[o]
G5.30-16-1interface guard test가 go-run.mjs 형식을 요구하게 한다. 원인: G5.30-16은 검증 명령 interface-self-test를 node tests/go-run.mjs interfaces-check ./tests/interfaces/check --self-test로 바꿨지만, tests/interfaces/check/prohibited_test.go의 TestCIRequiresGeneratedChecks는 여전히 contracts/features.json에 go run ./tests/interfaces/check --self-test라는 글을 요구했다. 그 test는 G5.30-16의 범위 make owner-check가 고르지 않은 suite target go-test-check에 속하고, G5.32-9의 전체 실행이 그것을 실패시켰다. 이제 interfaceCommand는 새 명령이므로 guard와 그 mutation은 단계 로그와 함께 build하는 형식을 요구한다. Red: go test -run TestCIRequiresGeneratedChecks ./tests/interfaces/check가 prohibited_test.go:89: contracts/features.json must verify go run ./tests/interfaces/check --self-test로 실패했다. Green: 같은 test가 통과한다. path 5개로 make owner-check가 interface_contract의 검증 명령 1개, coverage 단위 4개, make target 7개를 통과했다.——[o]
G5.32모든 client가 PostgreSQL statement를 type 있는 bind와 함께 describe나 따로 드는 prepare 없이 round trip 하나로 보내게 한다. Cause: planner는 bind slot의 dbspec type을 date, time, datetime, decimal에만 두고(engine/planner/planner.go의 bindType과 다른 planner의 같은 함수) 다른 slot에서는 버리므로 plan은 무엇을 bind하는지 말하지 않는다. 그래서 Rust는 server에 parameter type을 묻고(sqlx Executor::prepare를 쓰는 pg_describe. 새 결과 column의 origin도 pg_attribute에서 읽는다) Go는 statement text마다 자기 round trip으로 prepare했다. tests/events/vectors.json의 server_transactions(G5.31)에서 model_statements의 첫 실행은 event 8개에 Rust가 transaction 15개, Go가 13개를 썼고 PHP와 TypeScript는 8개였다. 하위 항목 G5.32-1에서 G5.32-8이 이를 고친다: 네 planner의 모든 bind slot이 dbspec type을 가지고, Rust는 그 type으로 bind하며 server에 아무것도 묻지 않고, 연결은 probe 문장을 보내지 않으며, slot에 type이 없던 raw SQL API는 사라졌다. Go는 pgx statement cache를 유지하고(G5.32-5), Rust는 docs/perf.md에 기록한 sqlx prepare round trip을 유지한다.——[o]
G5.32-1Go의 PostgreSQL statement를 pgx query mode exec로 prepare하지 않고 전송한다. Cause: clients/go/orm의 DB.stmt와 txConn.stmt는 새 statement text마다 database/sql PrepareContext로 prepare했고, pgx는 그 prepare(Parse, Describe, Sync)를 실행 전에 자기 round trip과 implicit transaction으로 전송한다. 변경: client가 만드는 PostgreSQL DSN은 default_query_exec_mode=exec를 두고, PostgreSQL에서 두 함수는 pool이나 transaction 연결의 QueryContext, QueryRowContext, ExecContext를 text로 실행하는 unprepared statement를 돌려준다. pgx는 statement와 text format bind를 parameter type 없는 unnamed statement 하나로 보내므로 PHP와 TypeScript처럼 server가 type을 추론한다. MySQL과 SQLite는 prepared statement cache를 유지한다. server_transactions의 first_run_clients에 go를 넣는다. docs/operations/perf.md(F1)와 docs/spec/config.md(+ko, max_prepared_statements)가 이를 적는다. Red: vector를 바꾸고 이전 client로 실행한 TestStatementEventsServerTransactions는 첫 실행에서 event 8개에 transaction 13개로 실패했다(두 번째 8개). Green: 8과 8이다. TestParseDSN은 PostgreSQL native DSN 두 개에 default_query_exec_mode=exec를 기대한다. node tests/go-test.mjs ./clients/go/orm ./clients/go/orm/pg는 MySQL, PostgreSQL, SQLite에서 모든 test가 통과한다. go run ./tests/conformance/check run -driver postgres는 기록된 기대값에 대해 26 vectors × 4 languages identical을 보고하므로 exec mode의 text 결과 format은 같은 값으로 decode된다. php tests/codec/check.php는 Go, Rust, TypeScript codec 출력의 80 vector로 통과한다. go run ./tests/interfaces/check --record --language go는 private symbol modelStatement, unprepared와 바뀐 DB.stmt, executor를 contracts/symbols/go.json에, 그 hash를 contracts/interfaces.json에 기록했고, go run ./tests/interfaces/check --self-test는 네 언어에서 통과한다. 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-2네 planner의 모든 bind slot에 dbspec type을 둔다. Cause: engine/planner/planner.go의 bindType(그리고 Rust, TypeScript, PHP planner의 bind_type, bindType)은 비교하거나 할당하는 column의 type을 date, time, datetime, decimal에만 두고 나머지 slot에는 type이 없어서 plan은 무엇을 bind하는지 말하지 않았다. 변경: 모든 slot의 col_type은 placeholder가 받는 값의 dbspec type이다. column과 비교하거나 할당하는 값은 contains, 더하기와 빼기 할당, optimistic 검사를 포함해 그 column의 type이고(host style을 거친 값은 저장되는 값이다), SQL 쪽 style 함수의 입력은 그 함수의 입력 type(HEX bytes, INET6_ATON text), 비교하는 column 함수의 값은 결과 type, 상대 시각 함수의 간격은 i32(초는 f64), blind index의 평문은 blind index column의 type, secret은 text, config는 AES version column의 type, audit은 audit column의 type, now는 datetime이다. parent slot은 비교하는 child key column의 type인 key_types를 가진다. type 없는 slot은 IR_INVALID로 실패하고, raw fragment의 placeholder만 type이 없다(rawParam, raw_param). G5.32-3이 raw fragment를 없앨 때까지다. plan field는 Go의 BindSlot.ColType과 새 KeyTypes, Rust, TypeScript, PHP의 col_type과 key_types다. contracts/fixtures/planner.json의 planner coverage는 이제 param 번호 대신 모든 slot의 출처, param 번호, type을 비교하고, 새 case 네 개가 slot 종류를 덮는다: planner_bind_types_select(contains, between, decimal, blind index, days_ago, year, MySQL slot은 text이고 PostgreSQL과 SQLite slot은 bytes인 ip column), planner_bind_types_update(더하기, blind index와 version이 있는 AES, optimistic), planner_bind_types_insert(AES, version, SQLite now slot), planner_parent_key_types(composite key relation). docs/spec/dialects.md(+ko)가 규칙을 적는다. Red: 이전 Go planner와 새 fixture로 -tags featurecoverage TestCoveragePlanner*는 case 열 개 중 여덟 개(compile되는 모든 case. 오류 case 두 개는 통과)가 slot에 type이 없어 실패했다. Green: planner case 열 개가 Go(-tags featurecoverage TestCoveragePlanner*), PHP(coverage_planner.php), TypeScript(coverage_planner.mjs), Rust(engine::coverage_planner, 10 passed)에서 MySQL, PostgreSQL, SQLite 모두 같은 slot으로 통과한다. go test ./engine/...가 통과한다. MySQL, PostgreSQL, SQLite conformance는 26 vectors × 4 languages identical을 보고한다. type이 있는 slot으로 client test가 통과한다: Go ./clients/go/orm ./clients/go/orm/pg ./clients/go/model, PHP model_test.php 66, TypeScript model.mjs 57, Rust workspace 216. 네 언어의 symbol snapshot과 hash를 기록했고 go run ./tests/interfaces/check --self-test가 통과한다. 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-3Rust의 PostgreSQL 값을 plan slot이 선언한 type으로 bind하고 server에 type을 묻지 않는다. Cause: clients/rust/orm/src/driver.rs의 pg_describe는 parameter type을 알려고 새 statement text마다 sqlx Executor::prepare로 prepare했고 db.rs의 pg_types_pool, pg_types_conn이 연결 단위로 cache했다. 그 prepare는 자기 round trip과 transaction이고, sqlx prepare_with는 새 결과 column의 origin도 pg_catalog.pg_attribute에서 읽으므로 server_transactions의 첫 model_statements 실행은 event 8개에 server transaction 15개(8 + prepare 5 + catalog 읽기 2)를 썼다. 변경: pg_describe, pg_types_pool, pg_types_conn과 pg_types cache를 지운다. arg_types는 bind 값마다 그 slot의 dbspec type을 주고(parent slot은 펼친 값에 key_types를 되풀이한다) bind_pg는 그 type의 Rust 값으로 bind하며 sqlx는 그 PostgreSQL type을 Parse에 선언한다(i16 int2 … datetime timestamp, time time, uuid는 새 16 byte PgUuid, slot type 없는 NULL은 type을 정하지 않은 parameter). 어떤 dbspec type도 닿지 않는 INET, JSON/JSONB, TIMESTAMPTZ 분기는 지운다. Utils의 statement는 bind type을 선언한다: key, 이름, 값은 text, AES rotation은 entity schema의 AES, version, key column type이다. raw fragment의 placeholder는 raw fragment가 사라질 때까지 그 값의 type을 쓴다. sqlx는 연결에서 처음 실행하는 statement text를 여전히 자기 round trip으로 prepare하므로 server_transactions는 rust를 새 first_run_prepare_clients에 두고, 그 첫 실행은 event마다 transaction 하나에 statement text마다 하나를 더해야 한다. docs/spec/dialects.md(+ko)가 bind 방식을 적는다. Red: 이전 executor와 새 기대값으로 statement_events_server_transactions는 statement text 5개의 event 8개에 transaction 15개로 실패했다(기대 13). Green: 13, 두 번째 실행 8이다. cargo test --workspace는 216 test가 통과한다. Rust symbol snapshot과 hash를 기록한 뒤 모든 feature coverage의 Rust part(node scripts/features/coverage.mjs --feature <id> --part owner/rust, MySQL, PostgreSQL, SQLite의 18 feature)가 통과한다. PostgreSQL conformance는 26 vectors × 4 languages identical을 보고한다. 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-4probe statement 없이 연결한다: SQLite version은 driver에서 읽고 TypeScript SELECT 1을 지운다. Cause: 연결은 클라이언트가 statement 없이 알 수 있는 것을 server에 물었다. 모든 클라이언트는 새 SQLite 연결마다 SELECT sqlite_version()으로 최소 3.46을 확인했고(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), TypeScript 클라이언트는 연결마다 연결 확인용 SELECT 1을 보냈다(clients/typescript/src/database.ts:406). 네 클라이언트의 runtime 경로(dbspec introspection과 apply는 제외)에는 metadata를 읽는 다른 statement가 없다. model 실행은 그런 statement를 publish하지 않는다. Utils의 상태 API(exists, installed, empty, inspectTable)와 Go BackendWaitingForLock은 manifest에 없는 database 상태를 묻는 것이므로 유지한다. pool 클라이언트의 연결별 prepare는 G5.32-5에 따라 유지한다. 변경: version은 driver가 link한 library에서 읽는다. Go SQLite driver package는 modernc.org/sqlite/lib.SQLITE_VERSION을 새 orm.RegisterSQLiteVersion으로 등록하고, PHP는 PDO::ATTR_SERVER_VERSION, TypeScript는 process.versions.sqlite, Rust는 sqlx가 bundle한 SQLite의 libsqlite3_sys::SQLITE_VERSION을 읽는다. TypeScript connect는 pool의 연결 하나를 열고 statement 없이 돌려주며(reserve와 release), 쓰지 않는 unobserved를 지운다. scripts/repo/probes.mjs의 새 규칙 connectProbeErrors는 make repo-check에서 클라이언트 runtime source를 검사해 SQL sqlite_version()과 TypeScript 'SELECT 1'에서 실패한다. docs/spec/dsl.md(+ko)가 이를 적는다. Red: 이전 source에 규칙을 적용하면 위의 다섯 줄을 보고했다. Green: make repo-check와 그 case a client connects without a probe statement가 통과하고, node tests/go-test.mjs ./clients/go/orm ./clients/go/orm/pg ./clients/go/orm/sqlite ./clients/go/model, TypeScript dsn.mjs 16과 model.mjs 57, PHP dsn.php와 model_test.php 66, Rust workspace 216이 MySQL, PostgreSQL, SQLite에서 통과한다. Go symbol snapshot과 hash를 기록했고 interface self-test가 통과한다. 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-4-1orm의 libsqlite3-sys 의존을 bench/rust/Cargo.lock에 기록한다. Cause: G5.32-4는 clients/rust/orm/Cargo.toml에 libsqlite3-sys를 더했다. clients/rust/Cargo.lock은 이미 그것을 orm 아래에 두었지만, orm crate를 path로 쓰는 bench workspace는 자기 lock file을 가지며 그것을 기록하지 않았으므로 cargo fetch --locked --manifest-path bench/rust/Cargo.toml이 "cannot update the lock file … because --locked was passed"로 멈췄고, make rust-fetch와 그것에 의존하는 모든 target이 실패했다. 변경: cargo update --workspace --offline --manifest-path bench/rust/Cargo.toml은 workspace member의 항목만 갱신해 orm의 의존에 이미 lock된 libsqlite3-sys(0.37.0)를 더하고 어떤 version도 바꾸지 않는다. Red: make rust-fetch가 그 오류로 실패했다. Green: make rust-fetch가 통과하고, 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-5Go PostgreSQL client를 pgx statement cache로 되돌린다(G5.32-1 되돌림). Cause: G5.32-1은 첫 실행만 비교했다. Go pool은 연결을 요청보다 오래 유지하므로 각 연결은 statement text를 한 번 prepare하고 server plan과 함께 재사용한다. 모든 statement를 새 연결에서 실행하는 것은 연결이 요청 하나 동안만 사는 PHP뿐이다. bench database에서 GetBySeq, GetsByServiceSeqAndIsClose, GetCountByServiceSeq와 10행 목록으로 된 page로 PostgreSQL에서 측정했다(연결 4개의 pool, worker 4개, warm-up 200 page 뒤 800~1600 page, 세 번). G5.32-1의 pgx exec query mode는 steady state p50이 24~44% 느렸고(7.8~8.9ms 대 5.5~6.3ms), page당 backend CPU가 22~31% 많았으며(2.9~3.0ms 대 2.2~2.5ms), 처리량이 20~33% 낮았다(초당 386~466 대 572~697 page). 새 연결의 첫 page만 0~9% 빨랐다. MySQL에서 같은 비교(driver 쪽 interpolation 대 server 쪽 prepare)는 부하가 큰 기계의 noise 안에 있었으므로 MySQL 클라이언트는 server 쪽 prepare를 유지하고 MySQL 변경은 넣지 않는다. 변경: client DSN은 더 이상 default_query_exec_mode=exec를 쓰지 않고, DB.stmt와 txConn.stmt는 다시 모든 database에서 prepare한다(modelStatement와 unprepared를 지운다). server_transactions의 기준을 다시 쓴다: 짧은 연결(PHP)은 문마다 round trip 하나이고, pool 클라이언트는 statement text를 연결마다 한 번 prepare하고 재사용한다. first_run_clients는 PHP와 TypeScript(pg는 Parse를 첫 Bind, Execute와 함께 전송한다)이고, first_run_prepare_clients는 Go와 Rust이며 그 첫 실행은 event마다 transaction 하나에 statement text마다 하나를 더한다. Go test가 이를 검사한다. docs/operations/perf.md(F1, +ko)가 규칙, 모든 클라이언트의 첫 실행과 두 번째 실행 수(Go 13 / 8, Rust 13 / 8, TypeScript 8 / 8, PHP 8 / 8), 측정값을 적고, docs/spec/config.md(+ko)는 Go 클라이언트를 다시 named prepared statement 클라이언트로 적는다. Red: 위 측정값이다. Green: TestStatementEventsServerTransactions가 13과 8로, TestParseDSN이 query mode 없는 native DSN으로 통과하고, node tests/go-test.mjs ./clients/go/orm ./clients/go/orm/pg ./clients/go/model이 MySQL, PostgreSQL, SQLite에서 통과하며, Go symbol snapshot과 hash를 기록했고, 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-6timeout과 취소 test의 느린 statement가 다른 연결이 잡은 row lock을 기다리게 한다. Cause: test는 raw 조건으로 statement를 느리게 했다. pg_sleep, SLEEP, MySQL의 MD5 self-join, SQLite의 recursive CTE다(Go clients/go/orm/cancel_test.go TestWithContextCancels와 timezone_test.go TestStatementTimeout, TestStatementTimeoutThroughAPooler, PHP clients/php/tests/model_test.php의 statement timeout과 pooler case, TypeScript clients/typescript/tests/model.mjs의 statementTimeout, pooler, 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). 이 조건은 G5.32가 없애는 raw API와 중립 expression에 없는 함수를 쓴다. 변경: test는 다른 연결을 열어 열린 transaction에서 table의 모든 행을 갱신한다(Go holdRows, PHP holdRows와 pooler case의 다른 process holdRowsFor, TypeScript holdRows, Rust HeldRows). 제한하는 statement는 transaction 안의 locking read(lockedRead, locked_read)이고 MySQL은 max_execution_time, PostgreSQL은 statement_timeout으로 제한한다. 취소하는 statement는 잡힌 행의 update다. MySQL과 PostgreSQL에서 Go 취소는 server가 statement의 lock 대기를 보고할 때까지 기다린다. SQLite는 busy handler에서 lock을 기다리고 취소된 context는 그것을 끊지 않으므로, Go는 TypeScript test처럼 statement 전에 SQLite context를 취소한다. pooler case는 행을 선언된 400 ms 동안 잡아 200 ms 제한을 넘기므로 제한 없는 연결이 제한받지 않음을 보인다. internal/testdb.Open은 SQLite DSN을 SQLite driver로 연다.—Evidence: 바꾼 test가 통과한다: Go ./clients/go/orm(MySQL, PostgreSQL, SQLite의 모든 test), PHP model_test.php 66, TypeScript model.mjs 57, Rust --test zone 11. 어느 것도 raw API나 sleep 함수를 쓰지 않는다. 바뀐 path로 make owner-check가 통과한다.[o]
G5.32-7중립 식 문법의 쿼리 확장 not인 부정 조건 묶음을 더한다. Cause: raw SQL이 맡던 조건은 type 있는 DSL로 쓰며, 그 원소 집합은 dbspec check predicate 집합(비교, in, is null, and, or, 괄호 묶음, column과 type 있는 값)에 between과 고정 column, 값 함수를 더한 것이다. 여기에 not이 없어서 부정 조건은 raw SQL이 필요했다. 변경: IR group은 not을 가진다(Go ir.Group.Not, PHP, Rust, TypeScript not, contracts/interfaces.json의 IRGroup record). not을 가진 하위 묶음은 네 planner에서 NOT (…)로 렌더링되고, not을 가진 top-level where나 on 묶음은 IR_INVALID다. 모델은 not(fn), andNot(fn), orNot(fn)(Rust not, and_not, or_not)을 가진다: Go는 generator template와 Core.AddNegatedGroup, PHP와 TypeScript는 base model, Rust는 fixed.rs.txt와 Core::add_negated_group이다. 네 generator는 그 이름과 column 이름 not을 예약한다. docs/spec/dsl.md(+ko)는 2.1.1절 쿼리 식 문법을 더해 dbspec check 문법과의 관계와 쿼리 전용 확장 세 가지를 적고, docs/spec/plan.md, usage.md, interfaces.md, interface-implementation.md(+ko)가 부정 묶음을 적는다. planner fixture는 planner_not_group과 planner_rejects_top_not을 더하고, conformance vector expression_forms는 네 runner에서 andNot, 맨 앞의 not, orNot을 실행한다. 그 기대값은 다른 26 vector가 기록된 기대값과 일치한 실행에서 MySQL, PostgreSQL, SQLite로 기록했다. Red: 이전 Go engine은 planner_not_group 요청을 IR_INVALID: json: unknown field "not"로 거부했고, runner는 새 method 없이 build되지 않았다. Green: planner case 열두 개가 Go, PHP, TypeScript, Rust에서 통과하고, expression_forms는 세 database에서 네 client의 출력이 같다(PostgreSQL: 428, 3행, 2000). 생성된 Go model을 다시 생성했고 네 언어의 symbol snapshot과 hash를 기록했으며 interface self-test가 통과한다. 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-7-1Go plan cache key에 묶음의 부정을 넣는다. Cause: G5.32-7은 IR group에 not을 더했고, Go client는 IR field를 차례로 hash해 plan cache key를 만든다(shape.go). 그 hash가 Group.Not을 넣지 않아 부정 묶음과 not이 없는 같은 묶음의 key가 같았고, 두 번째 요청은 첫 요청의 plan을 다시 썼다. TestShapeCoversEveryField는 hash가 빠뜨린 field를 잡으며 G5.32-7 뒤에 실패했다. 변경: shape.group이 g.Not을 넣는다. PHP, TypeScript, Rust는 not을 포함하는 직렬화된 IR로 cache key를 만든다. Red: go test ./clients/go/orm -run TestShapeCoversEveryField가 .Query.Relations[0].Query.Relations[0].Query.Where.Not: same shape key as .Query.Relations[0].Query.Relations[0].Query.Where!와 On.Not의 같은 메시지로 실패했다. Green: test가 통과한다. 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-8네 client에서 raw SQL API를 없앤다. Cause: raw 진입점은 SQL text를 IR에 실었고(predicate, 출력 column, 정렬 항목, 할당의 expr과 group_by_expr), planner는 그 text의 ?를 type 없는 slot으로 bind했다(rawParam). 그래서 plan은 모든 slot이 무엇을 bind하는지 말할 수 없었고 Rust 실행기는 그 값을 값 자신의 type으로 bind했다(value_type, UntypedNull). raw API를 없애고, 조건은 쿼리 식 문법(G5.32-7)을 가진 type 있는 DSL로 쓴다. 변경: IR에는 SQL text가 없다. Go ir.Expr, ir.GroupExpr, Query.GroupByExpr, Columns.Expr, Pred.Expr, Order.Expr, Assign.Expr/Ps를 지웠으므로 그 field를 가진 요청은 IR_INVALID(모르는 field)다. PHP Validator record, TypeScript ir.ts, Rust ir.rs, contracts/interfaces.json의 IRExpr, IRGroupExpr record와 field도 따른다. 네 planner에서 renderExpr, placeholder 채우기(fillPlaceholders, fill), type 없는 rawParam/raw_param slot이 사라지고, dialect에서는 조각만 쓰던 CurrentTime과 $CURRENT_TIME token이 사라진다. client에서는 raw, andRaw, orRaw, orderByRaw, groupByRaw, setRaw<Col>, addRawColumn<Alias>와 addColumn<Col>Alias<Name>의 서식 문자열이 사라진다. addColumn<Col>Alias<Name>은 이제 column 함수만 받는다(Go orm.Func, PHP Func, TypeScript ColumnFunction, Rust orm::Func). 그 core 함수도 사라진다(Go Core.Raw, AddColumnFormat, AddRawColumn, OrderByRaw, GroupByRaw, SetRaw; PHP Request::raw; TypeScript Core.raw, addRawColumn, addColumnFormat(이제 addColumnFunc); Rust Core::raw, add_column_format, add_raw_column, order_by_raw, group_by_raw, set_raw, add_column_as, ColumnFormat, IntoColumnFormat, Binds, args::bind). 네 generator는 그 이름을 만들지도 예약하지도 않으며 raw는 더 이상 예약 column 이름이 아니다. Go와 TypeScript model을 다시 생성했다. Rust 실행기는 PostgreSQL 값을 slot의 type으로만 bind한다: value_type과 UntypedNull을 지웠다. 서식 column을 더하던 conformance vector columns는 이제 addColumnStartDtAliasStartYear(year())를 더한다. 이 vector가 Rust client는 YEAR(col)이 돌려주는 MySQL YEAR cell을 읽지 못함을 보였으므로(sqlx의 i64는 YEAR를 받지 않는다) row.rs는 year_cell로 그 type 검사 없이 YEAR를 decode한다. Test: engine은 TestRawFormsRejected(predicate, column, 정렬, group_by_expr, 할당 형식이 IR_INVALID)를 더하고, TestRandomOrder와 TestBinaryContains가 두 raw case를 대신하며, TestAggregates의 group_by_expr case는 column 두 개의 group_by가 된다. TestJoinAliasNamespaces는 column을 add로 고른다. client test는 raw 조건과 정렬 대신 geReadCount(20), seq(x), orderBy<Col>Asc()를 쓰고 column과 subquery case의 raw column과 서식 column을 뺀다(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). conformance vector raw_forms와 네 runner의 그 block을 지웠고, columns를 MySQL, PostgreSQL, SQLite에서 다시 기록했으며, check run|compare|record는 -vector NAME을 받는다(tests/conformance/README.md, +ko). contracts/features.json은 입력 raw_sets를 뺀다. docs/spec/dsl.md, protocol.md(G5.32-7이 빠뜨린 group의 not도 적는다), dialects.md, plan.md, usage.md(+ko)에서 raw 형식을 뺐다. Red: main의 engine에서 TestRawFormsRejected는 "kind":"all","entity":"place","where":{"items":[{"pred":{"expr":"{price} > ?","ps":[0]}}]}: want IR_INVALID, got <nil>로 실패했고, YEAR decode 전에는 Rust runner가 MySQL의 columns에서 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/, Go planner와 shape case(TestCoveragePlanner*, TestShapeCoversEveryField), PHP, TypeScript, Rust의 planner case 열두 개(coverage_planner), Rust orm-build generate test가 통과한다. go run ./tests/conformance/check run -vector columns는 세 database에서 1 vector × 4 languages identical을 보고한다. 네 언어의 symbol snapshot과 hash를 기록했고 component 페이지를 생성했으며 go run ./tests/interfaces/check --self-test가 통과한다. 바뀐 path로 make owner-check가 통과한다.——[o]
G5.32-9server_transactions의 TimeZone source는 server에 바로 연결할 때만 비교한다. Cause: G5.31은 statement event 밖의 SET 문장으로 time zone을 바꾸는 client를 잡으려고 TimeZone의 pg_settings source가 client이기를 요구했다. PgBouncer를 거치면(client-pooler-check) client는 여전히 TimeZone을 startup parameter로 보내지만, pooler가 client의 startup parameter를 공유하는 server connection에 SET으로 적용하므로 client가 무엇을 하든 source는 session이다. 요구는 연결이 요청마다 문장 없이 UTC로 실행되는 것이다. setting과 transaction 수는 두 경로에서 이를 보이고, source는 client가 server에 직접 닿을 때만 startup parameter를 보인다. PgBouncer는 client parameter를 server connection의 startup parameter로 전달할 수 없으므로(track_extra_parameters는 SET으로 적용할 parameter를 더할 뿐이다) test server는 그대로 둔다. 변경: case는 환경에서 경로를 읽고(pooler를 거치면 ORM_TEST_POSTGRES_DSN이 ORM_TEST_POSTGRES_SERVER_DSN과 다르다) 직접 연결에서만 source를 비교한다. Go(TestStatementEventsServerTransactions), PHP(statement_events_run.php), TypeScript(statement-events-run.mjs), Rust(statement_events_server_transactions)가 그렇고, tests/events/vectors.json의 server_transactions note가 이를 적는다. Red: ORM_TEST_PGBOUNCER_DSN으로 Go case는 TimeZone setting and source = UTC, session, want UTC, client로 실패했고(transaction은 직접 경로처럼 13과 8), main의 PHP와 TypeScript case는 TimeZone setting and source = [["UTC","session"]]로 실패했다. Green: 네 case가 직접 경로와 PgBouncer에서 같은 수로 통과한다(Go 13과 8, PHP와 TypeScript 8과 8, Rust 통과). 바뀐 path로 make owner-check가 통과한다.——[o]
G5.33서로 다른 checkout과 session의 실행이 함께 쓰는 test 자원을 서로 망가뜨리지 않게 한다. 여러 실행이 동시에 쓰는 자원은 lease로 다룬다: 쓰는 실행은 shared lease를 가지고, 자원을 지우거나 다시 만드는 일은 exclusive lease를 가지며 그것은 lease가 하나라도 있으면 보유자를 적고 거부된다. 한 실행만 쓸 수 있는 자원은 보유자가 하나다. process가 없어진 보유자의 lease는 보고하고 명시적으로만 지운다. 그 밖의 것은 실행마다 따로 둔다. 하위 항목 G5.33-1에서 G5.33-11이 test server, bench와 decimal database, Makefile 밖의 build와 test binary를 포함한 공유 Rust target directory, 남은 실행 file, TypeScript build 출력과 그것을 읽는 쪽, lease program의 경쟁을 다룬다.—증거: G5.32-3 위로 rebase한 branch의 path 32개로 make owner-check가 make target 10개를 골랐고, 9개가 통과했으며(test-servers-check 1 m 29 s, docs-verify-idempotent 55 s), repo-check는 남아 있던 BENCH_SQLITE_DSN 기대로 실패했다. G5.33-2의 둘째 commit이 그것을 고쳤고, 그 뒤 make repo-check가 case 54개를 통과한다. 둘째 묶음(G5.30-14, G5.33-5에서 G5.33-10)의 증거: G5.32-5 위로 rebase한 branch의 path 22개로 make owner-check가 자기 실행의 database와 함께 기능 9개의 검증 명령 72개와 coverage 단위 35개, helper check 1개, make target 11개를 골랐고 단계 32개가 모두 통과했다(가장 긴 것은 authenticated_encryption/verification 2 m 5 s). 첫 실행은 G5.33-10이 고친 lease 경쟁으로 dsn-mysql-tls-rust에서 멈췄고, 둘째 실행은 disk가 3 GB 아래로 내려가 멈췄다. scripts/repo/check.mjs도 바꾼 G5.32-4 위로 rebase한 뒤 node --test scripts/repo/check.test.mjs가 case 56개를 통과한다. G5.33-2 이후 어떤 실행도 쓰지 않는 공유 bench와 decimal 자원은 그것을 쓰는 연결과 열린 file이 없음을 확인한 뒤 실행 중인 서버에서 지웠다: MySQL과 PostgreSQL의 database orm_bench(134 MB와 181 MB)와 orm_decimal_case(replica가 삭제를 적용했다), .runtime/servers/orm_bench.sqlite(109 MB), .runtime/servers/send-savepoint.sqlite, .runtime/decimal-env, .runtime/decimal-case.sqlite, .runtime/decimal-owned이다. orm_bench.sqlite는 G5.33-2가 11:46에 main에 들어가고 서버 환경 file이 11:47에 다시 쓰이기 전인 11:33에 마지막으로 쓰였고, 그때 그 file은 아직 BENCH_SQLITE_DSN으로 그것을 정했다. 이제 그것을 적는 orm 경로는 없고, 아직 공유 bench database를 설명하던 scripts/test-servers.sh의 머리말은 고쳤다. 셋째 묶음(G5.30-15, G5.33-8, G5.33-9)의 증거: G5.32-4-1 위로 rebase한 branch의 path 28개로 make owner-check가 자기 실행의 database와 함께 기능 3개의 검증 명령 2개와 coverage 단위 6개, make target 10개를 골랐고 단계 18개가 모두 통과했다(owners/make/testcase-check 1 m 7 s, owners/interface_contract/coverage 1 m 5 s). G5.33-11은 각 실행의 환경에 send-savepoint SQLite file을 준다. G5.33-11과 G5.34-1의 path 7개로 make owner-check가 make target 7개를 모두 통과했다.[o]
G5.33-1test server에 lease를 둔다. 원인: make test-servers의 서버(고정 port 33171, 55471, 33181, 55481, 33182, 55482의 .runtime/servers)는 모든 checkout의 실행이 TEST_ENV로, 다른 도구도 함께 쓰지만, 누가 쓰는지 기록하는 것이 없었다. 그래서 다른 실행이 case를 실행하는 중에 make test-servers-stop이 서버를 멈추고 .runtime/servers를 지웠고, make test-servers 안의 scripts/test-servers-mysql.mjs MySQL migration이 MySQL을 멈추고 data를 옮겼다. 이제 새 Go 명령 tests/lease(.runtime/bin/lease, Makefile이 $(RUN_LONG) go-build/lease로 build)는 directory의 lease를 보유자의 checkout, process id, 시작 시각, 명령을 담은 file로 원자적으로(shared는 rename, exclusive는 link) 쓴다. hold는 주어진 process가 끝날 때까지 lease를 가지고 그 종료를 internal/procevent(G5.30-13-1의 종료 감시를 옮긴 것과 directory 감시: darwin은 kqueue, linux는 inotify)로 관측한다. run은 명령이 실행되는 동안 lease를 가지고 --wait이면 거부 대신 directory 변경 알림을 기다린다. list는 보유자를 적고 clear-dead는 process가 없어진 lease를 지우며 그것을 적는다. 보유자 process가 없는 lease는 결코 가져가지 않고, 그런 lease가 막으면 --wait도 곧바로 거부한다. scripts/test-servers.sh는 환경 file에 ORM_TEST_SERVERS_LEASES(.runtime/servers.leases)를 쓴다. TEST_ENV를 읽는 모든 make 줄이 시작하는 WITH_TEST_ENV(이제 make check와 make owner-check도)는 그 줄 동안 shared lease를 가지고, 환경 file에 lease directory가 없으면 고치는 방법을 적으며 실패한다. stop, 새 start, MySQL migration(첫 변경 전, 이어 갈 때도)은 exclusive lease를 가지며 보유자마다 적고 종료 코드 3으로 실패한다. make test-servers-leases와 make test-servers-leases-clear는 lease를 적고 보유자 없는 것을 지운다. CONTRIBUTING.md와 한국어 짝이 lease를 설명한다. 실행 중인 서버는 다시 시작하지 않았다. 그 환경 file은 실행 중인 서버를 그대로 두는 다음 make test-servers에서 lease directory를 얻는다. Red: main의 script에 새 case를 실행하면 scripts/test-servers.test.mjs의 stopping the test servers is refused while another run holds a lease, naming the holder는 lease가 있는 동안 정지가 test-servers: removed .../.runtime/servers를 출력해 실패했고, scripts/test-servers-mysql.test.mjs의 the MySQL migration is refused while another run holds a lease of the servers는 일회용 MySQL fixture의 migration이 모든 단계를 실행해 실패했다. TEST_ENV를 읽고 6 s를 쉬는 main Makefile의 make 줄 동안 exclusive lease는 비어 있었다(exclusive-ran-during-main-run). Green: 두 case가 통과한다(113 ms, 그리고 port 39305와 39306의 MySQL fixture로 15.2 s: marker는 그대로 lower_case_table_names=2이고 .runtime/mysql-migration이 없다). 정지 case는 보유자가 끝난 뒤 정지가 진행되는 것도 확인한다. 이 Makefile의 같은 make 줄 동안 lease run <servers.leases> exclusive는 shared lease of pid ... (running) from .../orm-shared-resources-G5.33 since ...: /bin/sh -c ... sleep 6을 적으며 종료 코드 3으로 거부되었고, 줄이 끝나자 lease가 없어졌다. lease와 함께 make dialect-facts-check가 통과하고, lease directory가 없는 환경 file은 names no lease directory; run make test-servers to rewrite it으로 실패한다. node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-2실행마다 자기 bench database와 decimal database를 둔다. 원인: make test-servers는 모든 실행이 함께 쓰는 bench database(MySQL과 PostgreSQL의 orm_bench, .runtime/servers/orm_bench.sqlite)를 seed했고, 그 환경 file이 그것을 BENCH_*와 ORM_BENCH_MYSQL_DSN으로 정했다. Makefile은 decimal database 하나(DECIMAL_ENV = .runtime/decimal-env, orm_decimal_case, make decimal-db-setup이 준비)를 정했다. make check만 자기 것을 만들었다(scripts/check/run.mjs). make rust-driver-check나 make perf-check처럼 직접 실행한 target과, 공유 DECIMAL_ENV와 server 환경을 target과 명령에 넘긴 make owner-check(scripts/features/owners.mjs)는, 다른 실행과 scripts/bench-db.sh가 그 database를 지우고 다시 만드는 동안 그것을 쓰거나 읽었다. 이제 함께 쓰는 bench나 decimal database는 없다. make test-servers의 환경 file은 ORM_RUN_MYSQL_DSN, ORM_RUN_POSTGRES_DSN(database orm_run, scripts/check/databases.sh가 바꾸는 이름)과 ORM_RUN_SQLITE_QUERY만 export하고, 시작은 bench database를 seed하지 않는다. databases.sh는 그것으로 실행의 DSN을 만든다. DECIMAL_ENV에는 기본값이 없고 decimal-db-setup은 없어졌다. make owner-check는 기능, helper, server 환경을 읽는 make target(usesTestEnv)을 실행할 때 databases.sh로 자기 실행의 database를 만들고, 그 환경 file(environmentFile)을 명령의 환경으로 읽고, target에는 TEST_ENV와 DECIMAL_ENV로 넘기며, 끝에 지운다. 새 make run-databases TARGETS="..."는 target을 직접 실행하되 scripts/check/run.mjs로 자기 database와 함께 실행한다. 그것을 읽는 target을 server 환경만으로 실행하면 실패한다(ORM_BENCH_MYSQL_DSN is required ..., decimal-physical-check는 make run-databases를 적는다). CONTRIBUTING.md, tests/conformance/README.md와 한국어 짝이 그것을 설명한다. 실행 중인 서버의 옛 orm_bench database는 그대로 두었고, 그 환경 file은 다음 make test-servers에서 ORM_RUN_* 변수를 얻는다. Red: scripts/test-servers.test.mjs의 새 case the server environment names no bench or decimal database that runs share가 main의 script와 Makefile에서 the server environment names a shared bench database로 실패했다. Green: 그 case와 scripts/features/owners.test.mjs의 owner-check creates the databases of its run for a target that reads the server environment가 통과한다. ORM_RUN_* 변수를 담은 환경 file로 make run-databases TARGETS=perf-check는 orm_check_99833_ec8f23fe_bench와 _decimal을 만들어 seed하고(2 m 12 s), 그것으로 perf-check를 통과하고(23.3 s), 지웠다(1.5 s). repository case the CI workflow provides every variable of make test-servers는 make owner-check가 찾은, 아직 남아 있던 BENCH_SQLITE_DSN 대신 ORM_RUN_MYSQL_DSN, ORM_RUN_POSTGRES_DSN, ORM_RUN_SQLITE_QUERY, ORM_TEST_SERVERS_LEASES를 기대한다. 그 환경만으로 실행한 make perf-check는 ORM_BENCH_MYSQL_DSN is required; it names the seeded bench database, and bench tests never skip으로 실패한다. node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-3make target의 Rust program은 자기 복사본으로 실행하고, 공유 Rust target directory로의 build는 lease 아래에서 한다. 원인: 모든 checkout이 하나의 CARGO_TARGET_DIR로 build하고(disk가 빠듯해 함께 쓴다), make target 여섯 개가 그곳에 program을 build한 뒤 그곳의 program을 실행했다: dbspec-apply-pairs-check(debug/examples/dbspec_apply), dbspec-introspect-compare-check(dbspec_introspect), dbspec-compare-check(tests/cargo-target.mjs를 거친 dbspec_compare, dbspec_stress, dbspec_apply), dbspec-stress-bench와 timing-check(release/examples/dbspec_stress), example-check(debug/complex, debug/demo), case-database-check(debug/integration)이다. build와 실행 사이에 다른 checkout이 program을 다시 build할 수 있어 실행이 다른 checkout의 code를 실행했다. stress 문서는 target directory에 >로 썼고($(CARGO_TARGET_DIR)/dbspec/stress*.dbs), 두 실행이 서로의 것을 덮어썼다. 이제 RUN_DIR(.runtime/run/<target>-<make pid>)은 target 실행 하나의 file을, RUN_TARGET은 그 program의 복사본을 target directory와 같은 배치로 담는다. CARGO_COPY는 build를 실행하고 scripts/cargo-build-copy.sh가 이름 붙인 artifact를 RUN_TARGET으로 복사하며, 둘 다 $(CARGO_TARGET_DIR)/.leases의 exclusive lease(tests/lease run --wait, 다른 build가 가지면 directory 변경 알림을 기다린다) 아래에서 한다. 실행은 복사본을 쓴다(DBSPEC_APPLY_RUST, DBSPEC_INTROSPECT_RUST, EXAMPLE_RUST_*, TIMING_RUST_STRESS, 그리고 program을 그것으로 찾는 script에는 CARGO_TARGET_DIR=$(RUN_TARGET)). stress 문서는 각 target이 끝에 지우는 RUN_DIR에 쓴다. Makefile의 다른 모든 cargo build, check, clippy도 같은 lease를 가지므로(CARGO_LEASED) build와 복사 사이에 다른 build가 끼지 않는다. make lease-tool이 lease program을 먼저 build한다(rust-fetch, case-database-check, rust-check가 그것에 기댄다). make repo-check가 실행하는 scripts/repo/testcases.mjs의 sharedTargetErrors는 주석 밖에서 $(CARGO_TARGET_DIR)/ 아래 경로(lease directory는 빼고)를 쓰는 Makefile 줄과 $(CARGO_LEASED)나 $(CARGO_COPY) 없는 cargo build를 실패시킨다. Red: main의 Makefile에 sharedTargetErrors는 오류 31개를 돌려주었다: target directory 아래 경로 9개(첫째는 DBSPEC_STRESS_DOCUMENT = $(CARGO_TARGET_DIR)/dbspec/stress.dbs)와 lease 없는 build 22개다. Green: scripts/repo/check.test.mjs의 새 case the Makefile runs no program from the shared Rust target directory and builds there under its lease가 통과한다. main checkout의 target directory로 실행한 make dbspec-compare-check는 다른 process가 그 .leases의 exclusive lease를 8 s 가진 동안 첫 build 전에 lease: waiting for the exclusive lease of .../clients/rust/target.leases; held by: ...를 출력했고, debug/examples/dbspec_compare, dbspec_stress, dbspec_apply를 자기 실행 directory로 복사해 그 복사본으로 case 168개를 모두 통과했으며, .runtime/run directory를 남기지 않았다. lease directory를 target directory 안으로 옮긴 뒤 같은 target이 다시 실패 없이 통과했다. node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-4남은 실행 file을 실행마다 따로 두고, checkout의 TypeScript build 출력에 보유자를 하나만 둔다. 원인: SEND_SQLITE_DSN은 TEST_ENV 옆의 send-savepoint.sqlite(직접 실행이면 모두 .runtime/servers/)를 정했고 WITH_TEST_ENV가 모든 줄에 그것을 export했다. timing-check는 Go test binary를 .runtime/timing/dbspec.test로 build했다. 두 실행이 같은 file을 썼다. TypeScript client의 build 출력(clients/typescript/dist와 생성한 model)은 make target 18개가 다시 build하므로, 같은 checkout의 다른 실행이 그것을 다시 쓰는 동안 한 실행이 그것을 실행할 수 있었다. 이제 SEND_SQLITE_DSN은 $(RUN_DIR)/send-savepoint.sqlite이고, rust-send-savepoint-check만 그것을 ORM_SEND_SQLITE_DSN으로 넘기고 끝에 지운다. TIMING_GO_DBSPEC_TEST는 $(RUN_DIR)/dbspec.test다. TypeScript client를 build하는 모든 make target은 $(HOLD_TYPESCRIPT)로 시작하고, 그것은 make process 동안 .runtime/typescript.leases의 exclusive lease를 가진다(tests/lease hold --wait --pid, 새로 directory 변경 알림을 기다리고, 이미 exclusive lease를 가진 process는 그것을 그대로 쓴다). make repo-check는 server 환경, 도구, 실행 directory, lease directory 밖의 고정 .runtime file이나 TEST_ENV 옆의 file을 정하는 Makefile 줄(runtimePathErrors)과, $(HOLD_TYPESCRIPT)로 시작하지 않고 TypeScript client를 build하는 target(typescriptHolderErrors)을 실패시킨다. clients/rust/README.md와 한국어 짝이 SQLite file을 설명한다. Red: main의 Makefile에 runtimePathErrors는 SEND_SQLITE_DSN, DECIMAL_ENV, TIMING_GO_DBSPEC_TEST를, typescriptHolderErrors는 target 18개를 돌려주었다. Green: scripts/repo/check.test.mjs의 새 case the files of one run live in its run directory and a TypeScript build holds its output이 통과한다. 한 checkout에서 1.5 s 간격으로 시작한 make typescript-build 두 개 가운데 둘째는 lease: waiting for the exclusive lease of .../.runtime/typescript.leases; held by: ...를 출력하고 첫째가 끝난 뒤에야 build했다(19.4 s와 10.7 s). make rust-send-savepoint-check는 자기 실행 directory의 SQLite file로 case 3개를 두 번 통과했고 .runtime/run directory를 남기지 않았다. node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-5모든 cargo test를 그 test binary의 복사본으로 실행한다. 원인: G5.33-3 뒤에도 각 cargo test 실행은 cargo test --no-run build를 따른 뒤 cargo가 공유 target directory의 test binary를 실행하게 두었다: Makefile 11줄(testcase-check, dbspec Rust target, rust-send-savepoint-check, rust-driver-check), contracts/features.json 검증 명령 11개의 실행 14개, scripts/client-db-test.sh의 Rust lane이다. 그 사이의 다른 checkout build나, checkout을 구별하지 못하는 cargo의 fingerprint(G5.22-1) 때문에 실행이 다른 checkout의 test를 실행했다. 이제 tests/cargo-test.mjs <name> -- cargo [+toolchain] test ...는 같은 option으로 장기 작업 rust-build/<name>(lease run <CARGO_LEASES> exclusive --wait 아래의 cargo test --no-run --message-format=json-render-diagnostics, compiler 출력을 단계 줄로, 기한 없음)을 build하고, 그 lease 안에서 cargo가 알린 모든 test binary를 .runtime/run/cargo-test-<pid>-<random>으로 복사한 뒤, 복사본마다 package directory에서 CARGO_MANIFEST_DIR, test 이름 filter, -- 뒤의 인자로 실행하고, 처음 실패한 binary의 종료 코드로 멈추며, 복사본을 지운다. Rust source에는 실행할 doctest가 없다(code block이 text다). Makefile(CARGO_TEST, LEASE와 CARGO_LEASES를 export), 검증 명령, client-db-test.sh는 모든 cargo test 실행을 그것으로 하고, 따로 있던 --no-run build는 없어졌다. make repo-check의 unbuiltCargoTestErrors는 이제 tests/cargo-test.mjs 밖의 cargo test 실행을 실패시킨다. tests/cargo-test.test.mjs(testcase-check)는 인자 나누기와 cargo message 읽기를 검사한다. Red: /.runtime/run/에서 실행되지 않으면 실패하는 test를 가진 fixture crate에서 cargo test --test probe가 ran the test binary of the shared target directory: /tmp/orm-ct.bjSa/target/debug/deps/probe-d379e64323e50d0a로 실패했다. 이 변경 전 tree에 새 규칙은 검증 명령 실행 14개와 scripts/client-db-test.sh를 보고했다. Green: 같은 crate에서 node tests/cargo-test.mjs probe -- cargo test --test probe -- --nocapture는 RUN rust-build/probe no-deadline, 복사 줄, cargo-test: running probe (.../.runtime/run/cargo-test-13938-e6c8d95e/0-probe-...), CARGO_MANIFEST_DIR /private/tmp/orm-ct.VkhR/crate, test result: ok. 1 passed를 출력하고 0으로 끝났으며 실행 directory를 남기지 않았다. node --test tests/cargo-test.test.mjs는 case 2개, node --test scripts/repo/check.test.mjs는 case 53개를 통과하고 node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-6Makefile 밖에서도 공유 Rust target directory로의 build를 그 lease 아래에서 한다. 원인: G5.33-3은 Makefile의 build에만 lease를 두었다. contracts/features.json의 helper rust-test-helpers와 명령 chunking-rust, scripts/features/coverage.mjs의 coverage build(cargo test --no-run --workspace, 그 뒤 그 test binary를 target directory에서 실행했다), scripts/client-db-test.sh의 integration build, scripts/package-check.sh와 scripts/check-rust-150.sh의 cargo check는 lease 없이 build했고, chunking-rust와 client-db-test.sh는 target directory의 ${CARGO_TARGET_DIR}/debug/integration을 실행했다. 이제 각각 "$LEASE" run "$CARGO_LEASES" exclusive --wait -- 아래에서 실행한다(Makefile이 둘을 export하고, package-check와 rust-150-check는 lease-tool에 기댄다). chunking-rust와 client-db-test.sh는 scripts/cargo-build-copy.sh로 .runtime/run/...에 build한 복사본을 실행하고 지운다. coverage.mjs는 각 binary의 dep-info도 복사하는 tests/cargo-test.mjs --copy로 build하고 자기 임시 directory의 복사본을 실행한다. 복사는 file system이 지원하면 copy-on-write clone이다(COPYFILE_FICLONE, macOS cp -c, Linux cp --reflink=auto): 500 MB를 세 번 clone해도 disk를 쓰지 않았고 보통 복사는 500 MB를 썼다. make repo-check의 unleasedCargoErrors는 검증 명령, root package.json script, 그것들이 실행하는 모든 script에서 lease 없는 cargo build와 CARGO_TARGET_DIR 아래 program의 실행을 실패시킨다. Red: 이 변경 전 tree에 그것은 오류 7개를 돌려주었다: chunking-rust, rust-test-helpers, client-db-test.sh, package-check.sh, check-rust-150.sh의 build와 chunking-rust, client-db-test.sh의 target directory 실행이다. Green: 새 case commands outside the Makefile build into the shared Rust target directory only under its lease가 통과한다. 자기 실행의 database로 coverage.mjs --feature dsn_connection --part owner/rust는 build(1 m 59 s, test binary 77개 복사)와 MySQL, PostgreSQL, SQLite의 두 실행을 복사본으로 통과했다. check.mjs --run --feature parameter_chunking --command chunking-rust는 debug/integration을 자기 실행 directory로 복사하고 통과했다(26.5 s). node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-7TypeScript build 출력을 읽는 동안에도 그것을 가진다. 원인: G5.33-4는 TypeScript client를 build하는 target에 .runtime/typescript.leases의 exclusive lease를 주었지만, client의 dist로 clients/typescript/tests/decimal_model_db.mjs를 실행하는 decimal-physical-check는 lease 없이 그것을 읽었고, conformance-check는 lease 없이 그것을 build했으며(tests/conformance/check run 안의 npm run build), make owner-check의 기능 단계는 lease 없이 그것으로 TypeScript test와 coverage를 실행했다. 그래서 checkout의 다른 실행이 그 밑에서 그것을 다시 쓸 수 있었다. 보유자는 자기 하위 process도 막았다: feature-check가 exclusive lease를 가지고, 그 검증 명령이 다른 make process에서 그것을 다시 요구하는 make dbspec-ts-check와 make dbspec-compare-check를 실행한다. 이제 READ_TYPESCRIPT는 읽는 target의 make process 동안 shared lease를 가지고(읽는 target끼리는 함께 실행되고, build하는 target은 그것이 끝날 때까지 기다린다), decimal-physical-check는 그것으로, conformance-check는 HOLD_TYPESCRIPT로 시작한다. scripts/features/owners.mjs는 그 단계 일부가 출력을 build하므로 첫 helper나 기능 단계부터 끝까지 exclusive lease를 가진다. tests/lease는 그 process나 조상 process가 이미 exclusive로 가진 lease를 가진 것으로 보고, 조상은 ps -o ppid=로 찾는다. make repo-check는 client의 dist를 직접, 상대 import로, 또는 node가 있는 줄의 clients/typescript file 실행으로 쓰는 file을 node로 실행하면서 $(READ_TYPESCRIPT)나 $(HOLD_TYPESCRIPT)로 시작하지 않는 target을 실패시키고(typescriptReaderErrors), typescriptHolderErrors는 tests/conformance/check run도 build로 본다. Red: 이전 commit의 Makefile에 두 규칙은 decimal-physical-check와 conformance-check를 보고했다. 이전 commit의 lease program에서는 exclusive lease를 가진 shell의 하위 process가 거부되었다(lease: refused the exclusive lease of ...; held by: exclusive lease of pid 1805 (running) ...). Green: 새 case a target that runs code using the TypeScript build output holds it가 통과한다. 이 Makefile을 include한 probe Makefile에서, 4 s를 쉬는 build target보다 1.5 s 늦게 시작한 읽는 target은 lease: waiting for the shared lease of .../.runtime/typescript.leases를 출력하고 3 s 뒤에 실행했고, recipe에서 하위 make로 다른 build target을 실행하는 build target은 기다리지 않고 inner-ran-under-outer-hold를 출력했다. node --test scripts/repo/check.test.mjs와 node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-8Go checker의 Rust program은 target lease 아래에서 build하고 복사본을 실행한다. 원인: tests/conformance/check(run의 conformance/build와 physical test TestPhysicalResultRunners, TestPhysicalRustGroupBoolean)는 공유 Rust target directory의 lease 없이 cargo build ... --bin conformance를 실행하고 $CARGO_TARGET_DIR/debug/conformance를 실행했고, tests/interfaces/check는 orm-interface-symbols를 같은 방식으로 build해 cargo가 알린 target directory의 실행 file을 실행했다. rust_tool_test.go는 1분짜리 test case 안에서 그 추출기를 임시 target으로 build했다. 또 scripts/check/run.mjs는 하위 make에 CARGO_TARGET_DIR을 넘기지 않아, worktree의 make run-databases가 worktree 자신의 target directory로 build했다. 이제 buildRustRunner와 buildRust는 lease run <CARGO_LEASES> exclusive --wait -- sh scripts/cargo-build-copy.sh <run dir> <artifact> -- cargo build ...를 실행하고 복사본을 실행한다(rustRunner, 끝에 지우는 임시 directory의 추출기). 두 physical test는 testcase.RunLong으로 test의 directory에 build한다. interface-check가 그 build를 실행하므로 rust_tool_test.go는 없어졌다. interface-check, conformance-rust-group-check, conformance-result-physical-check는 lease-tool에 기대고, 마지막 것은 TypeScript build 출력도 가진다. run.mjs는 자기 CARGO_TARGET_DIR을 make command line으로 넘긴다. make repo-check의 goCargoErrors는 lease와 복사 script 없이 cargo를 실행하는 Go file을 실패시킨다. Red: 이전 commit의 file에 goCargoErrors는 tests/conformance/check/main.go, rust_group_physical_test.go, tests/interfaces/check/main.go를 보고했다. 이 worktree의 첫 make run-databases TARGETS=conformance-rust-group-check는 worktree의 clients/rust/target에 957 MB를 build했다. Green: main의 target directory로 make interface-check는 cargo-build-copy: .../orm/clients/rust/target/debug/orm-interface-symbols copied to /var/folders/...를 출력하고 모든 case를 통과했다. make run-databases TARGETS=conformance-rust-group-check는 .../orm/clients/rust/target/debug/conformance를 복사하고(PASS conformance/rust-build elapsed=39.87s), 그 복사본으로 MySQL, PostgreSQL, SQLite에서 TestPhysicalRustGroupBoolean을 통과했으며, worktree에 target directory를 남기지 않았다. node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-9Rust test가 package program의 경로를 실행될 때 받게 한다. 원인: Rust test 7개(bench/rust/tests/dsn.rs, seeded.rs, 그리고 clients/rust/tests의 client_bench_args.rs, client_bench_dsn.rs, coverage_conformance_verification.rs, coverage_performance_gate.rs, example_dsn.rs)의 12줄이 실행할 program을 env!("CARGO_BIN_EXE_<name>")에서 얻었고, cargo는 그것을 compile 시점에 공유 target directory의 경로로 정한다. 그래서 tests/cargo-test.mjs(G5.33-5)가 복사한 test binary도 다른 checkout이 그곳에서 다시 build할 수 있는 program을 실행했다. 이제 orm_testcase::program(name)은 test가 실행될 때 ORM_PROGRAM_<NAME>을 읽고, 값이 없으면 그 변수 이름을 적으며 panic한다. tests/cargo-test.mjs --copy는 cargo가 test와 함께 build한 package program(programs)도 같은 lease 아래에서 실행 directory로 복사해 ORM_PROGRAM_<NAME>으로 넘기고(programEnvironment), scripts/features/coverage.mjs는 자기가 실행하는 test binary에 같은 변수를 준다. make repo-check의 binExeErrors는 주석 밖에서 env!("CARGO_BIN_EXE_를 쓰는 Rust 줄을 실패시킨다. bench Rust test는 이 tree에서 실행할 수 없었다: main에서도 cargo fetch --locked --manifest-path bench/rust/Cargo.toml이 cannot update the lock file로 실패해 rust-fetch가 실패한다. Red: 이전 commit의 file에 binExeErrors는 12줄을 보고했다. 변경 뒤 그냥 실행한 cargo test --test client_bench_args는 ORM_PROGRAM_CLIENT_BENCH is unset; run the test through tests/cargo-test.mjs, which gives the copied program으로 실패한다. Green: main의 target directory로 node tests/cargo-test.mjs programs-probe -- cargo test ... --test client_bench_args --test example_dsn은 client_bench, complex, demo와 다른 package program을 실행 directory로 복사하고, 복사본으로 두 test의 case 4개를 통과했다. tests/cargo-test.test.mjs와 scripts/repo/check.test.mjs의 새 case가 통과하고 node scripts/repo/check.mjs가 통과한다.——[o]
G5.33-10막 풀린 lease를 보유자 없는 lease와 구별한다. 원인: tests/lease는 거부될 때 막는 보유를 먼저 읽고 그 뒤에 그 process가 살아 있는지 물었다. 그 사이에 자기 file을 지우고 끝난 보유자는 보유자 없는 보유로 보고되었고 --wait는 그것을 곧바로 거부했으며, 이미 exclusive.json을 다시 만든 새 보유자는 옛 보유자로 오인되었다. make owner-check 중에 dsn-rust가 target lease를 막 풀었을 때 dsn-mysql-tls-rust가 exclusive lease of pid 64521 (dead) ... a dead lease is never taken over로 그렇게 실패했다. 이제 거부는 생존 확인 뒤에 막는 file이 아직 같은 보유자(process id와 시작 시각)를 담는지 다시 본다. 그 사이에 풀린 보유면 기다리는 쪽이 곧바로 다시 시도하고, 끝난 process를 아직 적은 보유만 보유자 없는 보유로 본다. Red: 이전 commit의 lease program으로 lease run <dir> exclusive --wait -- true 두 개를 함께 시작하는 200회 가운데 102회가 거부되었다. Green: 300회씩 두 번 실행해 거부가 없었다.——[o]
G5.33-11모든 실행의 Rust client database test에 send-savepoint SQLite file을 준다. 원인: G5.33-4는 SEND_SQLITE_DSN을 rust-send-savepoint-check의 실행 directory로 옮기고 WITH_TEST_ENV가 ORM_SEND_SQLITE_DSN을 export하지 않게 했지만, client-db-check의 Rust lane(scripts/client-db-test.sh, tests/cargo-test.mjs를 거친 cargo test --workspace)도 tx::send_tests를 실행한다. 그래서 G5.32의 전체 실행에서 tx::send_tests::sqlite_send_savepoint가 declared test DSN is required(tx_send_tests.rs:9)로 panic했다. 이제 scripts/check/databases.sh는 실행마다 그 실행 directory의 SQLite file로 ORM_SEND_SQLITE_DSN을 환경 file에 쓰고, make check, make owner-check, make run-databases가 그것을 target에 준다. rust-send-savepoint-check는 여전히 자기 것을 넘긴다. Red: 그 줄이 없는(이전 script가 쓴 것과 같은) 실행 환경 file로 tests/cargo-test.mjs가 cargo test ... --lib tx::send_tests::sqlite_send_savepoint를 실행하면 FAIL tx::send_tests::sqlite_send_savepoint elapsed=2ms: declared test DSN is required를 출력했다. Green: 새 script가 쓴 환경 file(ORM_SEND_SQLITE_DSN='sqlite://.../.runtime/check/orm_probe_85875/send-savepoint.sqlite')로 같은 case가 PASS tx::send_tests::sqlite_send_savepoint elapsed=16ms를 출력했다. 그 실행의 database는 뒤에 지웠다.——[o]
G5.33-12exclusive lease 보유자의 하위 process들이 그 lease를 한 번에 하나의 exclusive 보유자로 나누게 한다. 원인: tests/lease는 exclusive lease를 가진 process의 모든 하위 process가 그 lease를 자기 것으로 쓰게 했으므로, 보유자 하나가 동시에 실행하는 process들이 모두 함께 그것을 가졌다. make feature-check는 TypeScript build 출력의 exclusive lease를 가지고 scripts/features/check.mjs는 기능 단계를 병렬 lane으로 실행하므로, G5.32-9의 전체 실행에서 schema_definition/schema-compare가 tsc로 clients/typescript/dist를 다시 build하는 동안 schema-typescript(make dbspec-ts-check)가 그것을 읽었다. dbspec-rules.mjs는 The requested module './parse.js' does not provide an export named 'parseDocument'(dist/dbspec/introspect_catalog.js)로 load에 실패했고, node --test는 이를 통과한 case 226개 옆에 test at clients/typescript/tests/dbspec-rules.mjs:1:1 'test failed'로 적었다. test file과 case helper의 잘못은 아니었다. 이제 하위 process는 조상의 lease 안, 곧 그 lease의 directory within-<pid>-<시작 시각>에서 같은 규칙으로 lease를 얻는다: 형제끼리는 서로의 exclusive를 거부하거나 기다리고, 읽는 쪽은 함께 가지며, 같은 process가 다시 청할 때만 자기 lease를 그대로 쓴다. 안쪽 directory는 조상의 lease가 풀릴 때 지우고, lease list와 lease clear-dead는 안쪽 lease도 다룬다. Red: scripts/test-servers.test.mjs의 새 case(children of an exclusive holder share its lease one exclusive holder at a time)는 이전 commit의 lease program으로 형제가 exclusive를 가진 동안 exclusive=0, shared=0을 적었다. Green: 새 program으로 세 번 실행해 모두 exclusive=3, shared=3, after=0, readers=0을 적고 형제 보유자를 적었다.——[o]
G5.33-13CI의 interface 결과 checker를 make로 실행한다. 원인: G5.33-8은 tests/interfaces/check의 Rust 추출기를 공유 target directory의 lease 아래에서 build하고 Makefile이 export하는 LEASE와 CARGO_LEASES를 읽게 했지만, CI step common state and execution contracts는 node tests/go-run.mjs interfaces-check ./tests/interfaces/check --results ...를 직접 실행했다. 그래서 G5.34-2의 CI 실행은 make check를 통과한 뒤 그 step에서 FAIL interfaces/rust-build elapsed=0s: LEASE and CARGO_LEASES are unset; run this through make, which exports them로 실패했다. 이제 새 target make interface-results-check가 lease-tool 뒤에 결과 검사 세 개(tests/conformance/out과 그 postgres, sqlite directory)를 실행하고, step은 그 target을 실행한다. checker는 lease 변수가 없으면 여전히 거부한다. make repo-check에 ciLeaseErrors(scripts/repo/ci.mjs)를 더한다: source가 LEASE를 읽는 repository program(tests/go-run.mjs나 go run의 Go package, Node script, shell script)을 make 밖에서 실행하는 workflow step은 실패한다. Red: step의 첫 명령을 make 밖에서 실행하면 같은 FAIL interfaces/rust-build ... LEASE and CARGO_LEASES are unset을 적었고, scripts/repo/check.test.mjs의 새 case는 ci.yml의 그 step 세 줄을 보고했다. Green: make interface-results-check는 interfaces/rust-build, go, php, rust, typescript를 통과했고 repository case 59개가 통과한다. 로컬의 interfaces/results 비교는 개발 machine의 tests/conformance/out(이전 로컬 실행의 출력)에 대해 statement 수로 실패했다. D4의 CI 실행은 같은 기대 sequence(contracts/interfaces.json)로 그것을 통과했다.——[o]
G5.33-14conformance 결과를 그것을 만든 실행 안에서 공통 state contract와 비교하고, 모든 차이를 그 내용으로 적는다. 원인: contracts/interfaces.json의 sequence마다 있는 statements 수는 model statement(select, insert, update, delete), 곧 T42 이전에 client의 OnQuery hook이 알리던 statement만 셌다. T42(T42)는 conformance vector와 출력에 transaction 제어를 포함한 모든 statement event를 기록했지만 그 수는 그대로 두었다. 그래서 그 수를 출력의 모든 statement와 비교하던 interfaces checker는 G5.32-9의 전체 실행이 쓴 현재 출력에서 go: state contract now_defaults failed만 적으며 실패했다(기대 3에 출력 5, transactions는 5에 14). T42 이전인 D4의 CI 실행은 통과했었다. 또 검사는 이전 어느 실행이든 disk에 남긴 tests/conformance/out을 읽었으므로 결과가 그 전에 무엇이 실행됐는지에 달렸다. 이제 sequence는 자기 statement의 kind를 순서대로 적고 utility statement는 적지 않는다. 그 수와 위치는 dialect마다 다르고(transactions는 MySQL에서 GET_LOCK부터 SET @orm.ormtest.actor = NULL까지 네 개, PostgreSQL과 SQLite에서 두 개다) vector가 정확히 적는다. 목록은 세 dialect의 vector에서 가져왔고, utility를 빼면 세 dialect가 같다. 그 model statement는 이전 수와 정확히 같고, client가 실제로 실행하는 transaction 제어 statement가 더해졌다: now_defaults는 begin과 commit, creates_and_save는 begin, savepoint와 release 두 쌍, commit, delete_recursive는 begin, savepoint와 release, commit, transactions는 begin, savepoint, rollback_to, release, rollback, restore는 begin과 rollback이다. terminal_reuse, relation_empty, errors는 그대로다. make conformance-check는 runner를 check run -out $(RUN_DIR)/out(아직 없는 directory여야 한다)으로 실행하고, 같은 recipe에서 그 출력을 interfaces-check --results(반복할 수 있다)로 검사한 뒤 directory를 지운다. 그래서 더는 아무것도 tests/conformance/out을 읽지 않고, interface-results-check target과 .gitignore 항목도 없앴다. 차이는 스스로 설명한다(internal/stmtdiff): interfaces checker는 더해지거나 빠진 statement를 위치, kind, SQL과 kind 무리별 수로, 다른 결과를 두 값으로, kind가 없거나 알 수 없는 statement를, 없는 출력을 그 경로로 적는다. conformance 비교는 JSON 전체 두 개 대신 다른 field와, 더해지거나 빠진 statement를 kind, SQL, bind, error와 함께 적는다. make fuzz-check는 CHECK_TARGETS에 들어가고, CI는 준비 뒤에 make check만 실행한다(fuzz, interface 결과, server 환경 step을 없앴다). make repo-check에는 make check 뒤의 명령 step을 실패로 보는 ciAfterCheckErrors를 더하고, make 밖 step을 위해 server 환경을 export하라던 규칙은 뺀다. Red: 전체 실행의 출력에서 이전 checker는 FAIL interfaces/results elapsed=3ms: go: state contract now_defaults failed만 적었다. ciAfterCheckErrors는 이전 ci.yml의 fuzz와 interface 결과 step을 보고했다. Green: make run-databases TARGETS=conformance-check가 연달아 두 번, 관계없는 make fuzz-check 뒤에 한 번 더 통과했다. 매번 interfaces/results는 자기 실행의 출력으로 통과했고, 틀린 내용으로 심어 둔 tests/conformance/out은 읽히지 않았으며, 실행 directory는 남지 않았다. internal/stmtdiff, tests/interfaces/check(statement 차이, 결과, 없는 출력), tests/conformance/check(새 출력 directory, 차이 보고)의 case와 repository case 59개가 통과한다.——[o]
G5.35모든 Rust workspace의 형식을 검사한다. 원인: make rust-fmt-check는 clients/rust에서만 cargo fmt --all --check를 실행했고, contracts/check-inputs.json의 입력도 clients/rust/**/*.rs뿐이어서 bench/rust와 tests/interfaces/rust는 검사되지 않은 채 형식이 어긋나 있었다(Diff in 25개와 10개). 이제 rust-fmt-check는 두 workspace도 검사하고(rust-fmt/bench, rust-fmt/interfaces), 그 source는 target의 입력이며, cargo fmt --all이 둘을 정리했다. 바뀐 것은 형식뿐이다. Red: 정리하지 않은 source에 넓힌 target을 실행하면 make rust-fmt-check가 PASS rust-fmt와 FAIL rust-fmt/bench elapsed=4.365s: cargo exited with 1을 출력했다. Green: 정리한 뒤 PASS rust-fmt, PASS rust-fmt/bench, PASS rust-fmt/interfaces를 출력하고 0으로 끝난다. 실행 시점에 main과 다른 path(78개: 이 항목의 11개와 그 사이 main에 들어온 67개)로 make owner-check가 rust-fmt-check(1.3 s)를 포함한 단계 17개를 모두 통과했다.——[o]
G5.34실행할 수 없는 전체 묶음을 어떤 단계보다 먼저 거부한다. 원인: make check는 checkout의 상태와 상관없이, 진행 중인 항목이 있어도, commit하지 않은 변경이 있어도, 전체 묶음이 이미 실행된 tree에서도 단계를 시작했으므로 활성 항목이 모두 완료되었을 때 전체 묶음을 한 번 실행한다는 규칙은 실행하는 사람이 지킬 때만 지켜졌다. 완료 기준: make check와 새 make rerun-failed의 첫 줄은 node scripts/check/full-run.mjs decide <entry>이고, 이것은 어떤 단계보다 먼저(TEST_ENV를 읽고 lease program을 build하기 전에) 결정하고 그 결정을 이유와 함께 출력한다. docs/checklist.md의 항목(하위 항목 포함)이 진행 중인 동안(각 ID와 제목을 적는다), 추적하는 file에 commit하지 않은 변경이 있는 동안, 기록이 적은 실행의 process가 아직 실행 중인 동안, 그리고 make check이면 .runtime/full-run.json이 같은 tree(git rev-parse HEAD^{tree})의 전체 실행을 기록하고 있을 때(그 실행을 적는다) 종료 상태 2로 거부한다. runner(scripts/check/run.mjs --full-run과 --rerun-failed)는 .runtime/full-run.lock을 배타적으로 만든 동안 다시 결정하고, 첫 단계 전과 각 단계의 시작과 끝마다 기록을 쓴다: tree, commit, 결과, 통과하지 못한 target, database 단계와 모든 target의 상태와 시각. 그래서 강제 종료된 실행은 incomplete로 남는다. make rerun-failed는 database 단계와 현재 tree의 기록에서 통과하지 못한 target만 실행하며, 그런 기록이 없으면 거부된다. make bench와 make run-databases는 guard 없이 실행한다. CHECK_TARGETS의 owner target full-run-check(입력 scripts/check/**, Makefile, tests/testcase.mjs)가 case를 실행한다. 새 checkout에는 기록이 없으므로 CI는 push마다 make check를 실행하고, push는 모든 항목이 완료되었을 때만 한다. repository 규칙은 더하지 않는다: 두 recipe가 guard로 시작하지 않으면 full-run-check가 이미 실패한다. Red: scripts/check/full-run.test.mjs의 make check and make rerun-failed start with the guard가 실패했다. check recipe가 $(WITH_TEST_ENV) node scripts/check/run.mjs $(abspath $(TEST_ENV)) $(CHECK_TARGETS)이므로 진행 중인 항목이 있는 checklist에서도 runner가 시작된다. Green: scripts/check/full-run.test.mjs의 9개 case가 runner가 database 단계와 make target 대신 stub 명령을 실행하는 임시 checkout에서 통과한다: 활성 항목과 활성 하위 항목은 decide와 runner에서 거부되어 나열되고 단계는 실행되지 않으며, commit하지 않은 변경은 거부되고, 같은 tree의 두 번째 전체 실행은 첫 실행을 적으며 거부되고, 기록 없는 rerun은 거부되고, rerun은 database 단계와 실패한 target b만 실행한 뒤 다시 실행할 것이 없으며, SIGKILL로 종료된 실행은 target이 running인 incomplete로 남고 그 rerun은 끝나지 않은 target 셋을 실행한다. 변경한 path 13개로 make owner-check가 owner target 8개(checklist-check, full-run-check, version-check, repo-check, docs-rules-check, docs-check, docs-verify-idempotent, feature-unit-check)를 골라 모두 통과했다. 이 worktree에서 node scripts/check/full-run.mjs decide check와 make rerun-failed는 G5.34가 진행 중이었을 때 G5.32와 G5.34, commit하지 않은 file을 나열하며 종료 상태 2로 거부되었고, rerun은 no full run is recorded로도 거부되었다. make check는 실행하지 않았다. G5.34-1은 실패한 target과 끝나지 않은 target을 나누어 기록한다. G5.33-11과 G5.34-1의 path 7개로 make owner-check가 full-run-check(2.0 s)를 포함한 make target 7개를 모두 통과했다.——[o]
G5.34-1전체 실행 기록에 실패한 target과 끝나지 않은 target을 나누어 적는다. 원인: scripts/check/full-run.mjs는 전체 실행이 시작할 때 모든 target을 failed에 쓰고 실행이 끝날 때만 그 목록을 바꿨다. 그래서 feature-check 중에 멈춘 G5.32의 전체 실행 기록은 targets[]가 44개를 통과, 2개를 실패, 1개를 실행 중, 1개를 실행 전으로 보이는데도 48개 target을 모두 failed에 적었다. 이제 summarize가 기록을 쓸 때마다 target의 상태에서 failed(상태가 failed인 target)와 incomplete(실행 중이거나 실행 전인 target)를 다시 정하고, 첫 기록은 incomplete에 모든 target을, failed를 비운 채 시작하며, 끝난 실행은 두 목록 가운데 하나라도 비어 있지 않으면 실패이고, 거부와 마지막 줄은 두 목록을 모두 적는다. make rerun-failed는 여전히 통과하지 못한 모든 target, 곧 실패한 것과 끝나지 않은 것을 고른다. 이전 기록처럼 incomplete가 없는 기록도 읽는다. Red: 이전 commit의 recorder로 scripts/check/full-run.test.mjs의 새 case a killed run records failed and unfinished targets apart(a는 통과하고 b는 실패하며 c가 runner를 끝내는 stub 실행)가 실패했다: failed에 a, b, c가 있었고 incomplete가 없었다. Green: 그 case가 failed ['b'], incomplete ['c'], 정확히 b와 c의 재실행과 함께 통과하고, summarize separates failed from unfinished targets가 통과하며, node --test scripts/check/full-run.test.mjs가 case 11개를 통과한다.——[o]
G5.34-2make rerun-failed가 기록된 commit의 후손 commit에서 실행되게 한다. 원인: scripts/check/full-run.mjs는 기록된 전체 실행의 tree에서만 재실행을 허용했으므로, G5.32-9의 전체 실행이 찾고 뒤의 commit이 고친 code의 실패는 새 전체 묶음으로만 확인할 수 있었고, 통과한 target도 그것을 건드리지 않는 변경 때문에 다시 실행되었다. 이제 현재 commit이 기록된 tree이거나 기록된 commit(마지막 재실행의 commit, 없으면 전체 실행의 commit)의 후손이면(git merge-base --is-ancestor) 재실행을 허용한다. 재실행은 통과하지 못한 target(실패한 것과 끝나지 않은 것)과, 그 commit 뒤에 바뀐 path(git diff --name-only)가 contracts/check-inputs.json에서 고르는 owner target을 make owner-check와 같은 선택(scripts/features/owners.mjs의 selectTargets)으로 실행하고, owner target마다 그 path를 적는다. 시작 전에 고른 target을 not run으로 되돌리므로 강제 종료된 재실행은 그것을 끝나지 않은 target으로 남기고, 자기 commit, tree, 기준 commit, owner target을 기록하므로 다음 재실행은 그 commit 뒤의 변경으로 owner target을 고른다. 후손이 아닌 commit은 두 commit을 적으며 거부되고, 다른 거부(진행 중인 항목, commit하지 않은 변경, 실행 중인 실행)는 그대로다. AGENTS.md와 Makefile 주석이 새 규칙을 적는다. Red: scripts/check/full-run.test.mjs의 새 case 세 개가 이전 guard로 실패했다: 후손 결정은 the recorded full run is of tree t1, not of the current tree t2로, stub checkout의 새 commit에서의 재실행은 full-run: refused make rerun-failed로. Green: 그 file의 case 14개가 모두 통과한다. 강제 종료된 재실행의 owner target c가 not run으로 남아 다시 실행되는 것, 그 뒤 다시 실행할 것이 없는 재실행, 이전 commit의 checkout이 거부되는 것을 포함한다.——[o]
G5.36PHP 연결을 php-fpm worker의 요청을 넘어 유지하고, 모든 client에서 끊긴 연결을 한 code로 보고한다. 원인: Orm::connect(clients/php/src/Orm.php)는 Db마다 새 PDO를 만들었고 Config는 "the PHP client has none"이라며 poolIdleSize와 poolLifetimeMs를 거부했으므로 요청마다 startup과 인증을 거친 server session을 열었다. PostgreSQL에서 PHP page 요청 956개가 server session 956개를 열었고, Node client는 pool session 20개를 썼다. server가 끝낸 연결(종료된 session, 재시작, 유휴 한도)은 모든 client에서 DRIVER로 실패했으므로 어떤 규칙도 message를 읽지 않고는 그것을 다른 driver 오류와 구별할 수 없었다. 변경: poolSize는 다른 client와 같은 뜻, 곧 process가 database에 대해 한 번에 가지는 최대 연결 수를 유지하고 PHP에 process 안의 pool을 준다(clients/php/src/Connection.php). Db는 DSN과 연결 설정마다 있는 pool의 비어 있는 첫 slot을 잡고, 앞의 poolIdleSize개(0이면 모두) slot은 뒤 요청의 Db를 위해 process에 남는 PDO persistent 연결이며, 그 뒤 slot은 Db와 함께 닫힌다. close()와 Db의 끝이 slot을 돌려주며, 모든 slot을 process의 열린 Db가 가진 때 연결하는 Db는 CONFIG를 받는다. PHP 요청은 자기를 기다릴 수 없기 때문이다. poolSize 0은 Db마다 연결 하나를 유지하고, PDO가 남은 연결을 닫을 수 없으므로 0이 아닌 poolLifetimeMs는 여전히 CONFIG다. poolSize: 1은 worker마다 연결 하나다. session 설정은 연결이 열릴 때만 정한다. PostgreSQL은 전처럼 startup parameter이고, MySQL은 연결 뒤의 statement 대신 연결의 init command SET time_zone = '+00:00', SESSION max_execution_time = <ms>이며, SQLite는 연결할 때마다 process 안에서 pragma를 실행한다. transaction 안에서 끝난 요청(exit, fatal error)의 transaction은 Db::begin이 요청마다 한 번 등록하는 shutdown 함수가 종료한다. MySQL named lock을 풀고 MySQL local 값을 지우고 SQLite mode를 되돌리고 rollback한다. 새 driver code CONNECTION_LOST(docs/spec/errors.yaml, Code.php, codes.go, codes.rs로 생성)는 server가 끝냈거나 끊긴 연결이다. MySQL 2006, 2013, 4031, PostgreSQL SQLSTATE class 08과 57P01, 57P02, 57P05, 그리고 더 이상 열려 있지 않은 연결에 대한 driver의 오류다. backend pid가 0인 연결에서 server SQLSTATE 없이 온 pdo_pgsql 오류(OrmException::fromDriver는 이제 PDO를 받는다), Go의 go-sql-driver ErrInvalidConn, driver.ErrBadConn, pgx ErrConnClosed, Rust의 end of stream, reset, abort, broken pipe, not connected I/O 오류, TypeScript의 끊긴 연결의 code를 가졌거나 code가 없는 mysql2 fatal 오류와 연결 오류를 알린 node-postgres session의 statement 오류다. 어떤 client도 이것을 위해 message text를 비교하지 않는다. PHP client는 statement가 CONNECTION_LOST로 실패하고 현재 Db의 statement가 그 연결에서 실행된 적이 없으면 새 연결에서 한 번 더 전송한다(Db::send). Db의 PDO handle로 보낸 statement도 실행된 것으로 세고, 시도마다 statement event이며, 다른 오류는 그대로 보고한다. Red: 새 clients/php/tests/pool_test.php는 router pool_request.php로 PHP built-in server(요청을 차례로 처리하고 php-fpm worker처럼 요청마다 PHP 상태를 새로 시작하는 process 하나)를 띄우고 server DSN으로 case database에서 실행한다. G5.32-9의 client(이 항목이 생성한 Code.php만 넣었다)에서 pool_request_end/sqlite를 뺀 모든 case가 실패했다. pool_reuse mysql "the two requests use one session: 65554 and 65556", postgres 60596과 60600, sqlite 두 번째 요청의 total_changes 0, pool_request_end mysql과 postgres는 두 session, pool_lost_between_requests는 끝낼 session을 찾지 못했고, pool_bounds는 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와 postgres DRIVER: ... 7 FATAL: terminating connection due to administrator command, no_resend_after_statement [null,"DRIVER"]였다. pool은 있고 요청 끝의 shutdown 함수가 없으면 pool_request_end가 mysql("the local value of the ended transaction is cleared: 'left'", "the named lock ... is released: 65531")과 sqlite(요청이 남긴 읽기 전용 transaction 뒤의 READ_ONLY ... attempt to write a readonly database)에서 실패하고, transaction-local 값과 advisory lock이 transaction과 함께 끝나는 postgres에서는 통과했다. rollback test의 rollback_failed case는 이제 MySQL과 PostgreSQL에서 callback 오류와 rollback 오류가 CONNECTION_LOST이기를 요구한다. mapping 전에는 모든 client에서 실패했다. PHP DRIVER: ... 2006 MySQL server has gone away와 DRIVER: ... 7 no connection to the server, Go DRIVER: invalid connection, DRIVER: FATAL: terminating connection due to administrator command (SQLSTATE 57P01), DRIVER: driver: bad connection, Rust DRIVER: error communicating with database: peer closed connection without sending TLS close_notify와 DRIVER: terminating connection due to administrator command (SQLSTATE 57P01), TypeScript DRIVER: mysql: Can't add new command when connection is in closed state와 DRIVER: postgres: Client has encountered a connection error and is not queryable. Green: php clients/php/tests/pool_test.php가 15 case를 통과한다. pool_bounds(sqlite, mysql, postgres: 둘짜리 pool의 세 번째 Db는 CONFIG, 첫 연결은 다음 Db를 위해 남고, idle 크기를 넘는 연결은 Db와 함께 닫히며, close()가 연결을 돌려준다), pool_reuse(두 요청에 session 하나, 두 요청 모두 연결할 때의 session 설정: PostgreSQL TimeZone UTC와 statement_timeout 4321, 출처 client, MySQL time_zone +00:00과 max_execution_time 4321, SQLite foreign_keys 1과 busy_timeout 5000), pool_request_end(같은 session, 남긴 행은 rollback, MySQL local 값과 named lock은 남지 않고, 끝난 읽기 전용 transaction 뒤에 쓴다), pool_lost_between_requests(mysql, postgres: test가 옛 session을 끝낸 뒤 새 session), resend_first_statement(mysql, postgres: 같은 statement의 event [CONNECTION_LOST, null]과 그 결과), no_resend_after_statement(event 하나 [null, CONNECTION_LOST] 뒤의 CONNECTION_LOST)다. rollback test가 통과한다. PHP rollback_test.php 9 case, Go ^Test(Savepoint)?RollbackFailed 6, Rust --test rollback rollback_failed 6, TypeScript rollback.mjs 9, 그리고 TypeScript idle-connection-end.mjs 2다. dsn_connection의 새 verification command pool-php와 transactions의 rollback-go, rollback-php, rollback-rust, rollback-typescript가 이 test를 실행한다. PHP와 TypeScript symbol snapshot을 기록한 뒤 go run ./tests/interfaces/check -language php, typescript, go가 통과한다. 바뀐 36개 path로 실행한 make owner-check는 6개 feature의 verification command 21개와 coverage part 14개, make target 10개를 골라 21단계를 모두 통과했다. 첫 실행은 docs-rules-check(docs.writing-style이 금지한 한국어 표현)와 queries-php, chunking-php에서 실패했는데, 그 model_test.php case가 test PDO로 Db를 만들었기 때문이며 이제 Connection::fromPdo를 거친다.——[o]
G5.37addTablesAndColumns가 기존 table에 빠진 index를 만들고, 빠진 unique key는 그 이유와 함께 거부한다. 원인: 네 client의 step 함수(Go는 engine/dbspec/add_tables_and_columns.go의 dbspec.AddTablesAndColumnsSteps, Dbspec::addTablesAndColumnsSteps, orm-schema의 add_tables_and_columns_steps, TypeScript dbspec의 addTablesAndColumnsSteps)는 create_table과 null이거나 default가 있는 column의 add_column만 받고 다른 모든 차이를 SCHEMA_DIFFERS로 돌려주었으므로, 기존 table에 index를 더하는 set은 올릴 수 없었다. 설치된 set의 table audit에 index ix_audit_request (request_id)를 더하자 모든 database가 SCHEMA_DIFFERS: ... add_index audit.ix_audit_request로 실패했다. index(dbspec이 unique와 따로 다루는 index)는 어떤 row도 거부하지 않으므로 table이 가진 row 때문에 만들기가 실패하지 않는다. unique key는 실패할 수 있다. 변경: 네 step 함수는 add_index를 받아 table, column과 함께 plan 하나로 만들고(CREATE INDEX), 만든 index를 그 table의 더한 column 뒤에 table.index로 돌려준다. 빠진 unique key는 여전히 차이이며 이제 add_unique <table>.<name>: a missing unique key can fail on the existing rows; add it with a plan으로 적고, 네 client의 SCHEMA_DIFFERS message는 set이 "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), 생성된 Code.php, codes.go, codes.rs와 함께 docs/spec/errors.yaml, docs/features.md와 함께 contracts/features.json의 schema_install, PHP와 Rust README가 규칙을 적는다. Red: 네 addTablesAndColumns test의 새 case add_tables_and_columns_index는 contracts/fixtures/add_tables_and_columns의 version 1을 설치하고 unique.dbs를, 그다음 index.dbs(nullable column과 ix_addcol_item_label)를 적용한다. G5.33-13의 step 함수로는 모든 client에서 SQLite, MySQL, PostgreSQL 모두 실패했다. unique key는 add_unique addcol_item.uq_addcol_item_label로만 적혔고 index set은 SCHEMA_DIFFERS: ... add_index addcol_item.ix_addcol_item_label로 실패했다(PHP 실패 case 3, Go TestAddTablesAndColumnsIndex{SQLite,MySQL,Postgres}, TypeScript 3, Rust add_tables_and_columns_index는 sqlite에서 먼저). Green: 모든 client에서 세 database 모두 case가 통과한다. 빠진 unique key는 이유와 함께 SCHEMA_DIFFERS이고, index set은 [addcol_item.note, addcol_item.ix_addcol_item_label, addcol_item_history.note]를 돌려주며, 두 번째 호출은 []이고, 그 뒤 index set의 install이 아무것도 바꾸지 않으므로 index가 선언대로 있다. 거부되는 fixture 목록에서 index는 빠졌다. addTablesAndColumns test 전체가 통과한다. PHP add_tables_and_columns_test.php 15 case, Go ^TestAddTablesAndColumns 15, TypeScript add-tables-and-columns.mjs 15, Rust --test add_tables_and_columns 5 case(각각 세 database). G5.37과 G5.37-1의 바뀐 33개 path로 실행한 make owner-check는 2개 feature의 verification command 11개와 coverage part 8개, make target 11개를 골라 test-servers-check(1분 19초)를 포함한 17단계를 모두 통과했다.——[o]
G5.37-1test MySQL 서버의 binary log가 만료되게 한다. 원인: scripts/test-servers.sh는 두 mysqld를 compile된 binary log 보존, 곧 30일(binlog_expire_logs_seconds 2592000, 출처 COMPILED)과 max_binlog_size 1 GB로 시작했다. 이전에 SET PERSIST로 정한 3600초 만료는 data directory를 다시 만들 때 사라졌고, 만료만으로는 동작하지 않는다. server는 만료된 log를 시작할 때와 log를 회전할 때만 지우기 때문이다. primary와 replica에 각각 약 1.1 GB file 7개가 쌓였고 공유 disk 여유는 2 GB까지 줄었다. 변경: 두 mysqld에 MYSQLD_BINLOG(--binlog-expire-logs-seconds=3600 --max-binlog-size=67108864)를 주므로 log는 64 MiB마다 회전하고 server는 마지막 쓰기 한 시간 뒤 log를 지운다. 실행 중인 공유 서버는 다시 시작하지 않으며 다음 make test-servers가 적용한다. Red: scripts/test-servers.test.mjs의 새 case the test MySQL server removes an expired binary log by itself는 script가 선언한 보존 option을 만료 2초, 크기 4096 byte로 줄여 빈 port와 임시 data directory의 fixture mysqld를 시작하고, row 10개를 쓰고 3초 기다린 뒤 10개를 더 쓴다. G5.33-13의 script로는 실패했다: 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: case가 통과한다. server가 binlog.000001을 스스로 지웠고 script의 두 mysqld가 $MYSQLD_BINLOG를 받는다.——[o]
G5.39load되지 않는 문서 page가 그 이유를 적게 한다. 원인: G5.37의 전체 make check에서 docs-check는 docs-static/page/examples/complex-query.html을 15.009초 뒤에 page.goto: Timeout 15000ms exceeded ... waiting until "load"만 적으며 실패했고, 다른 page는 모두 100 ms쯤에 load되었다. case가 request, error, 측정값을 하나도 적지 않았으므로 log만으로는 실패를 설명할 수 없었다. 그 page의 자원은 다른 모든 page와 같고(stylesheet, vp-icons.css, preload한 font inter-roman-latin, favicon.svg, 자기만의 그림, frame, 외부 자원은 없다), 증거가 보이는 build나 local server의 결함은 없다. build한 site에서 같은 static context로 48 page를 60번 돌린 재현은 host load average가 165쯤일 때 한 번 세 page를 잇달아 멈췄다: ko/dsl.html은 font와 favicon.svg가 +60 ms부터 남았고, 이어서 ko/examples/complex-query.html과 ko/feature-coverage.html의 본 문서가 15초씩 남았다. 그러니 page 하나의 자원 하나가 아니라 browser와 local server 사이의 경로 전체가 45초쯤 멈췄다. server에 request 기록을 둔 두 번째 재현은 load가 168에서 25로 내려가는 동안 150번 돌려 하나도 멈추지 않았고, 이 branch의 npm run docs:check는 두 complex-query page를 54-72 ms에 load했다. 이제 docs check의 모든 page 열기(static page마다, 검색 case와 theme와 mobile case, mobile reload)는 scripts/docs/load.mjs를 거친다. 실패하면 Playwright의 message에 다음을 더한다: 남은 request마다 자원 종류, URL, 보낸 시각과 나이, local server가 그것을 받았는지와 언제 답했는지(scripts/docs/lib.mjs의 server가 이제 request 기록을 가진다), 실패한 request마다 HTTP status나 browser의 이유, console error와 page error, local server를 실행하는 process의 가장 긴 event loop 지연과 99번째 백분위, host load average와 CPU 수. page마다의 Playwright 기한 15초와 case 기한은 그대로다. Red: scripts/docs/load.test.mjs의 새 case(server가 답하지 않는 stylesheet, 없는 그림, error를 적고 던지는 script를 가진 page)는 이전 loading에서 page.waitForLoadState: Timeout 1000ms exceeded.만 받았다. Green: 매번 남은 stylesheet와 server의 상태, HTTP 404 image .../missing.png, console error 둘, page error, event loop 지연, host load를 받는다. npm run docs:check는 48 page, link, 검색, theme와 mobile을 모두 통과한다.——[o]
G5.38전체 suite를 push 뒤 GitHub CI에서 실행하고, 실패에서 멈추지 않으며, 다음 CI 실행 전에 모든 실패를 고칠 수 있을 만큼 정보를 남긴다. 원인: 전체 suite는 push 전에 로컬에서 실행되었고, 실행은 일찍 또는 조용히 끝날 수 있었다. scripts/check/run.mjs는 databases/create가 실패하면 모든 target을 기록 없이 건너뛰었고, target recipe는 첫 실패 줄에서, target 안의 script와 runner는 첫 실패 명령이나 test binary에서 멈췄으며, target 출력은 화면에만 나와 CI의 실패는 흘러가는 job log에서만 읽을 수 있었고, 실행 directory, 환경, summary를 모으는 것이 없었다. 완료 기준: 전체 suite는 push 뒤 CI에서 실행된다. 필요한 setup 단계가 실패한 target은 실패한 단계와 그 첫 실패 줄과 함께 not-run으로 기록하고 나머지 모든 target을 실행한다. target 안에서는 실패한 부분 뒤에도 독립된 부분을 실행한다. 실패마다 정확한 명령, 입력, 기대값과 실제값을 담은 출력, 끝나지 않은 case와 그 마지막 단계, 실행의 환경을 기록한다. 실행은 모든 target의 상태, 시간, 첫 실패 줄을 담은 summary로 끝나며, CI는 runner가 죽어도 그것을 job summary로 내고 그 실행 id의 보고서와 함께 artifact로 올린다. target이 실패하거나 실행되지 않으면 job은 실패한다. 로컬에서는 Red/Green test와 바뀐 path의 owner check만 실행한다. 하위 항목 G5.38-1부터 G5.38-5가 각 부분을 맡는다.—근거: 하위 항목 다섯이 바꾼 29개 path로 실행한 make owner-check는 make target 9개를 골라 모두 통과했다: checklist-check, full-run-check(runner case 16개, recipe 부분 case 2개, scripts/check/continue.test.mjs의 계속 case 2개), version-check, testcase-check(tests/cargo-test.test.mjs를 포함한 다섯 부분), repo-check, docs-rules-check, docs-check, docs-verify-idempotent, feature-unit-check. make check와 make rerun-failed는 실행하지 않았다: push 뒤 첫 CI 실행이 이 규칙 아래의 첫 전체 실행이다.[o]
G5.38-1실패한 setup 단계가 막은 target을 not-run으로 기록하고 나머지를 실행하며, 실행마다 보고서를 쓴다. 원인: runChecks는 databases/create가 통과할 때만 target을 실행했으므로 database setup이 실패하면 database를 읽지 않는 target까지 모든 target이 실행되지도 기록되지도 않았다. target 출력은 화면에만 나오고 file로 남지 않았으며, 실패한 target의 실행 directory, 환경, summary는 어디에도 없었다. 변경: runner는 setup 단계 두 개를 실행한다. servers(server 환경 file의 export NAME='value' 줄을 스스로 읽고 server의 shared lease를 잡으며, check, rerun-failed, run-databases recipe의 $(WITH_TEST_ENV)를 대신한다)와 databases/create다. contracts/check-inputs.json의 모든 target은 needs를 선언하고(server와 실행의 database를 읽는 26개는 ["databases"], 나머지 23개는 []), scripts/features/owners.mjs의 checkInputErrors는 없거나 모르는 값을 거부하며, setup 단계가 실패한 target은 the setup step <step> failed: <그 첫 실패 줄>과 함께 not-run으로 기록하고 다른 target은 실행한다. 기록은 시작하지 않은 단계를 pending이라 부른다. target은 make -k로 실행한다. scripts/check/report.mjs는 실행의 보고서 .runtime/check/<run id>/report를 쓴다. id는 ORM_CHECK_RUN_ID(CI는 run id와 attempt를 준다)이거나 새 이름이다. 보고서는 environment.txt(commit, tree, OS, Node, Go, PHP, cargo, mysqld, postgres, disk), targets/<target>.log(정확한 make 명령, Makefile의 recipe, 선언된 입력, 출력 전체), targets/<target>/run/(실패한 target이 남긴 실행 directory .runtime/run/<target>-<make pid>. build target, node_modules, database file, binary file은 옮기지 않고 MANIFEST.txt가 크기와 함께 적는다), summary.md다. target 기록은 log path, 시간, 첫 실패 줄을 가진다. 첫 실패 줄은 FAIL, --- FAIL, not ok, panic 줄과, 시작하고 끝나지 않은 case를 그 마지막 단계와 함께 적은 줄이므로 기한이나 끝난 process로 멈춘 case도 무엇을 기다렸는지 적는다. case가 실패를 보고하지 않은 target은 출력의 마지막 줄과 명령이 끝난 방식을 가진다. 1 MiB보다 큰 log와 옮긴 file은 마지막 256 KiB만 남기며 그 첫 줄과 summary가 그것을 적는다. scripts/check/summary.mjs(CI의 summary 단계)는 ORM_CHECK_RUN_ID의 summary를 GITHUB_STEP_SUMMARY와 보고서에 쓴다. runner가 끝나지 않았으면 그렇다고 쓰고 마지막으로 기록된 단계를 적으며, 기록에 그 실행이 없으면 make check가 runner의 기록 전에 멈췄다고 쓴다. Red: G5.37의 runner로 scripts/check/full-run.test.mjs의 새 case a failed setup step blocks only the targets that need it, and the report explains every failure(target 넷 가운데 a와 c가 database가 필요하고, database setup이 실패하며, b는 rows of t: expected 3, actual 5와 끝나지 않은 case로 실패하고, d는 통과하는 stub suite)는 ['sh create', 'sh drop']만 실행하고 어떤 target도 실행하지 않았다. crash case는 scripts/check/summary.mjs를 찾지 못했고, recipe case는 check에서 $(WITH_TEST_ENV)를 찾았으며, checkInputErrors는 needs가 없는 target과 needs: ['servers']인 target을 받아들였다. Green: setup case는 sh create, b, d, sh drop을 실행하고, a와 c를 the setup step databases/create failed: ... Access denied for user ...와 함께 not-run으로, b를 FAIL 줄과 case b/pending did not finish; its last step: STEP b/pending elapsed=1ms: waiting for the lock of t와 함께 failed로, d를 passed로 기록하고 1로 끝난다. 보고서는 environment.txt, 명령과 출력을 담은 targets/b.log, big.log를 [the last 262144 of 2097156 bytes of ...]로 담은 실행 directory, 모든 줄을 담은 summary.md이며 summary 단계는 그것을 GITHUB_STEP_SUMMARY에도 쓴다. target c에서 SIGKILL로 끝난 crash case는 b failed, c running과 함께 result **crashed**와 The runner did not finish: the runner process <pid> ended without recording the end of run ci_99_2를 내고, 기록이 없는 실행은 make check recorded no run ci_77_1을 쓴다. node --test scripts/check/full-run.test.mjs가 16 case, node --test scripts/features/owners.test.mjs가 15 case를 통과한다.——[o]
G5.38-2target의 독립된 명령을 make -k가 실패한 부분 뒤에도 실행하는 부분으로 실행한다. 원인: make는 recipe를 첫 실패 줄에서 멈추는데, CHECK_TARGETS의 13개 target은 서로의 결과를 읽지 않는 명령을 recipe 하나에 두었다. checklist-check, version-check, repo-check, git-check의 unit test와 검사, testcase-check의 runner 다섯, ts-check의 type 검사와 test, rust-check의 cargo 실행 넷, rust-fmt-check의 workspace 셋, fuzz-check의 fuzz 실행 둘, dialect-facts-check의 probe와 physical test, feature-unit-check의 검사 셋, feature-check의 coverage와 verification command, && 하나로 이은 client-unit-check의 PHP test 일곱이다. 첫 명령이 실패하면 나머지의 결과는 다음 실행까지 숨었다. 줄이 여럿인 다른 target은 다음 줄이 읽는 것을 build하거나 준비하므로 순서를 유지한다. 변경: 13개 target은 저마다 부분 <target>/<part>를 prerequisite으로 두고 각 부분은 명령 하나를 가진다(부분 40개, .PHONY로 선언). lease나 build가 필요한 부분은 그것(ts-check/hold, feature-check/build, lease-tool, rust-fetch)을 자기 prerequisite으로 가지므로 실패한 setup 부분은 그것이 필요한 부분만 멈춘다. runner(G5.38-1)와 make owner-check(scripts/features/owners.mjs의 makeArguments)는 target을 -k로 실행한다. Red: G5.38-1의 Makefile로 scripts/check/full-run.test.mjs의 새 case a target of independent commands runs each as a part of its own은 13개 target이 모두 명령을 recipe 하나에 둔다고 적었다(checklist-check: prerequisites checklist-check: instead of checklist-check/unit checklist-check/run 등). checklist-check의 정의를 unit test에서 실패하는 node와 함께 임시 Makefile에서 실행하는 case the parts of a target after a failed part still run under make -k는 --test scripts/checklist/check.test.mjs만 실행했고, makeArguments는 -k를 주지 않았다. Green: 구조 case가 13개 target에서 통과하고, make -k case는 두 부분을 모두 실행하고 make --no-print-directory -k checklist-check exited with 2로 끝나며, node --test scripts/check/full-run.test.mjs가 18 case, node --test scripts/features/owners.test.mjs가 15 case를 통과한다. make -n -k checklist-check client-unit-check ts-check는 모든 부분의 명령을 적는다.——[o]
G5.38-3test script의 독립된 명령을 실패한 명령 뒤에도 실행한다. 원인: target 안의 script는 첫 실패에서 멈췄다. scripts/client-db-test.sh(client-db-check, client-pooler-check)는 각 lane을 set -eu로 실행했으므로 실패한 test file이 lane을 끝내 나머지 PHP test file 10개, 둘째 Go 실행, Rust integration program, TypeScript test가 실행되지 않았다. scripts/typescript/sqlite-test.sh도 TypeScript test 아홉 개에서 그랬고, check의 모든 cargo test의 test binary를 실행하는 tests/cargo-test.mjs는 첫 실패 binary에서 돌아갔다. 다른 runner는 첫 실패에서 멈추지 않는다. node --test, tests/go-test.mjs(-failfast 없는 go test), PHP와 TypeScript test script, Rust case는 실패한 case 뒤에도 계속한다. 변경: lane과 sqlite-test.sh는 test마다 keep으로 실행한다. keep은 명령을 실행하고, 실패하면 그 명령과 종료 상태를 FAILED에 더하고 계속한다. 각 lane은 finish로 끝나며 finish는 client-db-test: the <lane> lane failed:와 실패한 명령을 모두 출력하고 1로 끝난다. 다음 명령이 읽는 build(Rust integration program, TypeScript build)는 여전히 그것이 필요한 것을 멈추며, Rust lane은 실패한 integration build를 the build of the integration program (exit <n>), so the integration program did not run으로 적는다. tests/cargo-test.mjs는 cargo test --no-fail-fast처럼 모든 binary를 실행하고 cargo-test: <n> of <m> test binaries failed: <target> (exit <code>), ...를 출력한 뒤 첫 실패의 코드로 끝난다. 명령은 여전히 한 줄에 하나이므로 make repo-check는 그것이 실행하는 test file을 모두 찾는다. 새 case는 full-run-check에서 실행되며 그 입력에 두 script가 더해졌다. Red: G5.38-2의 script로, 첫 test에서 실패하는 php나 node를 PATH 앞에 둔 새 scripts/check/continue.test.mjs의 case는 PHP test 11개 가운데 1개와 TypeScript 명령 10개 가운데 2개(build와 model.mjs)만 실행했고, tests/cargo-test.test.mjs의 새 case cargo-test runs every test binary after a failed one and names the failures(가짜 cargo와 lease, binary 셋, 둘째가 exit 4로 실패)는 one과 two만 실행했다. Green: PHP lane은 11개를 모두 실행하고 php clients/php/tests/model_test.php (exit 3)와 함께 client-db-test: the php lane failed:를 출력하며, TypeScript script는 10개를 모두 실행하고 node clients/typescript/tests/model.mjs (exit 3)를 적고, cargo-test는 binary 셋을 실행하고 cargo-test: 1 of 3 test binaries failed: two (exit 4)를 출력한 뒤 4로 끝난다. node --test scripts/check/continue.test.mjs가 2 case, node --test tests/cargo-test.test.mjs가 4 case를 통과한다.——[o]
G5.38-4CI가 server 시작이 실패해도 make check를 실행하고, 그 summary를 내며, 그 실행 id의 보고서만 올리게 한다. 원인: .github/workflows/ci.yml은 make test-servers가 실패하면 job을 멈췄으므로 server가 필요 없는 target까지 어떤 target도 실행되지 않았다. make check 뒤에는 아무것도 내지 않아 실패는 job log에서만 읽을 수 있었고, scripts/repo/ci.mjs의 ciAfterCheckErrors는 make check 뒤의 모든 명령 step을 거부했지만 action step은 무엇이든 받아들였다. 변경: database servers step은 continue-on-error: true를 가지므로 make check가 실행되고 server가 필요한 target을 그 실패와 함께 not-run으로 기록한다(G5.38-1). make check는 ORM_CHECK_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}를 받는다. 그 뒤에는 if: always() step 둘이 온다. summary(node scripts/check/summary.mjs: 그 실행 id의 summary를 runner가 끝나지 않았어도 GITHUB_STEP_SUMMARY와 보고서에 쓰고, 실행 기록을 record.json으로, test server의 log를 data directory 없이 servers/*.log로 보고서에 복사한다)와 report(actions/upload-artifact@v4, artifact check-<run id>-<attempt>, path는 .runtime/check/ci_<run id>_<attempt>/report/뿐, if-no-files-found: error)다. 그래서 다른 실행의 file은 모이지 않고 database data directory와 build target은 올라가지 않는다. job은 make check가 실패할 때, 곧 target이 실패하거나 실행되지 않았을 때 실패한다. ciAfterCheckErrors는 이제 make check step의 ORM_CHECK_RUN_ID와 그 뒤의 정확히 선언된 summary와 report step(AFTER_CHECK)을 요구하고, 다른 모든 step, 바뀐 upload path, 빠진 step을 적는다. full-run-check는 새 test가 make repo-check가 TypeScript build 출력을 읽는 것으로 세는 scripts/typescript/sqlite-test.sh를 실행하므로 $(READ_TYPESCRIPT)로 그 출력을 잡는다. Red: G5.38-3의 scripts/repo/ci.mjs로 scripts/repo/check.test.mjs의 새 case a workflow runs only the summary and the report upload after make check는 선언된 step을 가진 최소 workflow에서 ci.yml step "summary" runs node scripts/check/summary.mjs after make check로 실패했다. 이전 case는 make check 뒤의 action step을 무엇이든 허용했다. G5.38-3의 scripts/check/summary.mjs로 새 case the summary step keeps the record and the server logs in the report of the run은 보고서에서 record.json을 찾지 못했다. Green: 저장소 workflow와 최소 workflow가 통과하고, fuzz step, actions/cache step, report/ 대신 .runtime/check/ci_<id>_<attempt>/를 올리는 upload, 두 step이 없는 workflow, ORM_CHECK_RUN_ID 없는 make check는 저마다 오류를 돌려준다. 보고서는 기록과 같은 record.json, 그대로의 servers/postgres.log, [the last 262144 of 2097169 bytes of ...]로 줄인 servers/mysql.log를 담고 servers/mysql data directory는 담지 않는다. node --test scripts/repo/check.test.mjs가 59 case, node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs가 21 case를 통과하고 node scripts/repo/check.mjs가 통과한다.——[o]
G5.38-5AGENTS.md와 AGENTS.ko.md에 전체 suite는 push 뒤 CI에서 실행한다고 적는다. 원인: AGENTS.md는 전체 suite가 활성 항목이 모두 완료되었을 때 로컬에서 정확히 한 번 실행되고 push 전에 make check를 한 번 실행한다고 적었다. 이제 전체 suite는 CI에서 실행하며, CI 실행 한 번은 다음 CI 실행 전에 그 실행이 찾은 모든 실패를 고칠 수 있을 만큼의 정보를 모아야 한다. 변경: 규칙은 두 언어로 이렇게 적는다. 전체 suite는 push 뒤 GitHub CI에서 실행한다. 실패에서 멈추지 않고 묶음을 끝까지 마친다. 실패마다 입력, 정확한 명령, 출력, 기대값과 실제값, 관련된 환경 사실을 기록해 로컬에서 다시 실행하지 않고 진단할 수 있게 하고, 실패한 case는 무엇이, 어디서, 왜 실패했는지 적는다. 실행은 CI가 내고 그 실행 id의 보고서와 함께 올리는 summary로 끝나며, target이 실패하거나 실행되지 않으면 job은 실패한다. 로컬 make check는 할 수 있지만 push 전에 필요한 단계가 아니다. push는 매번 owner의 승인이 필요하고 진행 중인 항목이 없을 때만 한다. CI 실패는 체크리스트 항목으로 고치고, 진행 중인 CI 실행에 대응하려고 push하지 않는다. AGENTS의 make check guard 문장은 거부 조건을 유지하고 로컬에서 make check를 한 번 실행하라는 지시를 뺀다. check recipe와 scripts/check/full-run.mjs의 주석은 전체 suite가 실행되는 곳을 적는다. make repo-check가 실행하는 scripts/repo/ci.mjs의 fullSuiteRuleErrors는 두 file에 규칙을 요구하고 이전 규칙을 거부한다. Red: 이전 AGENTS.md와 AGENTS.ko.md는 오류 12개를 냈다. 그 가운데 AGENTS.md does not state the full-suite rule: "runs on GitHub CI after a push"와 AGENTS.md still states the local full-suite rule: "runs exactly once, when every active checklist item is complete"가 있다. Green: 두 file이 통과하고, scripts/repo/check.test.mjs의 case AGENTS states that the full suite runs on CI after a push가 통과하며, node scripts/repo/check.mjs가 통과한다.——[o]
G5.40test가 TypeScript build 출력을 실제로 읽을 때만 그 출력의 reader로 세고, 잘못 센 결과가 요구한 lease를 뺀다. 원인: typescriptReaderErrors(make repo-check)가 따라가는 scripts/repo/testcases.mjs의 usesTypescriptOutput은 node라는 낱말이 있는 줄의 모든 clients/typescript/...mjs 경로를 test가 실행하는 file로 보았다. scripts/check/continue.test.mjs는 가짜 node의 기대 출력으로만 node clients/typescript/tests/model.mjs를 적는데도 규칙은 그것을 reader로 셌고, G5.38-3은 full-run-check에 $(READ_TYPESCRIPT)를 더해 그 target을 이유 없이 TypeScript build와 차례로 실행하게 했다. 같은 규칙은 실제 읽기를 놓쳤다. 이름 목록이 여러 줄에 걸친 ../dist/의 import(clients/typescript/tests/model.mjs)와 process.execPath로 하는 실행이다. 변경: JavaScript file은 TypeScript test를 실행하는 두 형태로만 실행한다(nodeRuns). program과 그 인자 배열('node', ['<file>', ...]이나 process.execPath, ['<file>', ...]), 그리고 exec, execSync, spawn, spawnSync, execFile, execFileSync 호출의 shell 문자열('node [options] <file>')이다. 기대 출력, stub, 정규식, 주석에 적은 경로는 실행이 아니다. build 출력의 import는 그 { ... } 이름 목록이 여러 줄에 걸쳐도 읽기다. 그 안에는 이름, as, 쉼표, 공백만 올 수 있으므로 함수 본문 뒤에 import를 인용한 주석은 import가 아니다. full-run-check는 $(READ_TYPESCRIPT)와 lease-tool prerequisite을 잃는다. 예외로 둔 path는 없다. Red: G5.38-5의 규칙으로 scripts/repo/check.test.mjs의 새 case a test that only names a TypeScript test path does not read the build output은 assertion, 정규식, stub에 경로를 적는 scripts/names.test.mjs를 reader로 적었고, scripts/exec-path.mjs(spawnSync(process.execPath, [...]))와 scripts/multi.mjs(../dist/index.js를 여러 줄로 import하는 test)는 적지 않았다. Green: case는 정확히 scripts/array.mjs, scripts/exec-path.mjs, scripts/shell.mjs, scripts/multi.mjs를 적고 scripts/names.test.mjs와 scripts/comment.mjs는 적지 않는다. 저장소에서 scripts/check/continue.test.mjs는 reader가 아니고 scripts/case-database-check.mjs와 clients/typescript/tests/model.mjs는 reader이며, node --test scripts/repo/check.test.mjs가 61 case를 통과하고 node scripts/repo/check.mjs가 full-run-check의 lease 없이 통과한다.——[o]
G5.41체크리스트 상태 표시를 항목의 상태로만 쓰게 한다. 원인: docs/checklist.md와 docs/checklist.ko.md는 3번째 줄에 네 표시의 범례를 두었고 세 항목의 글에 표시를 인용했다(영어 454, 505, 517번째 줄, 한국어 452, 503, 515번째 줄). 그래서 표시를 세는 stop hook이 그것을 진행 중인 항목으로 셌고, scripts/checklist/check.mjs는 어느 줄에든 표시를 허용했다. 이제 make checklist-check는 네 상태의 표시가 항목이나 하위 항목 맨 앞의 상태가 아닌 곳(제목, 범례, 항목의 글, 이어지는 글, inline code)에 있으면 file, 줄, 열을 적으며 실패한다. 예외와 허용 목록은 없다. 두 체크리스트에서 범례를 지워 항목만 두었다. 네 상태는 이미 정의하던 AGENTS.md와 AGENTS.ko.md가 정의하고, 표시는 항목의 상태로만 쓴다는 것도 이제 거기에 적는다. 세 항목의 글은 "진행 중", "완료로 표시된" 같은 말로 적는다. 체크리스트 밖의 글에는 괄호 표시가 남아 있다: AGENTS.md와 AGENTS.ko.md(각 4줄, 상태 정의와 전체 묶음 규칙), CONTRIBUTING.md와 CONTRIBUTING.ko.md(각 1줄), CHANGELOG.md와 CHANGELOG.ko.md(G5.34 항목). checker는 체크리스트만 다룬다. Red: 범례, 항목의 글, inline code, 이어지는 글에 표시가 있는 scripts/checklist/check.test.mjs의 새 case는 이전 checker에서 오류가 없었고, 새 checker는 이전 체크리스트에서 24곳을 보고했다. Green: 그 case는 표시마다 file, 줄, 열을 받고, 고쳐 쓴 체크리스트는 통과한다.——[o]
G5.41-1GitHub task list의 x와 X 표시도 항목 상태 밖에서는 거부한다. 원인: G5.41은 이 checklist의 네 표시만 거부했지만 GitHub task list는 괄호 안의 x나 X도 상태로 읽으므로, 항목의 글에 있는 그것을 항목으로 읽을 수 있었다. 이제 scripts/checklist/check.mjs는 그 둘도 표시로 세고, 맨 앞 상태가 아닌 곳의 표시마다 file, 줄, 열을 적는다. 맨 앞의 x는 그대로 invalid state 오류 하나다. checklist의 빈 줄이 아닌 모든 줄은 제목(#로 시작), 항목이나 하위 항목(- , 괄호 안의 상태, ID, 들여 쓸 수 있다), 또는 항목 뒤에 두 칸 이상 들여 쓴 줄(Markdown이 그 항목 안에 그린다)이다. 그 밖의 줄, 곧 문단이나 범례는 unnumbered content in task tracker이고, 제목 뒤의 들여 쓴 줄도 그렇다. Red: 한국어 항목의 글에 x, 이어지는 줄에 X를 둔 scripts/checklist/check.test.mjs의 새 case는 오류가 없었다. Green: docs/checklist.ko.md:3:22와 docs/checklist.ko.md:5:8을 보고하고, 맨 앞의 x는 invalid state를 내며, 두 checklist는 통과한다.——[o]
G5.42docs check의 모든 browser 기다림이 실패한 이유를 적게 한다. 원인: G5.39는 page load만 설명했다. G5.41의 owner-check에서 docs-static/theme-and-mobile은 locator.click: Timeout 15000ms exceeded와 call log 한 줄 waiting for getByRole('switch').first()로 실패했는데, switch가 있었는지, 숨었는지, 덮였는지도, page에 무엇이 있었는지도 적지 않았으므로 실패를 설명할 수 없었고, 다시 실행해 통과한 것은 그것에 대해 아무것도 증명하지 않는다. 이제 scripts/docs/steps.mjs(browserSteps)가 scripts/docs/check.mjs의 page 열기와 다시 열기, click, 입력, 요소와 URL과 조건 기다리기, 글 읽기, page 안의 계산을 하는 하나의 helper다. static page case와 검색, theme, mobile, reload 단계가 모두 그것을 쓴다. 실패하면 Playwright의 message에 다음을 더한다: 무엇을 기다렸는지, locator에 맞는 요소의 수와 요소마다 보이는지(display와 visibility와 함께), 쓸 수 있는지, box, 그 가운데 점에서 위에 있는 요소(자기 자신이나 덮은 요소)와 요소와 부모의 HTML, 맞는 요소가 없으면 page의 accessibility tree, 그리고 page URL과 G5.39의 기록(남은 request와 실패한 request와 server의 상태, console error와 page error, event loop 지연, host load). page 열기와 다시 열기는 scripts/docs/load.mjs의 load 기록을 그대로 쓴다. rawWaits는 docs check에서 helper 밖의 Playwright 기다림(goto, reload, click, fill, waitFor, waitForURL, waitForFunction, innerText, evaluate와 그 밖의 기다리는 method)을 browser 안에서 실행되는 code를 빼고 찾고, scripts/docs/steps.test.mjs의 case는 그것이 없기를 요구한다. 단계마다의 Playwright 기한 15초는 그대로다. 그 한 번의 실패의 원인은 밝히지 못했다: interfaces.html의 static HTML에는 switch가 둘 있고, 다음 실패는 page가 어떤 상태였는지 적는다. Red: switch가 없는 page와 overlay가 switch를 덮은 page의 click은 둘 다 locator.click: Timeout 1000ms exceeded.만 적었다. Green: 첫째는 no element matches getByRole('switch').first()와 accessibility tree(navigation, button "Menu", main, heading "Docs")를, 둘째는 1 element(s) match와 #1 visible (display block, visibility visible), enabled, box 10,10 40x20, is covered at its center 30,20 by <div class="overlay" ...>를 적고, 둘 다 request, error, event loop, load 줄을 적는다. 세 case는 연달아 세 번 통과하고, raw wait case는 fixture의 직접 click, waitForFunction, innerText를 보고하며 check.mjs에서는 하나도 보고하지 않는다. npm run docs:check는 48 page, link, 검색, theme와 mobile을 모두 통과한다.——[o]
G5.43첫 CI 전체 실행(commit G5.41-1)이 보인 runner와 보고서의 결함을 고치고, runner의 /tmp를 채운 것을 찾는다. 원인: 실행은 crash했고(runner pid 7516이 끝을 기록하지 못하고 사라졌고 databases/drop이 running으로 남았다), summary가 environment.txt와 targets/*.log를 가리키는데도 artifact에는 record.json, summary.md, servers/만 있었으며, client-db-check의 첫 실패 줄은 PASS와 STEP 줄이었고, root file system에 92 GiB 넘게 남은 동안 25개 target이 10:41:47부터 끝까지 /tmp의 disk quota exceeded(errno 122)로 실패했다. 완료 기준: 실패한 쓰기는 기록되고 runner를 끝내지 않는다. 보고서는 실행 끝까지 남는다. 첫 실패 줄은 실패 줄이다. 보고서는 시작할 때, target마다, 실패할 때 /, /tmp, 실행의 남은 공간과 가장 큰 directory를 기록하고, 공간이 없어 실패한 target은 그 수치와 함께 그렇다고 적는다. /tmp를 채운 원인은 CI 실행에서 G5.43-4의 측정이 필요한 G5.44다. 하위 항목 G5.43-1부터 G5.43-4가 이 부분을 맡는다.—근거: 하위 항목마다의 Red/Green. 네 하위 항목이 바꾼 10개 path로 실행한 make owner-check는 make target 7개를 골라 모두 통과했다: checklist-check, full-run-check(29.7초), version-check, repo-check, docs-rules-check, docs-check, docs-verify-idempotent. make check와 make rerun-failed는 실행하지 않았다.[o]
G5.43-1실패한 보고서와 기록의 쓰기, runner가 처리하지 못한 오류를 실행을 끝내는 대신 기록한다. 원인: G5.41-1의 실행의 job log는 Readable.push와 Pipe.onStreamRead에서 던져진 Error: ENOENT: no such file or directory, open '.../.runtime/check/ci_<run id>_1/report/targets/databases-drop.log'로 runner를 끝냈다. scripts/check/databases.sh drop은 rm -rf "$dir"로 끝나는데 그 실행 directory는 G5.38-1부터 보고서도 담았고(G5.43-2), drop 단계의 다음 출력 줄이 자식 process의 stream callback 안에서 지워진 log에 appendFileSync를 불렀다. 그 예외는 잡히지 않아 process를 끝냈고 그것을 기록하는 handler가 없었으므로 기록에는 databases/drop이 running으로 남았고 summary 단계는 runner가 끝을 기록하지 못했다고만 쓸 수 있었다. 변경: scripts/check/report.mjs의 reportWriter가 보고서의 모든 쓰기(log, environment.txt, 보관한 실행 directory, log의 끝 줄이기, summary)를 맡는다. 쓰기는 던지지 않고 없는 directory를 다시 만들지 않는다. 대신 report write failed: <path>: <code>: <message>를 모으고, runner는 그것을 일어난 단계의 기록(reportErrors)이나 실행의 기록에 남기고 출력하며 실행의 실패로 간주한다(exit 1, result failed). summary가 그것을 적는다. scripts/check/full-run.mjs의 recorder도 기록을 같은 방식으로 쓴다(record write failed: ...). scripts/check/run.mjs의 handleCrashes는 uncaughtException과 unhandledRejection을 기록의 runner error: <stack>, 그 이유를 가진 result crashed, summary, exit 1로 바꾼다. scripts/check/summary.mjs는 crash한 실행에 그 이유를 적는다. Red: G5.41-1의 runner로 scripts/check/full-run.test.mjs의 새 case a report write that fails is recorded and does not stop the runner(target b가 보고서 directory를 지우고 한 줄을 더 쓴다)는 sh create, a, b만 실행하고 runner를 끝냈고, case an unhandled error of the runner is recorded as the reason of a crashed run(target b가 runner의 어떤 callback 밖에서 던진다)은 실행을 incomplete로 남겼다. Green: 첫 case는 a, b, c, sh drop을 실행하고 b를 report write failed: .../report/targets/b.log: ENOENT: ...와 함께 passed로, 실행을 failed로 기록하며 실패한 쓰기를 출력하고 signal 없이 1로 끝난다. 둘째 case는 runner error: Error: boom outside the runner와 그 stack으로 crashed를 기록하고 runner 항목을 지우며 1로 끝나고, summary는 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가 23 case를 통과한다.——[o]
G5.43-2실행의 보고서를 실행의 database와 떼어 두고, target log를 쓰는 동안 크기를 제한한다. 원인: G5.41-1의 실행의 artifact에는 runner 뒤에 scripts/check/summary.mjs가 쓰는 record.json, summary.md, servers/만 있었다. runner는 environment.txt와 모든 targets/*.log를 썼지만, G5.38-1은 보고서를 .runtime/check/<run id>/report, 곧 runner가 실행의 database를 위해 scripts/check/databases.sh에 주고 drop이 rm -rf "$dir"로 지우는 .runtime/check/<run id> 안에 두었다. drop 단계는 보고서 전체와 그 안의 실패한 target의 보관된 실행 directory를 지운 뒤 runner를 crash시켰다(G5.43-1). file은 쓰지 않은 것도 다른 곳에 쓴 것도 아니었다. target log도 실행 중에는 제한 없이 커졌다가 끝난 뒤에야 마지막 256 KiB로 줄었으므로 머리, 곧 명령, recipe, 입력을 잃었다. 변경: 실행의 database는 databases.sh가 만들고 drop이 지우는 .runtime/check/<run id>/databases에 있고, 보고서는 그 옆의 .runtime/check/<run id>/report에 남아 아무것도 그것을 지우지 않는다. scripts/check/report.mjs의 cappedLog는 G5.43-1의 writer로 target log를 1 MiB까지 쓰고, 나머지는 마지막 256 KiB만 memory에 두었다가 [... <n> bytes of output omitted; the last <m> bytes follow ...]와 그 끝으로 log를 닫는다. 그래서 log는 머리와 끝을 모두 가지고 실행 중에도 1.25 MiB를 넘지 않으며, 기록은 생략한 byte를 truncated로 가진다. 예약 file은 더하지 않는다: 보고서는 92 GiB가 남은 root file system에 있었고, 그래도 실패하는 쓰기는 G5.43-1의 writer가 기록한다. Red: G5.43-1의 runner로 scripts/check/full-run.test.mjs의 새 case removing the run databases leaves the report of the run(drop이 databases.sh처럼 자기 directory를 지운다)은 report/environment.txt is missing을 찾았고, case a target log keeps its head and its tail within the size limit(target b가 3 MiB를 출력한다)은 3105026 byte로 자란 뒤 [the last 262144 of 3105026 bytes of this log]로 줄어 target b와 command: 줄을 잃은 log를 찾았다. Green: drop 뒤 report/에 environment.txt, summary.md, servers, databases/create, a, b, c, databases/drop의 log가 있고 databases directory는 없으며 실패한 쓰기가 없다. b의 log는 target b와 그 명령으로 시작하고 line 0, 생략 줄, 끝의 line 3071을 가지며 1 MiB와 256 KiB 안에 머물고, 기록은 b에 양수 truncated를 준다. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs가 25 case를 통과한다.——[o]
G5.43-3target의 실패 줄과 오류 줄을 lane 접두사 뒤에서도 첫 실패 줄로 삼는다. 원인: G5.41-1의 실행의 summary는 client-db-check의 첫 실패 줄로 [rust] STEP joins_and_relations/mysql ... created / ... dropped / [rust] PASS ...를 적었다. scripts/client-db-test.sh는 병렬 lane의 모든 줄 앞에 [<lane>] 을 붙이는데 scripts/check/report.mjs의 failures는 FAIL, RUN, STEP, PASS를 줄 맨 앞에서만 맞추었으므로 그 target에서 실패를 하나도 찾지 못하고 마지막 20줄, 곧 다른 lane의 통과한 case로 대신했다. 오류 수준의 줄도 몰랐으므로 [rust] sqlite: schema().install: DRIVER: disk I/O error, Rust panic, (exit 101), make: *** ... Error 1을 놓쳤다. 변경: failureLine은 lane 접두사와 GitHub annotation을 떼고, 나온 순서대로 FAIL과 TIMEOUT 줄, --- FAIL, not ok, panic, error 낱말(error, Error, ERROR)이 있는 줄, failed:, failed lanes, (exit <n>), exited with <n>, 그리고 disk quota exceeded, No space left on device, errno 28이나 122, ENOSPC, EDQUOT를 잡는다. PASS나 RUN 줄은 실패가 아니다. case는 lane마다 따로 추적하므로 한 lane의 끝나지 않은 case도 그 마지막 단계와 함께 적힌다. 출력의 마지막 줄은 실패한 target이 이것들을 하나도 출력하지 않았을 때만 대신한다. fixture scripts/check/fixtures/client-db-check-lanes.txt는 job log에서 runner 자신의 단계 접두사를 뗀 그 target의 마지막 25줄이다. Red: G5.43-2의 추출기로 scripts/check/full-run.test.mjs의 새 case the first failure lines of a target are its failure and error lines는 [rust] STEP joins_and_relations/mysql elapsed=47ms: database orm_case_33354_8 created로 시작해 PASS와 RUN 줄을 포함한 fixture의 마지막 20줄을 냈다. Green: case는 정확히 panic 줄, [rust] sqlite: schema().install: DRIVER: disk I/O error, [rust] FAIL columns_and_subqueries/sqlite ... 줄, 실패한 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, make의 종료를 돌려준다. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs가 26 case를 통과한다.——[o]
G5.43-4시작할 때와 단계마다 끝난 뒤 /, /tmp, 실행의 남은 공간을 기록하고, 공간이 없어 실패한 target은 그렇다고 분명히 적는다. 원인: G5.41-1의 실행은 target마다 그 앞에서 root file system의 free disk <n> GiB만 출력했고 그것은 92 GiB 위에 머물렀는데, 25개 target은 /tmp의 disk quota exceeded로 실패했다. /tmp의 공간, 그 mount, 그것이 담은 것, 지운 file을 열어 둔 process를 기록한 것이 없었고, 공간이 없어 실패한 target에는 그렇다고 적는 줄이 없었다. 변경: scripts/check/report.mjs의 diskSnapshot은 /, /tmp, process의 임시 directory(TMPDIR), 실행 directory의 df -k, /tmp의 findmnt(Linux에서 tmpfs의 size와 quota option), /tmp, TMPDIR, .runtime/run, .runtime/check/<run id>의 가장 큰 항목 20개(du -sxk), 그리고 /tmp에서 지웠지만 아직 열린 file(lsof +L1)을 기록한다. 없는 명령은 없다고 기록한다. runner는 시작할 때 기록 하나(report/disk/00-start.txt, 기록의 실행 disk)와 단계마다 끝난 뒤 기록 하나(report/disk/<n>-<step>.txt, 단계의 disk, 그 log의 free space after the step 줄)를 쓴다. 실패한 단계 뒤의 기록이 그 실패 순간의 공간이다. 단계의 실패 줄이 disk quota exceeded, No space left on device, errno 28이나 122, ENOSPC, EDQUOT를 담으면 첫 실패 줄은 out of space: the target failed with ENOSPC or EDQUOT; / <n> GiB free, /tmp <n> GiB free, ...; the largest under /tmp: ...가 된다. free disk 단계 줄은 없어졌다. Red: G5.43-3의 runner로 scripts/check/full-run.test.mjs의 새 case a target that runs out of space says so with the free space of / and /tmp(target b가 write /tmp/go-build.../_testmain.go: disk quota exceeded로 실패한다)는 quota 줄 자체를 첫 실패 줄로 찾았고 report/disk는 없었다. Green: b의 첫 실패 줄은 /와 /tmp의 남은 공간과 /tmp의 가장 큰 항목을 담은 out-of-space 줄이고 둘째가 quota 줄이다. 기록은 실행과 b에 disk를 가진다. report/disk는 00-start.txt와 servers, databases/create, a, b, c, databases/drop의 기록을 담고, 각 기록은 df, /tmp의 가장 큰 항목, lsof +L1 부분을 가진다. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs가 27 case를 통과한다. runChecks는 기록 함수를 snapshot으로 받고, stub suite는 공간 case를 빼면 고정 기록을 쓰므로 full-run-check는 stub 단계마다 /tmp를 읽지 않는다. G5.43-1부터 G5.43-4가 바꾼 path로 실행한 make owner-check가 통과했다(G5.43 참고).——[o]
G5.44CI runner의 client-db-check 동안 /tmp를 채우는 것을 찾고 그 원인을 없앤다. 원인: G5.41-1의 실행에서 /tmp의 첫 disk quota exceeded(errno 122, EDQUOT)는 client-db-check의 Rust lane 끝인 10:41:47에 나왔고 /tmp는 실행이 끝난 10:46까지 가득 찬 채였으므로 그것을 채운 것은 그 target의 process보다 오래 남았다. root file system은 92 GiB 넘게 남았다. job log에는 /tmp의 df도 목록도 없다. 완료 기준: CI 실행에서 G5.43-4의 기록(/, /tmp, 실행의 df, findmnt /tmp, /tmp의 가장 큰 항목, /tmp의 lsof +L1)이 자라는 것을 적는다. 그것을 쓰는 곳을 고쳐 끝난 target의 file이 /tmp에 남지 않게 하고, 그 Red/Green case를 둔다. machine의 남은 disk가 3 GB 이상일 때 client-db-check의 Rust lane을 로컬에서 측정한 결과(/tmp와 $TMPDIR의 du, lsof +L1, 실행 directory, 전과 후)를 기록한다.—근거: G5.43-4의 CI 실행의 disk 기록은 /tmp를 채운 것을 test-servers-check의 binary log case가 띄운 fixture mysqld로 지목한다. 그 지운 log는 그 target 뒤 930 MB에서 client-db-check에서 6.5 GB로 자랐다. G5.44-1은 그 서버를 directory를 지우기 전에 멈추며, 그 Red/Green은 이제 서버가 끝나고 아무것도 남지 않음을 보인다. G5.44-2와 G5.44-3은 그 실행이 보인 보고서 결함을 고친다. 기록이 Rust lane 밖에서 원인을 찾았으므로 Rust lane의 local 측정은 필요하지 않다. 다음 CI 실행의 /tmp 기록이 이 수정을 확인하며, 거기서도 /tmp가 자라면 그 기록과 함께 새 항목이 된다.[o]
G5.44-1binary log case의 fixture MySQL 서버를 그 directory를 지우기 전에 멈추고, 서버가 멈추지 않으면 case를 실패시킨다. 원인: G5.43-4의 CI 실행은 /tmp(usrquota를 가진 7.8 GiB tmpfs)의 사용량을 repo-check까지 151,200 KiB, test-servers-check 직후 1,165,248 KiB, client-db-check에서 quota인 6,606,232 KiB로 기록했고, 그 뒤 25개 target이 EDQUOT로 실패했다. test-servers-check부터 모든 기록에서 lsof +L1은 같은 process인 mysqld 10079가 fd 1과 2로 /tmp/orm-binlog-AHN00Q/mysqld.log (deleted)에 쓰는 것을 보였다. 그 크기는 test-servers-check 뒤 930,269,067 byte, client-db-check에서 6,493,765,632 byte였고, 그 process는 같은 지운 directory의 InnoDB file도 열고 있었다. 지운 file은 directory 항목이 없으므로 /tmp의 큰 항목 목록에는 한 번도 나오지 않았다. scripts/test-servers.test.mjs의 case "the test MySQL server removes an expired binary log by itself"는 TCP로 mysqladmin shutdown을 실행했다. 이 명령은 서버가 끝나기 전에 돌아오는데, case는 그 결과를 보지 않고 곧바로 data directory를 지웠다. 정지 도중 data directory가 사라진 서버는 끝나지 않는다. 앞선 실행이 개발 machine에 남긴 그런 서버 넷을 sample로 보니 InnoDB thread log_files_governor가 없는 directory에 redo log file을 다시 만들려고 계속 돌았고, lsof의 SIZE/OFF로 그 지운 log는 6.9–9.2 GB였다. 넷을 멈추자 약 15 GB가 돌아왔는데, 이는 그 offset의 합 34 GB보다 적으므로 SIZE/OFF는 그런 log가 차지한 block을 크게 보인다. case는 그래도 통과했다. 변경: case는 서버를 STOP_PROCESS TERM <pid>(process가 끝났다는 알림에 돌아오는 tests/stop-process)로 멈추고, 멈추었는지와 process가 없는지 확인한 뒤에만 directory를 지운다. 멈추지 않은 서버는 case를 실패시키고 directory를 남기므로 계속 돌지 않는다. Red: 옛 정지 방식에 지우기 전의 확인을 넣자 case는 mysqld 26697 still runs after its shutdown returned로 실패했고, directory가 남았으므로 그 서버는 스스로 끝났다. Green: case는 7.9초에 통과하고, 그 뒤에 orm-binlog process와 directory가 남지 않으며, make test-servers-check가 통과한다.——[o]
G5.44-2target의 첫 실패 줄은 case 이름이 아니라 실패에서 찾고, SQLite의 disk I/O error는 기록에서 남은 공간이 없는 file system이 보일 때만 공간 부족으로 적는다. 원인: G5.43-4의 실행에서 client-db-check의 첫 실패 줄은 [typescript] STEP driver-error/trigger_refused/sqlite ...: database file ... created 줄이었다. failureLine의 \b(?:error|Error|ERROR)\b가 case 이름 driver-error/...의 error에 맞았고, 실제 첫 실패인 [rust] FAIL audit_record_transaction ...: install: DRIVER: disk I/O error는 그 뒤에 나왔다. 그 SQLite 오류(778)에는 공간 부족 줄이 붙지 않았다. 공간 부족 줄은 /tmp의 du 항목만 적었는데, du는 지운 file을 보일 수 없다. disk I/O error에는 공간 말고도 원인이 있으므로 그 code만으로 공간 부족이라 해서는 안 된다. 변경: 단계 줄(STEP <case> ...: <message>)은 그 message로만 판단하고, 오류 낱말은 바로 앞에 -, /, .이나 글자가 없을 때만 오류로 본다. spaceCause는 ENOSPC나 EDQUOT 줄에는 전처럼 공간 부족 줄을 앞에 둔다. disk I/O error 줄에는 단계 뒤 기록에서 남은 공간이 0 KiB인 file system이 있을 때만 그 file system을 적어 앞에 둔다. spaceLine은 lsof +L1에서 /tmp 아래의 지웠지만 열린 가장 큰 file도 그 SIZE/OFF와 process와 함께 적는다. Red: main의 runner로 새 case a case name that holds an error word does not make its step lines failures의 첫 실패 줄에는 driver-error STEP 줄 넷과 every-error-in-order STEP 줄이 들어 있었다. 새 case a disk I/O error is out of space only while a file system has no free space는 /tmp가 0 KiB 남은 경우에도 평범한 FAIL 줄을 먼저 얻었다. Green: 첫 실패 줄은 Rust의 FAIL, panic, quota, compile 오류 줄이다. /tmp가 0 KiB이면 첫 줄은 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)이고, 공간이 남아 있으면 FAIL 줄이 첫 줄로 남는다.——[o]
G5.44-3실패한 target의 npm debug log를 보고서에 두고 그 오류 줄을 첫 실패 줄로 삼는다. 원인: G5.43-4의 실행의 package-check는 npm error A complete log of this run can be found in: <npm cache>/_logs/2026-10-05T11_42_43_213Z-debug-0.log만 보였다. 그 file은 보고서와 artifact 밖에 있으므로, 다시 실행하지 않고는 npm의 오류를 읽을 수 없었다. 변경: runner는 단계 줄이 A complete log of this run can be found in:으로 적는 경로를 모두 기억한다. 실패한 단계에서는 그 file을 report/targets/<단계>/에 옮기고(1 MiB를 넘으면 마지막 256 KiB), 그 <n> error <message> 줄을 npm debug log <사본>: <message>로 첫 실패 줄 맨 앞에 둔다. file이 없으면 그렇다고 적는다. Red: main의 runner로 새 case a failed npm command keeps its debug log and its error lines in the report는 npm error A complete log ... 줄을 첫 줄로 얻었고 보고서에 사본이 없었다. Green: 첫 실패 줄은 npm debug log .runtime/check/ci_13_1/report/targets/b/2026-10-05T11_42_43_213Z-debug-0.log: code E404와 그 404 줄이고, 사본은 debug log와 같다. node --test scripts/check/full-run.test.mjs scripts/check/continue.test.mjs가 30 case를 통과하고, 바뀐 script 넷에 대한 make owner-check는 full-run-check, repo-check, test-servers-check를 통과했다.——[o]
G5.47G5.36의 PHP 연결 pool을 없앤다. PHP client는 Db마다 연결 하나를 열고 요청을 넘어 유지하지 않는다. 원인: G5.36은 php-fpm worker마다 PDO persistent slot orm:<pool>:<slot>을 유지했다. slot은 worker 사이에서 공유되지 않고 기다릴 수도 없으므로 서버 연결은 worker × slot × 연결 설정만큼 늘고, 유지한 연결은 한 요청의 세션 상태를 다음 요청에 넘긴다. php-fpm worker는 한 번에 요청 하나를 처리하고 다음 요청과 아무것도 공유하지 않으므로, PHP는 process 안에서 연결을 제대로 pool할 수 없다. process 밖의 pooler는 할 수 있다. transaction mode의 PgBouncer와 ProxySQL은 적은 서버 연결을 모든 worker가 나누어 쓰게 하고, docs/spec/config.md "Connection poolers"가 둘을 설명하며, make client-pooler-check가 네 client를 그 둘을 거쳐 실행한다. 변경: slot과 $held를 가진 clients/php/src/Connection.php를 지우고, Orm::connect가 PDO를 열어 Db에 준다. Db에서 endRequest와 그 shutdown 함수, __destruct, $sent, CONNECTION_LOST 뒤 첫 statement의 재전송, reconnect를 지운다. 이제 연결은 요청과 함께 닫히고, 서버가 열린 transaction을 rollback하며 그 named lock과 세션 값을 푼다. 테스트 MySQL 세션은 GET_LOCK을 가진 동안 IS_FREE_LOCK 0을, PDO를 해제한 뒤 1을 돌려주었다. 다시 쓰는 유휴 연결이 없으므로 Db의 첫 statement는 connect가 방금 연 연결에서 실행되어 재전송이 맡을 경우가 남지 않는다. Config는 poolSize, poolIdleSize, poolLifetimeMs를 유지하고, Orm::connect는 그중 0이 아닌 값을 PgBouncer와 ProxySQL을 적은 CONFIG로 거부한다. 필드를 지우면 poolSize 뒤의 위치 인자가 밀려 statementTimeoutMs가 조용히 바뀐다. 값을 무시하면 호출자는 연결이 pool되지 않는데도 pool된다고 믿게 된다. Db::poolSize()는 지운다. 언제나 0을 돌려주는 method는 아무것도 설명하지 않기 때문이다. stats()는 Db의 실제 최대값인 1을 maxOpenConnections로 보고한다. MySQL은 여전히 init command 하나로 세션을 정한다. Db는 한 요청에서 transaction 여럿을 실행하므로, finish는 여전히 transaction마다 끝에서 named lock을 풀고 local 값을 지우고 SQLite mode를 되돌린다. CONNECTION_LOST, OrmException::fromDriver($e, $driver, $pdo), Go·Rust·TypeScript의 poolSize는 그대로다. pool_test.php와 pool_request.php를 지우고, docs/spec/config.md, usage.md, protocol.md와 dsn_connection 기능은 PHP가 요청마다 연결을 열며 PHP 연결의 pool은 PgBouncer나 ProxySQL이 맡는다고 적는다. Red: main의 PHP source로 clients/php/tests/model_test.php의 case pool options는 poolSize: 3으로 연결되었고, maxOpenConnections를 1이 아닌 3으로 보고했다. clients/php/tests/rollback_test.php의 새 case first_statement_lost는 새 Db의 서버 세션을 첫 statement 전에 종료한다. MySQL과 PostgreSQL에서 이 case는 오류 없이 event ["CONNECTION_LOST",null]을 찾았으므로, client는 statement를 다시 전송했다. Green: pool options는 SQLite, MySQL, PostgreSQL에서 통과하며, poolSize 3과 -1, poolIdleSize 1, poolLifetimeMs 1000에 pooler를 적은 CONFIG를 받는다. first_statement_lost는 MySQL과 PostgreSQL에서 CONNECTION_LOST와 event 하나 ["CONNECTION_LOST"]로 통과한다. model_test.php, rollback_test.php, schema_set_test.php, driver_error_test.php, runtime_db_test.php와 범위를 정한 owner check가 통과한다.——[o]
G5.45Go, TypeScript, Rust client는 transaction이 깨끗하게 끝났을 때만 connection을 pool에 돌려주고, 아니면 닫는다. 원인: transaction은 MySQL named lock을 풀고 MySQL local 값을 지우고 SQLite mode를 되돌린 뒤 COMMIT이나 ROLLBACK으로 끝난다. 그 상태는 pool connection에서 transaction보다 오래 남기 때문이다. 한 단계라도 실패하면 세션의 상태를 알 수 없다. RELEASE_LOCK이 실패하면 lock이 잡힌 채 남을 수 있고, 그러면 그 pool connection의 다음 사용자가 잡은 적 없는 lock을 갖게 된다. 그런데도 client는 그런 connection을 pool에 돌려주었다. Go의 txConn.rollback(clients/go/orm/tx.go)은 정리와 ROLLBACK의 결과와 상관없이 conn.Close()로 connection을 돌려주었고, commit은 정리가 실패하면 그 경로로 rollback했다. TypeScript의 finish(clients/typescript/src/database.ts)는 ROLLBACK이나 COMMIT이 실패할 때만 session을 버렸고, 정리만 실패했을 때는 버리지 않았다. Rust의 rollback(clients/rust/orm/src/tx.rs)은 앞의 정리가 실패해도 ROLLBACK이 성공하면 connection을 completed로 표시했다. Rust의 commit 경로는 실패한 정리가 end 전에 돌아오므로 이미 connection을 닫았다. 규칙은 pool의 connection은 깨끗하거나 버려진다는 것이다. 세션이 깨끗함을 보일 수 없는 client는 connection을 닫고, 서버는 세션과 함께 그 트랜잭션, lock, 값을 종료한다. 변경: Go는 commit의 실패한 정리를 txConn.unclean에 기록하고, rollback은 그 표시가 있거나 정리 단계나 ROLLBACK이 하나라도 실패하면 conn.Close() 전에 session을 닫는다(closeSession). TypeScript는 정리 statement가 실패하면 discard를 정한다. 다만 subscriber만 실패한 statement는 효과를 냈으므로 제외한다. Rust의 end는 clean을 받아 그것이 참일 때만 connection을 completed로 표시하고, rollback은 정리가 성공했는지를 넘긴다. docs/spec/interfaces.md가 이 규칙을 적는다. TypeScript SQLite driver는 연결 하나를 유지하며 그 연결은 닫을 수 없다. PHP client는 Db마다 연결 하나를 열며(G5.47) pool이 없다. Red: connection이 하나인 MySQL pool에서 RELEASE_LOCK이 실패했거나 lock을 미리 풀어 RELEASE_LOCK이 0을 돌려준 transaction 뒤에, 다음 사용이 같은 세션을 받았다. 새 Go test TestFailedTransactionEndDiscardsConnection은 rollback과 commit에서 거부된 release와 거부된 ROLLBACK 모두에서 session 69697 returned to the pool after its transaction end failed를 찾았고, 5초 뒤에도 lock이 잡혀 있었다. 새 TypeScript case mysql/failedEndDiscards는 rollback과 commit에서 같은 것(session 69805)을 찾았다. 새 Rust test failed_cleanup_discards_the_connection은 rollback: session 69907 returned to the pool after its cleanup failed로 실패했다. Green: 셋 모두 통과한다. 각각에서 깨끗하게 끝난 transaction은 세션을 그대로 쓰고, 실패한 끝의 lock은 다른 connection이 바로 잡는다. TestTransactionReports*, TestSavepointReportsFailedEnd, TestCancelledTransactionClearsSessionState와 TypeScript model.mjs, Rust tx::tests가 통과한다. 바뀐 경로 열 개에 대한 make owner-check는 version-check, repo-check, docs-rules-check, docs-check, docs-verify-idempotent, go-fmt-check, go-vet-check, rust-fmt-check와 model_queries, parameter_chunking, interface_contract의 verification과 coverage를 통과했다.——[o]
G5.45-1owner-check가 바뀐 file의 transaction 끝 test를 실행하게 한다. 원인: G5.45의 file에 대한 make owner-check는 clients/go/orm/transaction_end_test.go를 실행하는 명령("an input of no verification command")도, clients/rust/orm/src/tx.rs 안의 test도 고르지 않았다. tx::send_tests::sqlite_send_savepoint는 손으로 실행하자 declared test DSN is required로 실패했다. ORM_SEND_SQLITE_DSN은 make target만 주기 때문이다. make rust-send-savepoint-check로 실행하자 두 번 모두 셋 중 셋이 통과했다. test를 담은 file이나 기능의 test인 file은 그것을 실행하는 명령을 골라야 한다. 변경: contracts/features.json의 기능 transactions는 transaction-end-go를 선언한다. 이 명령은 transaction_end_test.go, context_reset_test.go, cancelled_session_test.go의 TestTransactionReports*, TestSavepointReportsFailedEnd, TestFailedTransactionEndDiscardsConnection, TestCancelledTransactionClearsSessionState를 실행한다. 또 transaction-end-rust를 선언한다. 이 명령은 tx.rs와 tx_send_tests.rs에서 Rust library의 tx:: test를 실행의 환경으로 실행하고, 두 Rust file을 그 기능의 test에 적는다. failing_driver_test.go는 자기 검사를 고르는 helper로 남는다. Red: scripts/features/owners.test.mjs의 새 case a changed file selects the transaction end tests that it holds or that test it는 clients/rust/orm/src/tx.rs에서 transactions 명령을 찾지 못했다. Green: tx.rs와 tx_send_tests.rs는 transactions/transaction-end-rust를, Go file 셋은 transactions/transaction-end-go를 고르고, owners.test.mjs는 16 case를 통과한다. make owner-check는 두 명령을 실행했고, 그 가운데 TestFailedTransactionEndDiscardsConnection, tx::send_tests::sqlite_send_savepoint, tx::tests::failed_cleanup_discards_the_connection이 통과했으며, 12단계를 모두 통과했다.——[o]
G5.46Rust를 각 checkout의 target directory에 build하고, 다른 checkout의 target directory는 거부한다. 원인: worktree는 make command line의 CARGO_TARGET_DIR로 main checkout의 clients/rust/target에 Rust를 build했고, run.mjs와 owner-check는 그 값을 실행하는 모든 target에 넘겼다. cargo는 artifact가 fresh인지를 package 기준 source 경로와 그 수정 시각으로 판단한다. 어느 checkout이 build했는지는 기록하지 않는다. rustc의 dep-info는 source를 workspace 기준 상대 경로로 적고, cargo가 uplift한 dep-info는 현재 checkout의 경로로 다시 쓰인다. 그러므로 source가 다른 checkout의 build보다 오래된 checkout은 그 다른 checkout의 code를 test하고, target directory의 어떤 것도 artifact를 어느 checkout이 build했는지 보일 수 없다. 감지하려면 build한 checkout과 수정 시각을 기록하고 비교해야 하는데, 기록이 빠지거나 낡으면 아무 표시 없이 잘못된 code를 test한다. 공유하지 않으면 원인이 없어진다. 비용은 disk이고 worktree마다 일시적이다. 이 worktree의 target directory는 owner check에서 3.6 GiB가 되었다. 변경: command line이 checkout 밖의 target directory를 정하면 Makefile은 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로 멈춘다. checkout 안의 실행 directory는 그대로 받는다. run.mjs와 owner-check는 실행하는 target에 CARGO_TARGET_DIR을 더 이상 넘기지 않으므로, 각 target은 Makefile이 정한 자기 checkout의 directory를 쓴다. AGENTS.md와 AGENTS.ko.md는 규칙과 그 이유를 적는다. lease가 여전히 한 checkout의 실행들의 build 순서를 정한다는 것과, worktree를 지우면 그 target directory도 함께 지워진다는 것도 적는다. target directory를 여러 checkout이 함께 쓴다고 적던 주석은 한 checkout의 동시 실행이 함께 쓴다고 적는다. Red: tests/cargo-test.test.mjs의 새 case cargo reuses what another checkout built into a shared target, and a target of its own builds this checkout는 checkout a("from a")를 공유 target에 build한 뒤, source가 "from b"이고 수정 시각이 더 오래된 checkout b를 build했다. cargo는 아무것도 다시 build하지 않았고 b는 "from a"를 실행했다. main의 Makefile로 새 case make refuses a Rust target directory outside its checkout는 make ran with the target directory of another checkout로 실패했다. Green: 자기 target directory에서 b는 "from b"를 실행한다. make는 다른 checkout의 directory를 위의 message로 거부하고, 자기 directory와 그 안의 실행 directory는 받는다. tests/cargo-test.test.mjs는 여섯 case를 통과한다. CARGO_TARGET_DIR override 없이, 곧 이 worktree의 target에 build하며 실행한 바뀐 file 15개의 make owner-check는 full-run-check, version-check, testcase-check, repo-check, docs-rules-check, go-fmt-check, go-vet-check, rust-fmt-check, feature-unit-check와 interface_contract, conformance_verification의 verification과 coverage를 통과했다.——[o]
G5.49G5.44의 CI 실행의 실패 둘, git-check와 client-pooler-check를 고친다. 원인: 그 실행은 50개 target을 통과했고 공간이 부족한 것이 없었으므로 G5.44를 확인했다. git-check는 FAIL git-subjects ...: the process ended inside the case; its error is above로, client-pooler-check는 FAIL first_statement_lost/mysql: the Db holds the only other session of the case database: [1514,1515]로 실패했다. 하위 항목 G5.49-1부터 G5.49-3이 부분을 맡는다.—근거: 각 하위 항목의 Red/Green. 바뀐 file 11개에 대한 make owner-check는 full-run-check, version-check, testcase-check, repo-check, test-servers-check, git-check, docs-rules-check, feature-unit-check와 transactions의 verification을 통과했다.[o]
G5.49-1git.subject-format을 어긴 제목의 커밋은 생기기 전에 거부한다. 원인: 제목을 쓰는 때에 검사하는 것이 없었다. git-check는 push 뒤에야 이력을 읽고, 그때는 push된 커밋을 바꿀 수 없다. 변경: 추적하는 hook .githooks/commit-msg는 node scripts/git/check.mjs --message <file>을 실행한다. 이 명령은 커밋하려는 message의 제목을 검사하고(주석 줄은 건너뛰고 merge는 검사하지 않는다), 어긴 제목은 규칙과 이유를 적고 거부한다. Makefile은 parse 때 core.hooksPath가 .githooks와 다르면 그것으로 정하므로, make를 실행할 때마다 checkout에 hook이 설치된다. git-check는 .githooks/**와 Makefile을 입력으로 선언하고, AGENTS.md는 hook을 적는다. Red: hook과 그 설치가 없을 때 scripts/git/check.test.mjs의 새 case a commit whose subject breaks the rule is refused by the commit-msg hook that make installs는 a subject of 53 characters was committed로 실패했다. Green: 제목 검사가 통과한다. core.hooksPath가 이 checkout의 .githooks인 임시 repository에서 53자 제목은 git.subject-format: subject exceeds 50 characters: ... (contracts/rules.json; the commit is refused)로 거부되고 커밋이 생기지 않으며, 46자 제목은 커밋된다. 새 repository에서 make -n은 그 core.hooksPath를 .githooks로 정한다. scripts/git/check.test.mjs는 다섯 case를 통과한다.——[o]
G5.49-2process를 끝낸 case의 오류를 그 FAIL 줄에 넣는다. 원인: tests/testcase.mjs의 sections()는 case가 process를 끝내면 FAIL <case> ...: the process ended inside the case; its error is above를 썼다. make check 실행의 summary는 FAIL 줄을 첫 실패 줄로 삼으므로 git-check 행에는 오류가 없었고, 실제 줄 git.subject-format: G5.44: ...는 target log에만 있었다. 변경: case가 열려 있는 동안 sections()는 잡히지 않은 error의 message(uncaughtExceptionMonitor)를, 없으면 stderr에 쓴 마지막 줄을 기억한다. FAIL 줄은 the process ended inside the case: <그 오류>이고, 오류가 없으면 no error was written이다. Red: main의 runner로 case testcase/sections는 FAIL fixture/section-crash ...: the process ended inside the case: uncaught section error를 찾지 못했다. Green: 그 줄이 잡히지 않은 error를 담는다. stderr에 git.subject-format: G5.44: subject exceeds 50 characters를 쓰고 process.exit(1)을 부른 case는 그 줄을 FAIL 줄에 담는다. tests/testcase.test.mjs는 여섯 case를 통과한다.——[o]
G5.49-3first_statement_lost에서 새 Db의 세션을 그것이 연 세션으로 찾는다. pooler 뒤에서도 같다. 원인: client-pooler-check에서 case는 the Db holds the only other session of the case database: [1514,1515]로 실패했고, 그 뒤 Db의 것이 아닌 세션을 끝냈다. fixture는 pooler(ProxySQL, PgBouncer)를 거쳐 설치되는데, pooler는 client가 닫은 뒤에도 case database에 자기 서버 세션을 열어 두므로 case database에는 Db의 세션 말고도 세션이 있었다. Db 자신은 서버에 직접 연결한다. case는 pooler 뒤에서도 같은 것을 확인했고, 그 확인은 그대로 둔다. 첫 statement는 CONNECTION_LOST로 실패하고, event는 하나이며, 다시 전송되지 않는다. 변경: case는 Db가 연결하기 전과 뒤에 서버를 통해 case database의 세션을 읽고, Db가 정확히 하나를 열었는지 확인한 뒤 그 세션만 종료한다. Red: client-pooler-check가 쓰는 pooler DSN으로 case는 MySQL에서 [70292,70293], PostgreSQL에서 [37655,37679]로 실패했고, 이어서 no error와 event [null]로 실패했다. Green: ProxySQL과 PgBouncer 뒤에서도, 그것 없이도 MySQL과 PostgreSQL에서 통과하고, rollback_test.php의 다른 case도 통과한다.——[o]
G5.50codec-check는 자기 실행에서 만든 codec 출력만 비교한다. 원인: codec-check는 php tests/codec/check.php만 실행했고, 그것은 tests/codec/out/{go,rust,typescript}.json을 읽었다. 그 file은 다른 target이 Go, Rust, TypeScript codec test를 실행할 때마다 썼고, 로컬 file은 그날의 앞선 실행에서 남은 것이었다. 그래서 검사는 이 실행의 code가 아니라 어느 checkout 상태의 앞선 실행이 남긴 것을 비교했다. Go writer는 os.MkdirAll의 오류도 무시했고 file을 그 경로에 바로 썼다. 변경: codec-check는 이제 부분 codec-check/go, codec-check/rust, codec-check/typescript와 codec-check/compare로 이루어진다. 앞의 셋은 세 codec test를 ORM_CODEC_OUT을 이 make 실행의 directory(CODEC_OUT, .runtime/run/codec-check-<make pid>)로 정해 실행한다. codec-check/compare는 셋이 끝난 뒤 그 directory로 PHP checker를 실행하고 directory를 지운다. writer는 서로 독립이므로 make -k에서 하나가 실패해도 나머지는 멈추지 않는다. writer는 ORM_CODEC_OUT이 정해졌을 때만 쓰고, 모든 오류를 확인하며, 임시 file에 쓴 뒤 이름을 바꾼다. checker는 ORM_CODEC_OUT을 요구하고 그곳의 go.json, rust.json, typescript.json만 읽는다. tests/codec/out/은 더 이상 쓰지 않고 무시 목록에서도 뺐으며, tests/codec/README.md가 그 실행을 설명한다. Red: 앞선 출력을 가진 main checkout에서 base64 전에 byte 하나를 덧붙이는 Go encoder로 TestCodecVectors가 실패했는데도, make codec-check는 옛 file로 세 case를 모두 통과했다. Green: 같은 깨진 encoder로 make -k codec-check는 codec-check/go에서 실패하고, codec-check/rust와 codec-check/typescript는 계속 실행된다. encoder를 되돌리면 make codec-check는 TestCodecVectors, codec::tests::vectors, codec-vector와 PHP case를 3 other-language outputs of 80 vectors로 통과하고 실행 directory를 남기지 않는다. 바뀐 file 일곱 개에 대한 make owner-check는 13단계를 모두 통과했다.——[o]
G5.51행동 시험을 하나도 고르지 않는 바뀐 path에서 owner-check를 실패시키고, 추적하는 모든 file을 그것을 읽는 test에 잇거나 scope를 선언한다. 원인: owner-check는 <path> is an input of no verification command, coverage part or helper; make check runs what reads it를 출력하고 0으로 끝났을 뿐이다. 추적 file을 훑으니 237개가 기능 명령, coverage 단위, helper를 고르지 않았고, owner target도 lint target만 골랐다. repo-check는 **로 모든 path를, go-fmt-check, go-vet-check, rust-fmt-check, docs-rules-check는 확장자로 맞추었다. 그 가운데에는 generator와 cmd/orm-gen, TypeScript index, 생성된 오류 code, internal/*, tests/dialects의 physical test, tests/codec, tests/conformance가 있었다. 그래서 make owner-check PATHS=generator/gen_go.go는 lint만 실행하고 통과했다. 변경: contracts/check-inputs.json의 target은 행동을 시험하는 file인 inputs와, 형식만 검사하는 pattern인 lints를 따로 선언한다. repo-check는 inputs scripts/repo/**와 lints **를, docs-rules-check는 inputs contracts/rules.json, scripts/docs/**와 lints **/*.md를 가지며, formatter는 lints만 가진다. contracts/check-inputs.json에는 paths도 있다. 어떤 test도 맡을 수 없는 정확한 path마다 scope lint나 suite와 이유를 둔다. pattern, 추적하지 않는 path, 다른 scope, 빈 이유는 선언 오류다. 기능 명령, coverage 단위, helper도, inputs로 고르는 target도 고르지 않고 scope도 선언하지 않은 바뀐 path는 이제 그 path와 선언 방법을 적고 owner-check를 실패시킨다. 지운 path는 보지 않는다. 237개 file은 이제 이어졌다. suite scope였지만 특정 file을 읽는 target 27개가 그 inputs를 가진 owner target이 되었다: 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와 dbspec target 15개다. 전체 suite target 여섯은 suite로 남는다. 새 검증 명령 40개가 어떤 명령도 실행하지 않던 test를 실행한다. Go 명령은 그 file의 Test와 Fuzz 함수를 실행한다. 기존 명령은 그 test가 읽는 fixture, Faults.php, sqlite-paths.json, source를 inputs에 더했고, helper 12개가 script와 생성 file을 맡는다. helper error-codes는 docs/spec/errors.yaml에서 clients/go/orm/codes.go, clients/php/src/Code.php, clients/rust/orm/src/codes.rs를 다시 만들어 비교한다. 전에는 그 생성 file을 검사하는 것이 없었다. scope를 선언한 path는 열여섯이다. lint는 .gitignore, LICENSE, clients/rust/rustfmt.toml이다. suite는 모든 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, Rust case-clock과 case-database library, scripts/cargo-build-copy.sh다. Red: main의 owner-check로 owners.mjs --list generator/gen_go.go는 repo-check, go-fmt-check, go-vet-check만 고르고 0으로 끝났다. Green: 이제 go-model-check를 고르고, 아무것도 선언하지 않은 file은 selects no behaviour test로 1로 끝난다. scripts/features/owners.test.mjs의 새 case는 lint 선택, 시험받지 않는 path, scope 오류, generator source, 추적하는 모든 file을 확인하며, 1027개 가운데 선언하지 않은 file은 0개다. 선언 변경에 대한 make owner-check는 고른 검증 명령 127개, coverage 단위 58개, helper 12개를 한 번씩 모두 실행했다. 아래와 G5.51-1의 수정 뒤 실패했던 단계는 다시 실행해 통과했다.——[o]
G5.51-1conformance와 decimal 검사가 비교하는 database 상태에서 ORM 자신의 SQLite row lock table을 뺀다. 원인: helper decimal-physical이 make decimal-physical-check를 실행하자 그 검사는 sqlite/go/1: database state changed로 두 번 실패했다. 이 target은 CHECK_TARGETS에 없어서 그동안 실행한 것이 없었다. SQLite client는 첫 transaction 전에 transaction 밖에서 orm__row_lock을 만들고(docs/operations/usage.md), tests/conformance/check/state.go의 snapshotDatabase는 모든 table을 hash했다. 그래서 rollback된 test가 아무 data도 남기지 않았는데도 첫 client의 실행이 상태를 바꾸었다. 변경: snapshotDatabase는 table orm__row_lock을 건너뛴다. 기능 conformance_verification은 tests/conformance/check의 test를 실행하는 conformance-check-go를 선언한다. 전에는 go vet만 그 file을 골랐기 때문이다. 첫 실행에서 나온 수정은 G5.51에 속한다. error-codes는 tests/go-run.mjs로 orm-gen을 실행하고(repository 규칙), helper는 DECIMAL_ENV를 넘기며, preempted-timing은 그 test의 program을 선언하는 make timing-check를 실행하고, docs/spec/errors.yaml은 error-codes를 고른다. Red: main의 state reader로 새 TestSnapshotIgnoresTheRowLockTable가 실패했다. Green: 그 test가 다른 state test와 함께 통과하고, decimal-physical은 MySQL, PostgreSQL, SQLite에서 통과하며, conformance-runners와 conformance-check-go가 통과한다.——[o]
G5.52CI setup step과 make check는 앞의 setup step이 실패해도 실행하고, 실패한 step이 설치하는 것이 필요한 target은 not-run으로 기록한다. 원인: .github/workflows/ci.yml에서는 database servers step만 continue-on-error를 가졌고, 다른 설치(Go, Node 의존성, Rust, PHP 두 release, PHP SQLite 검사, composer, server program)는 실패하면 job을 멈추었다. 그러면 make check, summary, report가 실행되지 않으므로, 설치 하나가 실패하면 모든 target이 가려지고 보고서도 남지 않았다. continue-on-error는 실패를 job의 결과에서 지우기도 했다. .github/workflows/docs-pages.yml에서도 설치가 실패하면 build, 멱등 검사, smoke test를 건너뛰었다. 변경: ci.yml의 setup step은 모두 id와 if: ${{ !cancelled() }}를 가지므로 앞의 step이 실패해도 실행된다. make check, summary, report는 if: ${{ !cancelled() }}로 실행되고, make check는 step 결과를 ORM_CI_SETUP: ${{ toJSON(steps) }}로 받는다. scripts/check/ci-setup.mjs는 step id마다 그 step이 설치하는 것, 곧 go, node-modules, rust, php-min, php, composer, server-programs, databases를 적는다. rust-cache는 target에 필요한 것을 설치하지 않고, checkout과 Node는 runner 자신에게 필요한 것이다. runner는 성공하지 않은 step마다 setup 단계 ci/<id>로 기록하고, 그 step이 설치하는 것이 필요한 target은 the CI setup step <id> failed; its output is in the job log of that step과 함께 not-run으로 기록하며, 나머지 target은 모두 실행한다. server-programs가 실패하면 databases도 막힌다. contracts/check-inputs.json은 target 49개의 이 need를 선언하고, owner-check는 어떤 step도 마련하지 않는 need를 거부한다. docs-pages.yml은 step마다 id와 if: ${{ !cancelled() ... }}를 둔다. build는 Node 의존성 뒤에만, smoke test는 build와 browser 뒤에만, upload는 모든 step이 통과했을 때만 실행된다. 어떤 workflow도 continue-on-error를 쓰지 않는다. scripts/repo/ci.mjs의 새 규칙 ciSetupErrors가 이것을 강제한다. summary와 report 규칙은 이제 if: ${{ !cancelled() }}를 요구하고, AGENTS.md와 AGENTS.ko.md는 설치 하나가 실패해도 묶음이 멈추지 않는다고 적는다. Red: main의 runner로 scripts/check/full-run.test.mjs의 새 case a failed CI setup step marks the targets that need it not-run and runs every other target는 need rust가 실패했는데도 target a를 실행했다. scripts/repo/check.test.mjs의 새 case a workflow runs every setup step and make check after a failed setup step and passes the results on는 G5.44의 workflow에서 오류를 찾는다. Green: 실행은 ci/rust를 실패로, a를 그 step과 함께 not-run으로, b와 c를 통과로 기록하고, rust-cache는 기록하지 않는다. 규칙은 바뀐 workflow를 통과시키고, 빠진 id, 모르는 id, 빠진 조건, continue-on-error, 빠진 ORM_CI_SETUP을 보고한다. 바뀐 file에 대한 make owner-check는 full-run-check, version-check, repo-check, docs-rules-check, feature-unit-check를 통과했다. workflow 자체는 다음 CI 실행에서 실행된다.——[o]
G5.53script, recipe, 검증 명령의 독립된 부분은 앞의 부분이 실패해도 실행하고, 독립된 검사를 차례로 실행하는 recipe와 명령은 거부한다. 원인: main G5.46를 읽기만 하는 감사가 아직 첫 실패에서 멈추는 곳 여럿을 찾았다. scripts/client-db-test.sh는 lane을 현재 shell에서 차례로 실행했으므로, 실패한 lane의 finish의 exit 1이 다음 lane 전에 실행을 끝냈다. scripts/perf-test.sh, scripts/package-check.sh, databases.sh drop은 독립된 단계를 set -e 아래에서 실행했으므로, MySQL drop이 실패하면 PostgreSQL database와 실행 directory가 남았다. recipe 일곱 개는 독립된 test나 lint를 recipe 하나에서 실행했으므로 make -k에서도 첫 실패가 나머지를 멈추었다: dbspec-php-check, dbspec-rust-check, conformance-result-check, dbspec-compare-check, rust-send-savepoint-check(test 앞의 clippy), dbspec-stress-bench다. conformance-check는 앞선 검사 셋이 끝난 뒤에야 자기 conformance 실행을 했다. contracts/features.json의 검증 명령 여섯 개는 test 둘을 &&로 이었다. 변경: 차례로 실행하는 lane은 각자 subshell에서 실행되고, 실행은 client-db-test: failed lanes: <lanes>로 끝난다. perf-test, package-check(package 검사 넷은 각자 set -e subshell에서 실행되는 함수다), drop은 계속 실행하고, perf-test: failed:, package-check: failed:, databases: drop failed:와 실패한 단계로 끝난다. 일곱 recipe는 부분 <target>/<part>로 나누었다. file을 함께 쓰는 부분은 pattern-specific RUN_DIR로 top target의 실행 directory를 쓰고, top target은 모든 부분이 통과한 뒤 그것을 지운다. conformance-check는 검사 셋 옆에 부분 conformance-check/run을 가진다. 이어 쓰던 명령은 따로 된 명령이다: generation-scan-typescript, generation-lib-rust, encryption-model-go, encryption-codec-rust, constraints-engine-go, constraints-driver-go, 그리고 G5.51의 catalog-unit-rust를 넘겨받은 catalog-lib-rust다. scripts/repo/ci.mjs의 새 repository 규칙 independentTestErrors와 chainedCommandErrors는 서로 다른 test나 lint 줄을 둘 이상 가진 recipe와 test를 &&로 잇는 명령을 거부한다. 같은 test의 두 번째 실행, 실행 없는 compile(go test -c, cargo test --no-run), file로 쓰는 입력, 의존성 도구의 build는 세지 않는다. Red: scripts/check/continue.test.mjs의 새 case는 다음을 찾았다. PHP lane이 실패한 뒤 TypeScript lane이 실행되지 않았다. performance 검사 넷 가운데 첫째만 실행되었다. npm pack이 실패한 뒤 composer, cargo, go가 실행되지 않았다. MySQL drop이 실패한 뒤 psql이 실행되지 않고 directory가 남았다. 새 repository case는 G5.52의 recipe를 찾았다. Green: 그 case들이 통과하고, 모든 lane, performance 검사, package, drop 단계가 실행되며 실패를 적는다. repository case는 Makefile과 contracts/features.json을 통과시키고 fixture를 거부한다. make dbspec-php-check, dbspec-rust-check, conformance-result-check, rust-send-savepoint-check, dbspec-compare-check, dbspec-stress-bench가 통과하고 실행 directory를 남기지 않으며, make run-databases TARGETS=conformance-check가 통과한다. 바뀐 file 열 개에 대한 make owner-check는 package-check와 명령을 나눈 기능 넷의 verification을 포함한 17단계를 통과했다.——[o]
G5.54test를 하나도 고르지 않은 test 실행은 실패시킨다. 원인: 아무것도 고르지 않은 실행이 통과했다. node tests/go-test.mjs -v -run '^TestNoSuchName$' .은 0으로 끝났다. tests/cargo-test.mjs는 이름 filter가 어떤 test도 맞추지 않아도 모든 binary를 실행하고 통과했다. ORM_CLIENT_DB_LANGS=typscript는 lane을 하나도 실행하지 않고 client-db-test: every lane passed:를 출력했다. Makefile, script, contracts/features.json의 모든 Go -run 선택을 go test -list로 훑자, 아무것도 실행하지 않는 것이 하나 있었다. model_queries와 model_writes의 decimal-go-physical은 ^TestDecimalPhysical(MySQL|Postgres|SQLite)$를 고르면서 그 test file의 build tag인 -tags decimalphysical이 없었으므로, 그 세 test는 한 번도 실행되지 않았다(tag 없이 0개, tag와 함께 3개). 변경: tests/go-test.mjs는 실행 단계를 go test -json으로 실행하고, test2json event의 Output(test 출력과 build 출력)을 사람이 읽는 출력으로 그대로 잇는다. -run이 있는 실행은 test를 적은 "Action":"run" event가 하나도 없으면 go-test: -run <pattern> selected no test in <packages>로 실패한다. 수는 -v의 === RUN text가 아니라 문서화된 event에서 얻는다. -run '^$'는 빈 선택을 밝힌 것이므로 검사하지 않는다. tests/cargo-test.mjs는 어떤 binary도 실행하기 전에 binary마다 filter와 인자로 --list를 실행한다. 모든 binary에서 고른 test가 없으면 아무것도 실행하지 않고 cargo-test: the filters <filters> select no test in <n> test binaries로 실패하며, 아니면 고른 수를 출력한다. scripts/client-db-test.sh는 lane이 아닌 ORM_CLIENT_DB_LANGS의 이름과 lane 없는 선택을 exit 2로 거부한다. 두 decimal-go-physical 명령은 -tags decimalphysical을 준다. Red: main의 runner로 새 case a go test run whose -run selects no test fails(임시 module과 실제 go test)는 0으로 끝났다. 새 case cargo-test fails when the name filters select no test in any test binary는 stub binary 둘을 실행하고 0으로 끝났다. client-db-test refuses an unknown lane name and an empty selection은 typscript와 ,를 통과시켰다. Green: 그 실행들은 위의 message로 실패하고, 선택 ^TestPresent$와 two는 통과한다. case the selected tests are counted from the run events of test2json은 === RUN text 없는 run event를 수에 넣고, run event 없는 === RUN text는 넣지 않는다. 다시 훑은 결과 빈 Go 선택은 더 없고, owner-check가 실행한 모든 Rust filter는 test를 골랐다. 바뀐 file에 대한 make owner-check는 이제 세 test를 실행하는 decimal-go-physical을 포함한 11단계를 통과했다.——[o]
G5.55test 단계 뒤에 아무것도 남기지 않는다. 원인: 임시 directory에 orm 실행이 남긴 entry가 1513개(923M) 있었다. 그중 orm-testcase-*가 399개, orm-rust-sqlite-lock-*가 242개, orm-conformance-runners-*가 12개(399M), orm-interface-symbols-*가 10개였고, .runtime/run에는 kill된 실행의 실행 directory 2개가 있었다. 원인은 다섯 가지였다. 어떤 code는 자기 file을 지우지 않았다. tests/testcase_test.php는 .php file은 지웠지만 tempnam이 이름을 차지하려고 만든 빈 file은 지우지 않았고, clients/rust/orm/tests/sqlite_concurrency.rs는 자기 directory를 지우지 않았다. 어떤 code는 실패하면 지우기를 건너뛰었다. tests/interfaces/check/main.go는 defer os.RemoveAll 뒤에 os.Exit(1)을 불러 그 defer가 실행되지 않았다. tests/conformance/check/main.go는 os.RemoveAll 전에 must로 끝났다. clients/rust/orm/src/tx.rs의 test는 끝에서만 directory를 지웠으므로 실패한 assertion이 그것을 남겼다. signal로 끝난 실행은 finally를 실행하지 않았다(scripts/features/coverage.mjs, tests/cargo-test.mjs). runner는 실패한 target의 실행 directory를 보고서로 복사하고 disk에는 그대로 두었다. 나머지는 지금은 없는 code가 만든 것이다(orm-gen-cli-*, orm-ts-tools-*, 모두 10-03 전에 만들어졌다). 임시 file을 만드는 곳은 133곳이므로 찾은 곳만 고쳐서는 다음 곳을 막지 못한다. 그리고 통과한 단계가 실행 중으로 남긴 process는 아예 보이지 않았다. 변경: scripts/check/step.mjs는 check runner와 make owner-check의 단계를 시스템 임시 directory 아래의 자기 임시 directory와 자기 process group에서 실행한다. 임시 directory는 TMPDIR로 주며, 모든 client의 임시 file 호출이 그것을 따른다. 단계는 그 group의 leader다. 단계 뒤에는 group에 남은 process를 적고 끝내고, 그다음 directory에 남은 entry와 실행 directory .runtime/run/*-<make pid>를 적는다. 그중 하나라도 남긴 통과한 단계는 check: <label> left N processes: <pid> <command>나 check: <label> left N temporary entries: <names>로 실패한다. 실패한 단계의 임시 entry는 보고서(targets/<단계>/tmp)로, 실행 directory는 전처럼 targets/<단계>/run으로 복사한다. 두 경우 모두 그 뒤 지우고 process를 끝내며, runner가 signal을 받으면 실행 중인 단계의 group을 종료한다. setup 단계(servers, databases, CI setup)는 시스템 임시 directory와 runner의 group을 그대로 쓴다. 그 단계가 시작한 server는 단계보다 오래 살고, daemon으로 띄운 mysqld는 TMPDIR을 자기 임시 directory로 쓰기 때문이다. 이전 code를 훑은 결과 단계 사이에 임시 directory로 나누는 상태는 없었다. lease는 .runtime에 있고, conformance lock은 고정된 /tmp path이며, 고정된 임시 이름은 실행마다 이름이 다르다. 고친 것: interface check와 conformance check는 os.Exit(run())으로 끝나고, must와 usage는 run이 recover하는 panic으로 run까지 돌아오므로 그 defer가 실행되고 conformance lock이 풀린다. tx.rs는 test directory를 Scratch 값으로, sqlite_concurrency.rs는 database를 자기 directory를 가진 Database 값으로 가지며, 둘 다 drop할 때(실패한 assertion이 unwind할 때도) 그것을 지운다. tests/testcase_test.php는 tempnam file을 지운다. tests/cargo-test.mjs는 실행 directory를 임시 directory 아래에 만들어 kill된 실행의 복사본이 단계와 함께 지워진다. make repo-check는 defer 뒤에 os.Exit를 부르는 Go 함수를 거부한다(scripts/repo/gosource.mjs). 임시 directory를 읽는 검사 두 개는 단계의 directory를 따른다. npm은 TMPDIR 아래에 Node의 compile cache를 켜는데, 그것은 실행끼리 함께 쓰는 cache이고 test가 남긴 것이 아니므로 단계는 npm에게 전과 같은 시스템 임시 directory의 NODE_COMPILE_CACHE를 준다. make repo-check의 socket case는 mysql.sock이 들어갈 만큼 짧은 root가 필요한데, macOS 임시 directory 아래의 단계 directory는 이미 그보다 자리가 적으므로 root를 /tmp 아래에 만든다. AGENTS.md는 그 규칙과, 자기 session을 새로 여는 process는 group을 떠나 보이지 않는다는 한계를 적는다. Red: main의 runner로 새 scripts/check/step.test.mjs case가 실패했다. 단계는 시스템 임시 directory를 자기 것으로 보았고, orm-leftover-probe를 만든 통과한 단계와 sleep 37을 background로 띄운 통과한 단계는 통과하고 둘 다 남겼으며, 실패한 단계의 entry는 보고서에 들어가지 않았다. 늘린 full-run.test.mjs의 setup 실패 case는 보고서 복사 뒤에도 disk에 .runtime/run/b-4242를 찾았다. main의 interface check는 -language rust와 없는 results directory로 실행하자 실패하고 orm-interface-symbols-2642790756을 남겼다. main의 conformance check는 build가 실패하자 orm-conformance-runners-3878254187을 남겼다. tests/testcase_test.php는 file 7개를 남겼고, sqlite-concurrency-rust 명령은 통과한 뒤 orm-rust-sqlite-lock-69336을 남겼다. 새 repository rule은 main의 interface check에서 os.Exit 호출 다섯 곳을 찾았다. Green: 단계 case는 left 1 temporary entries: orm-leftover-probe와 left 1 processes: <pid> sleep 37을 적으며 통과하고, sleep은 끝나고 남은 것이 없다. 실행 directory는 복사 뒤 사라진다. interface check와 conformance check는 전처럼 실패하고 아무것도 남기지 않으며, conformance lock은 풀린다. tests/testcase_test.php와 sqlite-concurrency-rust(test 6개)는 아무것도 남기지 않는다. scratch_is_removed_after_a_failed_assertion이 통과한다. repository rule은 tree에서 아무것도 찾지 않는다. 바뀐 경로 19개에 대한 make owner-check는 full-run-check, repo-check, testcase-check, transactions, interface_contract, conformance_verification의 verification과 interface_contract의 coverage를 포함한 17단계를 통과했고, 임시 entry나 process를 남긴 단계는 없었다.——[o]
G5.55-1실행의 쓰기가 MySQL test server에 남긴 binary log를 실행이 끝날 때 지운다. 원인: test는 자기 database를 지우지만, 그 쓰기가 만든 binary log entry는 primary와 replica에 남는다(log_replica_updates로 replica도 자기 log를 쓴다). server에는 양쪽에 각각 약 4.5 GB의 binary log 5개가 있었고, scripts/check/databases.sh의 create와 drop 한 번이 양쪽에 177 MB씩 더했으므로 disk는 실행마다 늘었다. 선언된 보존(scripts/test-servers.sh의 MYSQLD_BINLOG)은 server가 그것으로 실행될 때만 log를 제한한다. 실행 중인 한 쌍은 그 선언 전에 시작되어 기본값인 max_binlog_size 1073741824와 binlog_expire_logs_seconds 2592000을 보고했다. test server의 binary log를 읽는 것은 replica뿐이다. 변경: check runner와 make owner-check의 마지막 단계이며 실패한 단계 뒤에도 실행되는 databases.sh drop이 binary log도 지운다. primary의 log를 회전하고, replica가 그 지점까지 primary를 적용할 때까지 SOURCE_POS_WAIT로 기다린 뒤, primary의 log를 지금 file까지 지우고, replica의 binary log와 relay log를 회전한 다음 replica의 binary log를 지금 file까지 지운다. 적용을 마친 relay log는 relay_log_purge가 지운다. replica가 모두 적용하지 않았으면 log를 남기고, file과 위치를 적으며 drop 단계를 실패시킨다. 함께 쓰는 server의 다른 실행에도 안전하다. 지우는 것은 replica가 이미 적용한 log이고 지금 file은 남기 때문이다. 명령에는 DSN이 들어 있으므로 drop 단계는 실패한 부분을 이름(label)으로 적고 DSN은 출력하지 않는다. scripts/check/continue.test.mjs의 가짜 program은 실패 pattern을 따옴표로 감싼다. 공백과 backquote가 든 pattern이 그것을 깨뜨렸다. Red: main의 databases.sh로 create와 drop을 한 번 하자 primary는 4,511,362,870 byte에서 5개 file 4,688,489,440 byte가 되었고, replica는 4,511,458,821에서 4,688,585,640이 되었다. Green: 새 case "dropping the run databases purges the binary logs that the replica applied"가 통과한다. 그 case는 가짜 mysql로 지우는 순서를 확인하고, SOURCE_POS_WAIT가 NULL을 돌려주면 log를 남기고 이유와 함께 1로 끝나는지 본다. 새 script로 create와 drop을 한 번 하자 primary에는 158 byte file 1개, replica에는 317 byte file 1개가 남았고, replica는 두 thread를 오류 없이 계속 실행했으며, server directory는 각각 약 4.5 GB에서 567 MB와 551 MB가 되었다. 그 뒤 make owner-check의 drop 단계는 매번 binary logs purged up to binlog.000014를 출력했다.——[o]
G5.56끝난 보유자가 lease나 lock을 막지 않게 한다. 원인: G5.53의 CI 실행에서 conformance-result-check/typescript가 lease: refused the exclusive lease of .runtime/typescript.leases; held by: shared lease of pid 128962 (dead) … decimal-physical-check와 함께 exit 3으로 실패했다. 22초 전에는 같은 lease가 running으로 보고되었다. lease hold의 lease는 releaser process(release-on-exit)가 보유자가 끝난 것을 본 뒤 지운다. 그래서 정상으로 끝난 보유자 뒤에도 releaser가 지울 때까지 file이 남는다. 그 사이에 깨어난 waiter는 보유자가 없고 file이 있는 것을 보고 그것을 dead로 판단해 곧바로 거부했다. dead lease는 가져가지 않았고 --wait도 그것을 기다리지 않았기 때문이다. 같은 규칙 때문에 kill된 보유자의 lease는 누군가 clear-dead를 실행할 때까지 뒤의 모든 실행을 막았다. 다른 lock 두 개도 같은 결함을 가졌다. conformance check는 mkdir /tmp/orm-conformance-<hash>.lock으로, scripts/decimal-bench-sqlite.sh는 mkdir .runtime/decimal-bench.lock으로 lock을 잡았고, directory를 지우지 못하고 끝난 실행은 뒤의 모든 실행을 막았다. 다른 lease 사용처(CARGO_LEASED, HOLD_TYPESCRIPT, READ_TYPESCRIPT, WITH_TEST_ENV, runner와 owner-check의 hold, cargo-test, interface와 conformance build, test-servers.sh)는 모두 tests/lease를 거치므로 그곳을 고치면 함께 고쳐진다. 변경: releaser는 hold가 돌아오기 전에 lease file에 자기 pid를 적는다. 그래서 보유자는 끝났고 releaser가 실행 중인 lease는 풀리는 중이 된다. 기다리는 요청은 directory 변경이나 releaser의 종료를 기다리고, lease list는 그것을 ended, releaser <pid> running으로 보인다. 보유자와 releaser가 모두 끝난 lease는 dead이고 아무도 막지 않는다. dead shared lease는 어느 요청이든 가져간다. 그 보유자는 자원을 읽기만 했기 때문이다. dead exclusive lease는 다음 exclusive 요청이 가져가고 그 요청이 자원을 다시 만든다. dead exclusive lease가 남아 있는 동안 shared 요청은 거부되며, 거부 이유는 그 보유자가 자원을 만들다 끝났을 수 있다는 것이다. 가져갈 때마다 stderr에 적는다. conformance check는 임시 directory의 orm-conformance-<hash>.lock에 flock을 잡는다. 운영체제는 process가 어떻게 끝나든 그 lock을 푼다. lock을 놓는 실행은 lock을 풀기 전에 file을 지우므로, check는 lock을 잡은 뒤 그 path가 아직 잡은 file인지 확인한다. 거부는 보유자의 pid를 적는다. decimal-bench-sqlite.sh는 .runtime/decimal-bench.leases의 exclusive lease 아래에서 준비한다(lease run --wait). CONTRIBUTING.md와 그 한국어 번역이 규칙을 적는다. Red: main의 lease program으로 scripts/test-servers.test.mjs의 새 case가 실패했다. releaser가 실행 중인데 보유자가 끝난 lease 앞에서 waiter는 곧바로 거부되었다. dead shared lease는 exclusive 요청을 거부했다. hold는 lease file에 releaser를 적지 않았다. main의 main.go로 새 Go test TestLockOfAnEndedRunDoesNotHold는 lock을 놓지 않고 끝난 실행 뒤에 mkdir /tmp/orm-conformance-ad16a2804aebc7ba.lock: file exists로 실패했다. main의 decimal-bench-sqlite.sh는 kill된 준비가 남긴 directory가 있을 때 decimal SQLite bench preparation is already running과 함께 1로 끝났다. Green: waiter는 releaser가 실행되는 동안 기다리고, releaser가 lease를 지우지 못하고 끝나면 그것을 가져간다. dead shared lease는 exclusive 요청이 가져간다. dead exclusive lease는 shared 요청을 이유와 함께 거부하고 exclusive 요청이 가져간다. hold는 살아 있는 releaser를 적고, 보유자가 끝나면 releaser가 lease를 지운다. 끝난 실행의 conformance lock은 더 이상 막지 않고, 실행 중인 보유자의 lock은 그 pid와 함께 거부한다. dead exclusive lease가 있는 상태의 make decimal-bench-sqlite는 lease: took over the dead exclusive lease of pid 50342를 적고 database를 준비했다. 바뀐 경로 8개에 대한 make owner-check는 test-servers-check, full-run-check, repo-check, decimal-bench-sqlite helper, conformance_verification의 verification을 포함한 13단계를 통과했다.——[o]
G5.56-1lock helper test는 공용 testcase package로 case를 시작한다. 원인: G5.56이 더한 tests/conformance/check/lock_test.go의 TestLockHelper는 testcase.Start 없이 skip하거나 실행했다. 그래서 main에서 make repo-check가 TestLockHelper does not start its case with internal/testcase, so it runs without a deadline or RUN line으로 실패했다. G5.56의 owner-check가 통과한 것은 repository check가 tracked file을 읽는데 그때 test file이 아직 tracked가 아니었기 때문이다. 변경: helper는 testcase.Start(Compute)로 case를 시작하고, 그것을 실행하는 test는 helper가 이제 RUN 줄을 먼저 출력하므로 locked가 나올 때까지 출력을 한 줄씩 읽는다. Red: main에서 repository case "every Go and Rust test starts its case with the shared testcase package"가 그 줄로 실패했다. Green: 그 case가 통과하고, TestLockOfAnEndedRunDoesNotHold도 통과한다.——[o]
G5.57FAIL 줄이 가리키는 오류를 첫 실패 줄로 남긴다. 원인: G5.53의 CI 실행에서 performance_gate/preempted-timing이 FAIL TestStressDocument elapsed=10.25s: the errors reported above로 실패했지만, summary도 보고서도 그 오류가 무엇인지 적지 않았다. Go testcase는 실패를 stress_test.go:80: <message>로 적은 뒤 그 FAIL 줄만 쓴다. 첫 실패 줄은 실패와 오류 수준의 줄만 가졌고, 보고서의 target log는 앞부분과 끝부분만 남기므로 그 보고 줄은 어디에도 없었다. 변경: scripts/check/report.mjs의 failures()는 지금 case의 source 위치 보고 줄(<file>.go|rs|php|mjs|js|ts:<line>: <message>)을 모은다. lane 접두사와, runner 안의 runner가 붙인 겹친 STEP <case> elapsed=…: 접두사를 떼고 읽는다. case가 시작하거나 통과하면 모은 줄을 버리고, the errors reported above로 끝나는 실패 줄 바로 앞에 첫 실패 줄로 넣는다. 그 줄은 log 상한이 줄이지 않는 실행 기록과 summary에 들어간다. Red: main의 report.mjs로 새 case "a FAIL that points at the errors reported above keeps those errors as first failure lines"는 그 실행의 겹친 줄에서 FAIL 줄만 찾았다. Green: 첫 실패 줄은 stress_test.go:80 보고와 그다음 FAIL 줄이고, 앞서 통과한 case의 log 줄은 들어가지 않는다. 이전의 첫 실패 줄 case들도 통과하고, 바뀐 경로에 대한 make owner-check는 full-run-check와 repo-check를 통과했다.——[o]
G5.58stress parse를 같은 기계의 기준 작업과 비교해 재고, 출력만 하며 아직 판정하지 않는다. 원인: G5.53의 CI 실행에서 PHP stress parse가 400 ms budget에 대해 process CPU(getrusage의 user와 system 시간, clients/php/tests/case_clock.php) median 410.754 ms, preempt 상태에서 444.392 ms로 실패했고, 같은 기계에서 budget과 code 가운데 무엇이 틀렸는지 알려 주는 증거는 없었다. docs/dbspec.md의 네 budget은 개발 machine에서의 첫 측정으로 정한 절대 시간이고, 그 machine은 같은 parse를 300~349 ms로 잰다. 한 기계에서는 지켜지고 다른 기계에서는 실패하는 budget은 parser에 대해 아무것도 말하지 않는다. 같은 process에서 같은 시계로 잰 고정 작업에 대한 비율은, 그 비율이 기계 사이에서 안정하다면 기계의 속도를 상쇄한다. 변경: Go, PHP, TypeScript, Rust의 stress test는 기준 작업도 다섯 번 잰다. 기준 작업은 같은 stress 문서의 byte를 한 번 지나며 h = h * 31 + b (mod 2^32)를 계산하는 순차 loop로, 어떤 compiler도 병렬로 줄일 수 없다. 그 median CPU 시간과 median parse에 대한 비율을 출력하고, 새로 판정하는 것은 없다. budget은 CI가 그 비율을 출력할 때까지 그대로 둔다. Red: main의 stress test는 기준 시간도 비율도 출력하지 않는다. Green: make dbspec-stress-bench가 통과했고, 개발 machine에서(각 두 번) 기준 median과 비율을 출력했다. Go: 2.106과 2.561 ms, 비율 26.70과 24.97(parse 56.2와 63.9 ms). PHP: 38.145와 47.359 ms, 비율 8.24와 6.35(parse 314.3과 300.6 ms). TypeScript: 2.282와 2.509 ms, 비율 52.38과 49.57(parse 119.5와 124.4 ms). Rust: 2.494와 2.741 ms, 비율 23.98과 19.87(parse 59.8과 54.4 ms). checksum은 넷 모두 3992179881이므로 같은 일을 했다. 비율은 한 기계의 두 실행 사이에서 최대 23 % 달라지며(PHP, Rust), 이것이 비율 budget을 얼마나 좁게 잡을 수 있는지의 한계다.——[o]
G5.59실행이 자기 database를 만들기 전에 kill된 실행의 database를 지운다. 원인: 실행은 마지막 단계에서 자기 bench와 decimal database를 지우는데, kill된 실행은 그 단계에 이르지 못한다. 함께 쓰는 MySQL과 PostgreSQL test server에는 그런 실행이 남긴 orm_owner_81522_19e14363_bench와 _decimal이 있었고, 그것을 지우는 것은 없었다. 변경: scripts/check/databases.sh create는 먼저 drop_dead_runs(action drop-dead이기도 하다)를 실행한다. 두 server의 database 목록에서, process가 이 host에서 더 이상 실행되지 않는(ps -p) orm_owner_<pid>_…와 orm_check_<pid>_…의 bench나 decimal database를 지우고 하나씩 적는다. test server는 127.0.0.1에만 열리므로 모든 실행은 이 host의 process다. 살아 있는 실행의 database, CI 이름(ci_<id>), 다른 database는 남는다. Red: main의 databases.sh에는 그런 단계가 없다. 새 case "a run first drops the databases of runs whose process no longer runs"는 usage 줄과 함께 실패했다. Green: database 일곱 개를 내는 가짜 mysql과 psql에 대해, 각 server에서 끝난 pid의 셋을 지우고 살아 있는 실행의 database, CI database, orm_test는 남긴다. scripts/check/continue.test.mjs의 다른 case도 통과한다.——[o]
G5.60defer 뒤에 helper를 거쳐 닿는 os.Exit도 거부한다. 원인: G5.55의 rule은 같은 함수에서 defer 뒤에 직접 부르는 os.Exit만 거부했다. 다섯 program은 defer db.Close() 뒤에 같은 package의 helper를 거쳐 그것에 닿았다. bench/install(fail), examples/complex/go와 examples/thin-slice/go(check, dsn), tests/conformance/runner_go(check), tests/dbspec/introspect/go(fail)이다. 그곳에서 실패한 단계는 연결을 닫지 않고 process를 끝냈고, 사용자에게 program 작성법을 보여 주는 example은 같은 pattern을 가르쳤다. 변경: scripts/repo/gosource.mjs의 deferredExitErrors는 같은 package(directory)에서 os.Exit를 직접, 또는 그런 함수를 거쳐 부르는 함수를 끝내는 함수로 보고, defer 뒤의 그 호출을 helper 이름과 함께 거부한다. 다섯 program은 os.Exit(run())으로 끝난다. helper는 종료 코드를 담은 panic(type exit int)을 일으키고 run이 그것을 recover해 결과로 돌려주므로 defer된 close가 실행된다. run 안의 usage 오류는 그 코드를 return한다. Red: 넓힌 rule은 main에서 그 다섯 program의 호출 18곳을 찾았다. Green: package의 다른 file에 있는 fail을 부르는 check가 있는 새 case는 main calls check, which calls os.Exit, after a defer로 거부되고, tree에는 하나도 없다. 각 program은 여전히 usage 오류에 2로, 실패한 연결이나 없는 ORM_BENCH_MYSQL_DSN에 그 message와 함께 1로 끝난다.——[o]
G5.61script가 자기 group으로 시작한 모든 process의 process group을 확인한다. 원인: G5.55는 단계마다 process group을 확인하지만, 자기 group을 새로 만드는 process는 단계의 group을 떠나므로 단계는 그 group이 남긴 것을 보지 못한다. 다섯 script는 기한에 group 전체를 끝낼 수 있도록 명령을 spawn(…, { detached: true })로 시작한다. scripts/decimal-physical-check.mjs, scripts/group-rows-physical-check.mjs, scripts/unselected-column-physical-check.mjs, scripts/features/coverage.mjs, tests/timing/preempted.test.mjs이다. 그들은 기한이나 출력 한도에서만 group을 끝냈으므로, 정상으로 끝난 뒤 group에 남은 process는 보이지 않은 채 계속 실행되었다. 변경: scripts/check/step.mjs의 endGroup은 leader가 끝난 뒤 group에 남은 process를 적고, 그것을 끝내고(TERM, 3초 뒤 KILL) 돌려준다. 다섯 script는 명령이 끝날 때 그것을 부르고, 돌려받은 것이 있으면 <program> left N processes: <pid> <command>로 명령을 실패시킨다. make repo-check는 detached: true로 process를 시작하고 그 group을 확인하지 않는 JavaScript file을 거부하며(scripts/repo/gosource.mjs의 detachedGroupErrors), 주석과 문자열 literal 안의 낱말은 보지 않는다. tests/lease의 releaser는 이 rule 밖이다. 보유자가 끝날 때까지 부모보다 오래 살도록 만든 것이기 때문이다. Red: 새 rule은 main에서 다섯 script를 찾았다. Green: rule은 tree에서 아무것도 찾지 않고, endGroup이 없는 fixture는 거부한다. 새 case "endGroup names and ends what a process of its own group left behind"는 background sleep 39가 실행되는 동안 leader가 끝난 group에서 <pid> sleep 39를 받고, sleep은 끝나며, 두 번째 호출은 아무것도 돌려주지 않는다. 단계 case들과 scripts/features/coverage.test.mjs는 main과 결과가 같다.——[o]
G5.62test server를 호출자의 process group 밖에서 시작한다. 원인: make test-servers는 scripts/test-servers.sh의 start_logged로 PostgreSQL, 그 replica, ProxySQL, PgBouncer를 log reader shell과 함께 make를 실행한 shell의 process group 안에서 시작했다(10-06에 모두 pgid 75915였고, daemon으로 떠나는 mysqld 쌍만 자기 group을 가졌다). terminal의 interrupt, CI step의 정지, 명령의 group을 끝내는 도구처럼 그 group을 끝내면, 다른 실행이 함께 쓰는 그 server도 끝났다. 변경: start_logged는 각 server와 그 log reader를 tests/new-session(NEW_SESSION, make가 STOP_PROCESS처럼 build한다)으로 실행한다. 그것은 새 session(setsid)을 연 뒤 명령을 실행하므로, server와 reader는 시작한 명령의 process group 밖에서 실행된다. FIFO를 거친 준비 확인은 그대로다. make test-servers와 make repo-check가 그것을 build한다. Red: main의 test-servers.sh로 scripts/repo/check.test.mjs의 start_logged case가 the server runs in the process group of the command that started it로 실패했다. Green: case가 통과하고, 가짜 server의 process group은 test의 것과 다르며, repository test가 통과한다(66 case). 실제 server는 CI에서 시작한다.——[o]
G5.63PostgreSQL insert가 Locked를 알리기 전에 table을 잠근다. 원인: G5.54의 CI 실행에서 catalog_connection/catalog-rust가 orm-build/tests/row_insert_identity.rs:109:5에서 generated identity failures: ["postgres: generated insert must lock the table before publishing Locked"]로 실패했고, 같은 case는 G5.53의 실행에서 통과했다. PostgreSQL에서 clients/rust/orm-build/src/catalog/row_insert.rs의 insert_row는 MutationPhase::Locked를 알리기 전에 자기 lock을 잡지 않았다. schema 확인(metadata::describe)은 catalog만 읽고, 그 pg_get_expr가 table에 lock을 쥐는지는 부수 효과였으므로, 동시에 실행한 LOCK TABLE … ACCESS EXCLUSIVE NOWAIT이 성공하기도 했고 schema 변경이 확인과 쓰기 사이에 끼어들 수 있었다. MySQL은 이미 lock을 명시적으로 잡는다(SELECT 1 … FOR UPDATE). 변경: PostgreSQL에서 insert는 schema를 확인하고 Locked를 알리기 전에 먼저 LOCK TABLE <table> IN ROW EXCLUSIVE MODE(postgres_insert_lock)를 실행한다. INSERT가 잡는 lock이며 모든 schema 변경과 충돌한다. Red: main에서 PostgreSQL insert는 명시적 lock을 쥐지 않았고, CI는 probe의 lock이 허락되는 것을 찾았다. Green: 새 unit case a_postgres_insert_takes_the_insert_lock_of_its_table이 따옴표 처리를 포함한 statement를 고정하고, cargo fmt --check와 cargo clippy -p orm-build --features live-db --lib --tests -D warnings가 통과한다. database case는 CI가 실행한다.——[o]
G5.64push 전에 로컬 end-to-end 검사를 요구하지 않는다. 원인: AGENTS.md는 커밋마다 그 전에 make owner-check를 요구했고, 항목을 진행하는 동안 Red/Green 테스트와 함께 make owner-check를 실행하게 했으며, linux-runner 검증을 push 전에 로컬에서 확인하게 했다. 그 실행은 개발 machine에서 실제 database server, build, 묶음을 시작한다. CI가 push마다 어차피 실행하는 것이고, 하나하나가 작업을 몇 분씩 멈추었다. 변경: AGENTS.md와 그 한국어 번역은 개발하는 동안 unit test, 곧 바뀐 것의 Red/Green unit case만 실행한다고 적는다. end-to-end 실행(실제 database server, 언어 사이의 conformance, browser, container, 전체 build), make owner-check, 전체 묶음은 push 뒤 CI에서 실행하며, push 전에 필요한 로컬 검사는 없다. make owner-check는 어떤 규칙도 요구하지 않는 도구로 남는다. 바뀐 file을 읽는 것은 그 unit test로 실행하고, linux-runner 검증은 CI가 실행한다. Red: 문서는 커밋마다 그 전의 make owner-check와 push 전의 로컬 linux-runner 실행을 적었다. Green: 둘 다 더 이상 요구가 아니고, node scripts/docs/rules.mjs는 두 번역 모두에서 통과한다.——[o]
G5.64-1전체 suite 규칙의 새 문구를 확인한다. 원인: make repo-check는 AGENTS.md와 그 번역이 정해진 문구(scripts/repo/ci.mjs의 FULL_SUITE_RULE)로 전체 suite 규칙을 적기를 요구하며, 그중에는 "is no required step before a push"와 "push 전에 필요한 단계가 아니다"가 있었다. G5.64는 그 문장을 다시 쓰면서 문구를 고치지 않았으므로, main은 AGENTS.md does not state the full-suite rule로 make repo-check에 실패했다. 문서만 바꾼 commit은 문서 규칙만 실행했다. 변경: 규칙은 G5.64가 쓴 문구 "no local check is required"와 "push 전에 필요한 로컬 검사는 없다"를 요구한다. Red: main에서 case "AGENTS states that the full suite runs on CI after a push"와 repository 실행이 두 번역 모두에서 실패했다. Green: 둘 다 통과한다.——[o]
G5.65모든 검사를 offline으로 실행하고 download는 make install로만 한다. 원인: 검사가 실행 도중에 network에 닿았다. Rust target 20개의 선행 조건인 rust-fetch는 make check 안에서 cargo fetch를 online으로 실행했고, go test와 go run은 빠진 module을 기본 proxy에서 받았으며, scripts/typescript/node-min.sh는 ts-min-check 동안 curl로 가장 낮은 Node를 받았다. cache에서 download를 찾은 실행과 그것을 받아 온 실행을 구별하는 것이 없었으므로, 같은 tree가 registry의 응답에 따라 통과하거나 실패할 수 있었다. 빠진 crate는 cargo의 "retry without the offline flag"로 실패했다. 변경: Makefile은 CARGO_NET_OFFLINE=true, GOPROXY=off, npm_config_offline=true, COMPOSER_DISABLE_NETWORK=1을 export하고, ONLINE은 download target에서만 그것을 지운다. download target은 make install과 그 부분인 install-node, install-php, install-rust(rustup toolchain install과 세 Cargo.lock의 cargo fetch --locked), install-go(go mod download), install-node-min이다. scripts/check/downloads.mjs는 network 없이 crate(cargo fetch --locked --offline), Go module(GOPROXY=off 아래의 go mod download), 두 package-lock.json의 npm package, composer.lock의 Composer package가 고정된 version으로 있는지 확인하고, 빠진 것마다 online으로 다시 시도하라는 말이 아니라 run make install, which downloads it와 함께 적는다. runner는 그것을 setup 단계 downloads로 기록하고, 빠진 download가 필요한 target(rust, go, node-modules, composer)은 그 이유와 함께 실행하지 않는다. make owner-check는 같은 message로 멈춘다. cargo-downloads-check가 Rust target의 선행 조건인 rust-fetch를 대신하고, node-min.sh는 make install-node-min 아래에서만 download한다. CI는 setup step에서 download하고(make install-rust, make install-go를 실행하는 go-modules step, make install-node-min), repository의 server 순서 규칙은 make install target이 server보다 앞서는 것을 허용한다. make repo-check는 네 offline 설정이 없는 Makefile, install target 밖의 download 명령, install target 밖의 $(ONLINE)을 거부한다(offlineMakeErrors). AGENTS.md, CONTRIBUTING.md, docs/docs-development.md와 그 번역이 make install과 offline 규칙을 적는다. Red: main의 Makefile은 네 설정을 하나도 export하지 않았고 rust-fetch는 cargo fetch를 online으로 실행했으며, 새 규칙이 그것을 찾는다. Green: scripts/check/downloads.test.mjs의 새 case 넷이 통과한다. lock file과 설치된 version의 비교, make install을 적고 retry는 적지 않는 빠진 crate와 Go module, install-node-min 밖에서 download를 거부하는 node-min.sh, Makefile 규칙이다. runner, continue, step, repository, owners unit test가 통과하고(30, 9, 6, 65, 19 case), 이 확인은 main checkout에서 모든 download를 찾으며 새 worktree에서는 빠진 npm과 Composer package를 적는다.——[o]
G5.64-2unit test 밖의 검사는 CI에서 실행한다고 CONTRIBUTING.md에 적는다. 원인: G5.64는 AGENTS.md를 다시 썼지만, CONTRIBUTING.md와 그 번역은 여전히 기여자에게 "관련 로컬 검사를 실행"하고 "전체 묶음 make check는 활성 checklist 항목이 모두 완료되었을 때 한 번 실행"하라고 적었다. 변경: 5단계는 바뀐 것의 unit test, 곧 그 Red/Green case를 실행하며, end-to-end 실행, make owner-check, 전체 묶음은 push 뒤 CI에서 실행하고 push 전에 필요한 로컬 검사는 없다고 적는다. 6단계는 make check가 push마다 CI에서 실행된다고 적고, 거부 조건은 전과 같다. Red: 두 문장이 두 번역 모두에 있었다. Green: 둘 다 없고, 문서 규칙과 repository check(65 case)가 통과한다.——[o]
G5.66모든 CI step을 make target으로 실행한다. 원인: workflow는 어떤 make target도 실행하지 않는 명령을 실행했으므로, CI는 로컬 실행이 되풀이할 수 없는 일을 했고 그것을 확인하는 규칙도 없었다. ci.yml은 npm ci 두 번과 npx playwright install, rustup toolchain install, 가장 낮은 PHP를 위한 inline echo version=…, inline php -r SQLite 확인, composer install, database server program을 위한 15줄의 apt, curl, AppArmor script를 실행했다. docs-pages.yml은 npm ci와 npx playwright install을 실행했다. 변경: 그 step들은 make install-node, make install-node-min, make install-browsers, make install-rust(rustup toolchain install을 실행하고 rustc --version을 출력한다), make --no-print-directory ci-php-min-version >> "$GITHUB_OUTPUT", make ci-php-sqlite(scripts/ci/php-sqlite.php), make install-php, make install-server-programs(scripts/ci/server-programs.sh)를 실행한다. node, browser, 가장 낮은 Node 설치는 각각 setup step(node-modules, node-min, browsers)이고, 모두 scripts/check/ci-setup.mjs에 선언된다. make repo-check는 make target이 아닌 것을 실행하는 workflow step을 거부한다(scripts/repo/ci.mjs의 ciMakeErrors). 예외는 ci.yml의 summary step 하나다. 그 step은 끝나지 않은 runner 뒤에도 node scripts/check/summary.mjs를 실행하며 CI_RUN_EXCEPTIONS에 이름이 있다. server 순서 규칙과 재실행 규칙은 설치하고 확인할 뿐 test를 실행하지 않는 선언된 CI setup step이 server보다 앞서고 make check 밖에 있는 것을 허용한다. PHP와 Rust toolchain 규칙은 가장 낮은 PHP step과 rustup toolchain install을 그 target을 거쳐 읽는다. 새 script 둘은 scope suite로 선언한다. Red: 새 규칙은 main의 두 workflow에서 make 밖의 명령 24개를 찾았다. Green: 새 case "every CI step runs a make target"이 fixture에서 통과하고 tree에서 아무것도 찾지 않으며, repository check가 통과하고(66 case), make ci-php-min-version은 version=8.4를 출력한다.——[o]
G5.67문서 workflow의 보고서를 남긴다. 원인: docs-pages.yml은 make docs-build, make docs-verify-idempotent, make docs-static-check를 단순한 step으로 실행했고, job log 말고는 아무것도 남기지 않았으며, summary도 쓰지 않았다. 그래서 실패는 읽을 보고서를 남기지 않았고, 실패 뒤에 건너뛴 step은 그 이유를 적지 않았다. 변경: step 하나가 make docs-ci를 실행한다. 그것은 세 target을 make check의 runner(scripts/check/run.mjs)로 test server 없이(-) 실행한다. 그때 runner는 server와 database 단계 없이 실행하고, database가 필요한 target은 this run has no database servers와 함께 not-run으로 기록하며, target이 필요로 하는 download만 확인하고, 실패한 target 뒤에도 모든 target을 실행한다. target마다의 log와 summary를 .runtime/check/ci_<run id>_<attempt>_docs/report에 쓰고, full-run 기록이 없는 실행이면 job summary에도 쓴다. report step은 !cancelled() 아래에서 그 directory를 올리고, pages artifact는 문서 검사가 통과했을 때만 올린다. Red: main의 workflow는 보고서를 남기지 않았고, runner는 servers file을 요구했다. Green: 새 case "a run without servers runs the targets that need no database and reports them"은 docs-a를 실행하고, db-b를 그 이유와 함께 not-run으로 기록하며, server와 database 단계 없이 실행하고, target이 필요로 하지 않는 crate가 빠져도 downloads 단계를 통과시키며, summary와 target log를 쓴다. runner(31 case)와 repository(66 case) test가 통과한다.——[o]
G5.68부하에서 깨지던 test 둘이 wall time이 아니라 동작을 확인하게 한다. 원인: test 둘은 wall-clock 시간의 상한을 두었고, 부하가 걸린 machine은 결함 없이도 그것을 넘는다. clients/typescript/tests/model.mjs는 row lock을 기다리다 취소된 statement가 4000 ms 안에 돌아오기를 요구했고, scripts/repo/check.test.mjs의 start_logged case는 start_logged가 10초 안에 돌아오고 느린 server는 2.5초 뒤에야 준비되기를 요구했다. 둘 다 요구는 시간이 아니다. statement는 signal 때문에 끝나야 하고, start_logged는 준비 줄 뒤, server가 끝나기 전에 돌아와야 한다. 변경: 취소 case는 다른 connection이 아직 row를 쥐고 있는 동안 statement가 CANCELED를 돌려주고 signal이 이미 abort되었는지 확인한다. 그래서 풀린 lock이나 lock 대기 timeout은 그것을 끝낼 수 없었다. start_logged case는 start_logged가 돌아올 때 가짜 server가 아직 실행 중인지 확인하고, 느린 server는 준비 줄 바로 앞에 marker file을 만들며 start_logged가 돌아올 때 그 file이 있어야 한다. 남은 하한(200 ms 한도를 넘는 대기, 느린 go를 거친 build 둘)은 부하에서 깨지지 않는다. Red: main의 상한 둘. Green: start_logged case가 동작으로 통과하고, TypeScript case는 CI가 실행한다.——[o]
G5.63-1PostgreSQL insert lock을 Rust interface 밖에 두고 그 unit test를 선언한다. 원인: 두 CI 실행에서 conformance-check와 interface_contract가 rust: unexpected clients/rust/orm-build/src/catalog/row_insert.rs::postgres_insert_lock으로 실패했다. G5.63이 helper를 pub(super)로 선언했고, interface check는 private이 아닌 모든 symbol을 contracts/interfaces.json과 비교하기 때문이다. 또 feature-unit-check가 clients/rust/orm-build/tests/unit/row_insert_lock.rs에 대해 every tracked file selects a behaviour test or declares its scope로 실패했다. 변경: postgres_insert_lock은 row_insert의 private 함수이고, 그 자식 test module은 여전히 그것에 닿는다. unit test file은 다른 catalog unit test 옆에서 catalog_connection과 그 catalog-lib-rust 검증의 입력이다. Red: 그 실행들의 두 실패. Green: unit case가 private helper로 통과하고, node scripts/features/owners.mjs --list가 그 file에 catalog-lib-rust를 고르며, owners test가 통과한다(19 case).——[o]
G5.69문서 browser가 임시 directory에 남긴 것을 지운다. 원인: 한 CI 실행에서 docs-check가 check: docs-check left 1 temporary entries: .org.chromium.Chromium.1Yiv3c로 실패했다. headless Chromium은 TMPDIR에 .org.chromium.Chromium.*을 만들고, Linux에서는 browser.close() 뒤에도 그것을 남기기도 한다. G5.55 뒤로는 entry를 남긴 통과한 단계가 실패한다. scripts/docs의 script 넷이 단계의 TMPDIR로 Chromium을 시작했다. 변경: scripts/docs/browser.mjs의 launchBrowser는 자기 임시 directory를 TMPDIR로 주고 Chromium을 시작하며, browser를 닫을 때(닫기가 실패해도) 그 directory를 지운다. check.mjs, prepare.mjs, load.test.mjs, steps.test.mjs는 그것으로 시작한다. npm run docs:static-check는 새 browser.test.mjs를 실행한다. Red: main의 script 넷은 단계의 TMPDIR로 chromium.launch를 불렀고, 새 case가 그것을 거부하며, CI는 남은 것을 찾았다. Green: 새 case "the documentation browser gets a temporary directory of its own, removed when it closes"가 .org.chromium.Chromium.left entry를 남기는 가짜 Chromium으로 통과하고, scripts/docs의 어떤 script도 Chromium을 직접 시작하지 않는다. 실제 browser는 CI가 실행한다.——[o]
G5.70Go 취소 test는 취소하기 전에 그 case 자신의 lock 대기를 기다린다. 원인: 한 CI 실행에서 TestWithContextCancelsInsideTransaction/postgres/nested=true가 blocked read: CANCELED: timeout: context already done: context canceled (code "CANCELED"), want ROLLBACK으로 실패했다. case는 awaitLockWait가 case database에서 lock을 기다리는 session을 보면 context를 취소하는데, 그 확인은 기다리는 모든 session을 count(*)로 셌다. nested=false case가 취소한 statement는 connection이 닫힌 뒤에도 holder가 row를 놓을 때까지 server에서 기다릴 수 있었다. 그래서 다음 case는 그 session을 보고 자기 statement가 시작하기 전에 context를 취소했고, statement는 실패할 rollback 없이 context already done으로 끝났다. 변경: case는 statement를 시작하기 전에 기다리는 session(pg_stat_activity의 pid, performance_schema.data_locks의 thread id)을 읽고, awaitLockWait는 그 가운데 없던 session이 기다릴 때만 돌아온다(newWaiter). 다른 취소 case도 같다. Red: 위의 실행. main의 개수 확인에서는 전부터 기다리던 session이 이 case의 statement로 세어진다. Green: 새 unit case TestNewWaiterIgnoresSessionsThatAlreadyWaited가 전부터 기다리던 session은 무시하고 새 session은 찾는 것을 고정하고, go vet이 통과한다. database case는 CI가 실행한다.——[o]
G5.71통과한 case의 실패 줄은 첫 실패 줄에서 뺀다. 원인: 한 CI 실행에서 feature-unit-check의 첫 실패 줄은 FAIL sample/owner/typescript/none run 1 …: unexpected test output, duplicate observed case first 등이었다. 그 줄은 scripts/features/coverage.test.mjs의 case가 일부러 실패시키는 sample의 출력이고, 그 case는 통과했다. 진짜 실패인 every tracked file selects a behaviour test or declares its scope는 그 뒤에 나와 첫 실패 줄 20개 밖으로 밀렸으므로, summary는 진짜 원인을 적지 않았다. 변경: scripts/check/report.mjs의 failures()는 case마다 그 RUN 줄 때의 실패 줄 수를 기록하고, case가 PASS로 끝나면 그 뒤에 모인 줄을 버린다. 그 줄은 그 case의 출력이지 target의 실패가 아니기 때문이다. 줄은 한도 없이 모으고 읽을 때만 처음 20개로 자르므로, 버린 묶음이 뒤의 실패를 밀어내지 않는다. Red: main의 report.mjs로 새 case "the failure lines of a case that passed are not the first failure lines of the target"는 진짜 실패 대신 sample 줄 25개를 받았다. Green: 첫 실패 줄은 tracked-file case의 FAIL 줄 하나이고, 이전의 첫 실패 줄 case들도 통과한다(32 case).——[o]
G5.72catalog mutation의 lock을 그 Locked publisher 안에서 확인한다. 원인: 한 CI 실행에서 native_updates_lock_compare_verify_and_rollback이 orm-build/tests/row_mutation.rs:137에서 update completed before held-lock probe: true로 실패했다. test는 update의 future를 publisher가 Locked를 알릴 때까지 poll한 뒤 poll을 멈추고, 다른 connection에서 row lock을 확인했다. row_insert_identity.rs의 identity test도 table lock을 같은 방식으로 확인했다. poll을 멈춰도 더 이상 poll이 필요 없는 future는 멈추지 않는다. server가 client가 읽기 전에 각 statement에 답하면(부하가 걸린 runner, local server) update는 한 번의 poll 안에서 모든 답을 읽고, test가 확인하기 전에 commit까지 간다. 변경: 확인은 Locked 때 publisher 자신에서 자기 thread와 자기 connection으로 실행되고(tests/lock_probe/mod.rs의 row_lock_held, table_lock_held), publisher는 확인이 끝난 뒤에야 돌아온다. 그래서 mutation은 scheduling과 상관없이 정확히 Locked에서 멈춘다. 확인 뒤 publisher는 취소 flag를 세우므로, mutation은 JOB_CANCELLED로 끝나고 rollback하며, row는 각 test의 나머지가 기대하는 대로 남는다. 새 module은 두 catalog 검증의 입력이다. Red: 위의 실행. Green: Locked를 알리고 첫 poll 안에 끝나는 가짜 mutation으로 새 unit case a_probe_inside_the_locked_publisher_sees_the_mutation_at_locked가 mutation을 Locked에서 본다. cargo clippy -p orm-build --features live-db --tests -D warnings와 cargo fmt --check가 통과한다. database case는 CI가 실행한다.——[o]
G5.74Go stress parse 옆에 할당 위주의 기준 작업을 출력한다. 원인: G5.58의 checksum 기준 작업은 기계 사이에서 Go parse를 따라가지 못한다. 그 비율은 개발 machine에서 25.0과 26.7, CI runner에서 38.7~43.0이었고, 한 CI 실행은 100 ms budget에 대해 parse를 102 ms로 쟀다. byte loop는 빠듯한 loop 하나의 속도를 재지만, parse는 문자열을 자르고 할당하고 hash를 계산하므로 둘은 processor마다 다르게 변한다. 변경: engine/dbspec/stress_test.go의 TestStressDocument는 할당 위주의 기준 작업도 고정한 thread의 CPU 시계로 다섯 번 잰다. 문서를 줄과 낱말로 나누고 낱말마다 map에 개수를 더한다(allocatingReference, 서로 다른 낱말 22066개). 그 median CPU 시간과 그것에 대한 parse의 비율을 출력하며, CI runner가 그 비율이 유지되는지 보여 줄 때까지 새로 판정하는 것은 없다. Red: main의 test는 checksum 기준 작업만 출력한다. Green: test가 통과하고 stress allocating reference cpu median=… ratio=… words=22066을 출력한다. 개발 machine에서 다섯 번 실행한 비율은 5.26, 5.89, 4.11, 5.33, 5.03(할당 기준 작업 10.2~13.7 ms)이었고, checksum 비율은 25.4~30.9였다.——[o]
G5.73성능은 측정하고 보고할 뿐 test를 실패시키지 않는다. 원인: test가 잰 시간으로 실패했다. stress test 넷은 median parse CPU 시간이 절대 budget(Go 100 ms, PHP 400 ms, TypeScript 250 ms, Rust 300 ms)을 넘으면 실패했고, PHP와 Go budget은 parser의 변화 없이 CI runner에서 386~444 ms와 88~102 ms로 실패했다. Rust assertion 20개와 PHP 확인 5개는 case의 CPU 시간이 한도를 넘으면 실패했고, make perf-check는 hot-path 비율이 한도를 넘으면 실패했으며, Go introspection 비교는 5초를 넘으면 실패했고, Go 취소 test 둘은 취소된 statement가 4초나 10초 뒤에 돌아오면 실패했다. 그 과정에서 기준 작업 비율 budget을 시도했다. macOS에서 PHP 비율은 실행에 따라 6.35에서 10.1까지, Rust 비율은 17.3에서 24.0까지 움직였다. 성능 core와 효율 core가 두 시간을 함께 바꾸기 때문이다. CI runner는 6.84~7.16과 21.8~22.5였다. 성능은 측정하고 보고할 뿐 test를 실패시키지 않으며, 경고는 허용한다. 변경: 각 stress test는 parse마다의 CPU 시간과 wall-clock 시간, median, checksum 기준 작업의 median CPU 시간, 비율, 기계를 출력하고, 비율이 기준값을 넘으면 측정값, 기준값, 기계를 담은 WARNING 줄을 출력한다. 기준값은 PHP 11, Rust 34, Go 63, TypeScript 185이며, CI runner에서 잰 가장 높은 비율의 1.5배이므로 경고는 약 50% 이상의 퇴행을 알린다. Rust와 PHP case의 CPU 한도, cpuCaseEnd, Rust stress 실행, Go와 PHP의 hot-path 측정, introspection 비교는 실패 대신 경고를 출력한다. Go 취소 test 둘은 시간을 출력한다. row를 아직 쥔 동안 statement가 CANCELED를 돌려준 것이 이미 취소가 그것을 끝냈다는 것을 보이기 때문이다. case-clock test는 자기 CPU 시간을 자기 wall-clock 시간과 비교한다. testcase library에 warning(JavaScript, Rust), Warn(Go), testcase_warning(PHP)이 생긴다. check runner는 WARNING 줄을 첫 실패 줄에 넣지 않고, 단계마다 기록하며, summary의 warnings에 적고, GitHub ::warning:: annotation으로 쓴다. make repo-check는 측정한 시간이 한도를 넘었다고 실패하는 test를 거부한다(timeFailureErrors: 측정한 시간이 고정 한도를 넘으면 실패로 가는 if, 측정한 시간이 한도 아래에 머문다는 assertion. 하한, 두 측정의 비교, 0과의 비교는 정확성이다). G5.74의 할당 기준 작업은 지운다. docs/spec/dbspec.md, docs/operations/perf.md, docs/spec/dialects.md, performance_gate 기능, AGENTS.md와 그 번역이 규칙, 기준값, macOS에서는 출력만 하는 이유를 적는다. Red: repository 규칙은 main에서 시간 실패 38곳을 찾았고, main에서 PHP stress test는 CI runner에서 406~444 ms로 실패했다. Green: 규칙은 tree에서 아무것도 찾지 않는다. 기준값을 1로 둔 PHP stress test는 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 …을 출력하고 통과했다. Go와 Rust stress test는 통과하고 비율(26.93, 18.44)을 출력한다. 새 case "a WARNING line is kept as a warning, not a failure, and the summary lists it"와 "a test does not fail on a measured time above a bound"가 통과한다. go vet ./..., cargo clippy --workspace --tests --examples -D warnings, runner(33)와 repository(66) test가 통과한다.——[o]
G5.75새 push가 앞 push의 실행을 취소한다. 원인: ci.yml은 concurrency를 선언하지 않았고 docs-pages.yml은 cancel-in-progress: false로 실행을 남겼다. 그래서 push마다 시작한 실행이 더 새 push 뒤에도 계속되었다. runner는 적고 의미 있는 것은 가장 새 head의 실행뿐인데, 같은 branch의 대체된 실행이 runner를 쥐고 있었다. 변경: 두 workflow는 concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }를 선언하고, make repo-check는 push로 실행하면서 그 workflow 수준 group이 없는 workflow를 거부한다(scripts/repo/ci.mjs의 concurrencyErrors). Red: 새 규칙은 main의 ci.yml과 docs-pages.yml을 찾았다. Green: 새 case "every workflow that runs on push cancels the run of the push before it"가 fixture에서 통과하고(group이 없거나 cancel-in-progress: false인 workflow는 거부하고, push로 실행하지 않는 workflow는 보지 않는다), tree에서 아무것도 찾지 않으며, repository check가 통과한다.——[o]
G5.76PostgreSQL insert lock을 Rust symbol 목록에 기록하고 introspection test를 format한다. 원인: G5.75의 CI 실행에서 target 셋이 실패했다. conformance-check와 interface_contract coverage는 rust: unexpected clients/rust/orm-build/src/catalog/row_insert.rs::postgres_insert_lock을 보고했다. tests/interfaces/rust의 Rust symbol 추출기는 visibility와 상관없이 모든 최상위 함수를 목록에 넣으므로, G5.63-1에서 helper를 private으로 만든 것은 그것을 빼지 못했고, 목록 contracts/symbols/rust.json에는 그것이 없었다. go-fmt-check는 G5.73이 새 상수 묶음을 format하지 않고 남긴 tests/dialects/introspect_compare_physical_test.go에서 실패했다. 변경: 목록은 helper를 기록하고(go run ./tests/interfaces/check -language rust -record), contracts/interfaces.json은 목록의 새 hash를 가지며, test file은 gofmt로 format한다. 이제 commit마다 그 전에 Go file을 gofmt -l로 확인한다. Red: 위의 실행. Green: go run ./tests/interfaces/check -language rust가 native symbol 2781개로 통과하고, Go, PHP, TypeScript interface check가 통과하며, gofmt -l은 아무 file도 적지 않는다.——[o]
G5.77큰 test 복사본을 memory 밖에 두고, group signal을 지키며, runner의 자원을 job log에 남긴다. 원인: G5.76의 CI 실행의 두 번째 시도는 make check 52분쯤에 hosted runner를 잃었고, summary도 보고서도 올라가지 않았다. GitHub는 runner를 끝내는 process, CPU나 memory 부족, 막힌 network를 원인으로 든다. 이 연속 작업의 변경 둘이 그럴 수 있었다. (b) G5.55 뒤로 tests/cargo-test.mjs는 실행하는 모든 test binary를 단계의 TMPDIR, 곧 /tmp 아래로 복사했다. CI runner의 /tmp는 memory를 쓰는 7.8 GiB tmpfs이므로, 검증마다 Rust test binary가 최대 19개씩 16 GB machine의 RAM에 database server, replica, ProxySQL, PgBouncer, 병렬 build와 함께 있었다. (a) G5.55와 G5.61의 group signal은 kill(-group)을 보내는데, 0이나 1의 group이나 runner 자신의 group을 거부하는 것이 없었다. 그런 signal은 runner와 그 job을 멈추게 한다. job log가 자원 줄 없이 끊겼으므로 어느 쪽인지 보이는 log는 없었다. 변경: tests/cargo-test.mjs는 실행 directory를 checkout의 disk인 .runtime/run에 단계 이름(ORM_STEP, 이제 runStep이 넘긴다)을 담아 만들고, runStep은 자기 단계 이름을 담은 실행 directory를 그 단계의 잔여물로 지우므로 kill된 복사본도 지워진다. scripts/check/step.mjs의 signalGroup은 1 이하의 group, runner 자신의 pid와 process group을 거부하고, 거부를 출력하며, 보낸 signal을 모두 기록한다. scripts/check/resources.mjs는 runner가 시작할 때와 실행 중 30초마다 job log에 RESOURCES 줄을 출력한다. free -m, df -h / /tmp, process group과 함께 RSS가 가장 큰 process 다섯, 지난 줄 뒤에 보낸 group signal이다. 흘려 보낸 log는 runner를 잃어도 남는다. Red: main에서 test 복사본은 /tmp로 갔고 signalGroup에는 지킴이 없었다. 새 case의 대조군은 runner 자신의 group에 그대로 보낸 signal이 가짜 runner를 SIGTERM으로 끝내는 것을 보인다. Green: 새 case "a group signal never reaches the process group of the runner"(자기 group의 가짜 runner가 자기 group, 0, 1, -5, 자기 pid로의 signal 다섯을 거부하고, background process를 남기는 단계를 실행하고도 살아 있다), "a run directory named after the step is a leftover of the step", "the resource lines list the processes with the largest resident memory"가 통과하고, cargo-test(7), step(9), runner(33) test가 통과한다. 다음 CI 실행이 자원 줄을 보인다.——[o]
G5.78test file을 runtime.Caller가 아니라 working directory에서 찾는다. 원인: engine/dbspec/harness_test.go(repositoryRoot)와 tests/interfaces/check/grouped_contract_test.go는 runtime.Caller(0)로 저장소를 찾았다. 그것은 binary에 compile된 source 경로이고 실행하는 checkout이 아니다: -trimpath에서는 module 경로이고, 다른 checkout에서 build한 binary에서는 그 checkout의 directory다.—근거: 두 test는 go test가 package directory로 정하는 working directory(os.Getwd)에서 root로 올라간다. scripts/repo/gosource.mjs의 callerPathErrors는 tracked Go file의 runtime.Caller나 runtime.Callers 호출을 그 줄과 고치는 방법과 함께 거부하고, repository unit case는 이전 source에서 두 호출을 찾았고 지금은 찾지 않는다. TestFileVectors, TestGetsCountUsesDedicatedGroupedRowsInEveryClient, make repo-check가 통과한다.[o]
G5.79검사가 쓰는 모든 toolchain을 고정한다. 원인: Node(.node-version)와 Rust(rust-toolchain.toml)는 정확했고 PHP(.php-version)는 x.y였지만, CI는 가장 새 Go 1.27(go-version: "1.27"), setup-php의 가장 새 Composer, 가장 새 libclang-dev와 PostgreSQL 17을 설치했고, 실행한 Go나 Composer를 선언과 비교하는 검사가 없었다. 그래서 그 가운데 하나의 release가 같은 commit의 두 실행 사이에서 검사를 바꾸었다.—근거: .go-version(1.27.0)과 .composer-version(2.10.3)이 정확한 release를 선언하고, ci.yml은 go-version-file: .go-version을 읽고 모든 setup-php step이 tools: composer:2.10.3을 설치한다. scripts/repo/toolchains.mjs의 goVersionErrors와 composerVersionErrors는 정확하지 않은 선언, 다른 x.y의 go.mod go 줄, release를 직접 적거나 빠뜨린 workflow, 다른 release의 실행 중인 go나 composer를 각각 고치는 방법과 함께 거부한다(새 repository unit case 2개). scripts/ci/php-extension-tools.sh는 Ubuntu 26.04의 LLVM인 libclang-21-dev를 설치해 LIBCLANG_PATH로 알리고, scripts/ci/server-programs.sh는 PostgreSQL 17을 요구하고 PgBouncer와 SQLite release를 출력한다. setup-php와 PostgreSQL apt repository는 PHP x.y와 PostgreSQL 17의 가장 새 patch만 주므로 그 둘은 x.y와 major release로 고정한다. make repo-check, full-run-check, feature-unit-check가 통과한다.[o]
G5.80build 출력을 원자적으로 publish한다: 임시 file이나 directory에 build하고 rename한다. 원인: go build -o는 checkout의 모든 실행이 실행하는 .runtime/bin의 lease program, stop-process, new-session과 timing test binary를 최종 경로에 바로 썼고, node tests/dbspec/stress.mjs > <document>는 stress 문서로 redirect했고, tsc는 clients/typescript/dist를 file마다 썼고, scripts/cargo-build-copy.sh는 실행 directory의 최종 경로로 바로 복사했고, scripts/typescript/node-min.sh는 cache directory에 Node를 풀었다. 그래서 멈춘 build가 반쪽 file이나 directory를 남겼고 뒤의 실행이 그것을 완성된 것으로 읽었다.—근거: scripts/publish-output.sh는 명령을 <output>.next-<pid>(인자 @OUT@, 또는 표준 출력)에 실행하고 그것을 rename으로 출력에 덮으며, 명령이 실패하면 지운다(실패한 명령은 이전 file을 남겼다). Makefile은 세 program, stress 문서, timing binary를 $(PUBLISH)로 build한다. scripts/typescript/build.mjs는 dist.next-<pid>에 build하고 rename으로 최종 경로에 놓으며 TSC_BUILD와 npm build가 그것을 쓴다(model은 바뀌지 않았다). cargo-build-copy.sh와 node-min.sh는 임시 경로에 복사하고 풀어 rename한다. make repo-check의 unpublishedOutputErrors는 Go build 출력을 최종 경로에 바로 쓰거나, file로 redirect하거나, tsc를 최종 경로로 바로 실행하는 Makefile 줄을 고치는 방법과 함께 거부한다. 이전 Makefile에서 아홉 줄을 찾았고 지금은 찾지 않는다. make repo-check, full-run-check, feature-unit-check, testcase-check/node가 통과한다.[o]
G5.81실행 조건이 없다는 모든 실패 message에 원인과 고치는 방법을 적는다. 원인: 검사와 test(scripts, tests, bench, examples, client test)의 실패 message 98개는 환경 변수, 인자, 도구가 없을 때 원인에서 멈추었다. 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, ORM_CHECK_RUN_ID is unset 같은 것이며, 읽는 사람이 그것을 마련하는 target이나 step을 찾아야 했다.—근거: 각 message는 이제 원인 뒤에 고치는 방법을 적는다: server 환경을 주는 make target, 실행의 bench database를 위한 make check나 make run-databases, coverage 변수를 위한 make feature-check, stress 문서를 위한 make dbspec-compare-check, build한 program을 위한 make example-check, 도구를 위한 make install-server-programs나 package manager, 줄 인자, 설치할 release다. 없는 class를 확인하는 test assertion 두 개는 무엇이 없는지를 적는다. scripts/repo/messages.mjs의 fixlessMessageErrors는 그 범위의 실패 줄이 없는 조건(is required, is unset, is not set, is not installed)을 고치는 방법(; run, ; set, ; install, ; give 등이나 , which) 없이 적으면 거부한다. 이전 source에서 98줄을 찾았고 지금은 찾지 않는다. 모든 build tag의 go vet, Rust test와 bench의 cargo check, php -l, node --check, sh -n, rust-fmt-check, repo-check, full-run-check, feature-unit-check가 통과한다.[o]
G5.82함께 쓰는 모든 port와 database를 lease로 갖거나 실행마다 소유한다. 원인: test server는 Makefile의 고정 port 33171, 55471, 33181, 55481, 33182, 55482에서 listen했고, checkout 사이에서 그것을 덮는 lease가 없었다. server의 lease는 한 checkout의 .runtime에 있으므로 둘째 checkout(worktree)의 server가 같은 port를 다투었다. database는 이미 소유되어 있었다: 실행마다 bench와 decimal database를 만들고(scripts/check/databases.sh), database case마다 orm_case_<pid>_<n> database를 만들며(internal/testdb와 그 짝), Rust tool test는 orm_tools의 table 이름에 process id를 넣고, SQLite file은 process마다 따로이며, 모든 실행은 자기 checkout server의 shared lease를 가진다(WITH_TEST_ENV).—근거: test-servers.sh start는 checkout의 server를 시작할 때 127.0.0.1의 빈 port 여섯 개를 고르므로(scripts/free-ports.mjs) server는 listen하는 순간부터 그 port를 소유한다. 실행 중인 server의 start와 tls는 .runtime/servers/env의 DSN에서 port를 읽는다(env_ports는 빠진 변수와 고치는 방법을 적는다). Makefile은 port를 적지 않고, CONTRIBUTING.md와 한국어 짝 문서는 고른 port를 설명한다. make repo-check의 fixedPortErrors는 Makefile과 shell script의 숫자 *PORT= 할당과 port를 주는 test-servers.sh start를 거부한다. 이전 Makefile에서 일곱 줄을 찾았고 지금은 찾지 않는다. socket 경로 case는 port 없이 script를 시작한다. make repo-check, full-run-check, feature-unit-check, docs-rules-check, docs-check가 통과하고, server 시작 자체는 CI에서 실행된다.[o]
G5.83G5.78부터 G5.82까지의 규칙을 AGENTS.md에 적는다. 원인: 고정한 toolchain, 원자적인 build 출력, 고치는 방법을 적은 실패 message, lease로 갖거나 소유한 port와 database, compile 시점 source 경로의 금지는 make repo-check가 강제했지만 개발 규칙으로 적혀 있지 않아 기여자는 그것을 검사 실패로만 만났다.—근거: AGENTS.md와 AGENTS.ko.md는 unit test 규칙 앞에 다섯 규칙을 적는다: 모든 toolchain을 선언 하나로 고정하고(.node-version, .go-version, .composer-version, rust-toolchain.toml은 정확하게, .php-version과 PostgreSQL major는 설치 도구가 허용하는 만큼) CI 증거와 함께 새 release를 들이는 것, 다른 것이 읽는 build 출력을 옆에 build하고 rename해 publish하는 것($(PUBLISH), scripts/typescript/build.mjs), 원인과 고치는 방법을 적는 실패 message, lease로 갖거나 한 실행이 소유하는 공유 port와 database와 고정 TCP port의 금지, runtime.Caller가 아니라 working directory나 Makefile이 준 경로에서 file을 찾는 것이다. docs-rules-check, repo-check, feature-unit-check가 통과한다.[o]
G5.48checklist 항목이 진행 중인 동안 push를 pre-push hook에서, 그리고 CI에서 다시 거부한다. 원인: AGENTS.md는 진행 중 항목이 없을 때만 push를 허용했지만 push 전에 그것을 확인하는 것이 없었고, 저장소에 GitHub 원격이 있는데도 첫 줄은 여전히 "원격 저장소를 추가하거나 푸시하지 않는다"였다.—근거: .githooks/pre-push(mode 100755)는 scripts/check/push-gate.mjs(push 검사)를 실행하고, 그것은 push하는 commit마다와 working tree의 docs/checklist.md를 guard의 activeItems로 읽어 각 항목의 ID와 제목, 규칙, 고치는 방법을 적고 거부한다. 읽을 수 없는 checklist, git 오류, 예외도 거부한다. scripts/check/hooks.mjs는 hook이 실행되지 않을 이유(core.hooksPath가 .githooks가 아님, hook이 없거나 실행 가능하지 않음)를 알린다. Makefile은 parse 때 두 hook의 core.hooksPath를 둔다(G5.49-1의 commit-msg block과 합쳤다). make hooks는 그것을 두고 확인하며, make owner-check는 make hooks-check로 시작하고, 전체 묶음의 guard(scripts/check/full-run.mjs)는 hook이 없는 checkout을 거부한다. .github/workflows/push-gate.yml은 tag의 commit에 고정한 action으로 make push-gate-commit COMMIT=<push한 sha>를 실행하고, 이것은 commit이 hook을 mode 100755로 추적하기도 요구하며 거부를 annotation과 step summary로 적는다. AGENTS.md(+ko)가 push 규칙과 그 검사를 적는다. scripts/check/push-gate.test.mjs는 실제 hook을 거쳐 임시 bare 원격에 push하고(push하는 commit이나 working tree에 진행 중 항목이 있으면 거부, 아니면 허용) CI 명령과 hooks-check를 실행하며, full-run.test.mjs는 core.hooksPath가 없는 checkout을 어떤 단계보다 먼저 거부한다. make full-run-check(test 63개), make repo-check, make docs-check, make hooks-check가 통과하고, make push-gate-commit COMMIT=HEAD는 hook을 추적하지 않는 이전 commit을 거부했다.[o]
G5.85main에서 make feature-unit-check가 다시 통과하게 한다. 원인: G5.48에서 owners test는 ".github/workflows/push-gate.yml과 clients/php-extension/tests/dbspec_apply_cleanup_test.php가 behaviour test를 고르지 않고 scope도 선언하지 않는다"로 실패했고(둘 다 G5.48과 T9.13이 더했다), contracts/features.json의 schema_definition description 대신 생성 문서 docs/features.md(+ko)가 고쳐졌으므로 feature-docs-check는 "generated document is stale: docs/features.md"로 실패했다.—근거: contracts/check-inputs.json은 push-gate.yml을 suite 입력으로 선언하고, contracts/features.json은 apply 정리 test 두 개를 schema_definition의 test와 make dbspec-php-extension-check의 입력에 적으며, schema_definition description(en, ko)은 문서처럼 "그것을 부르는 code"로 쓰고, node scripts/features/build.mjs는 docs/features.md(+ko)를 바꾸지 않는다. make feature-unit-check(owners test 19개, coverage 16개)와 make repo-check가 통과한다.[o]
G5.86orm의 PHP package와 namespace를 polyspec 아래에 둔다. 원인: PHP client는 namespace Orm의 composer package orm/php-client, 확장은 Orm\Dbspec\Native의 orm/orm-dbspec였고, orm이 commit한 generated model과 test model은 Polyspec\Orm\Tests\Model과 polyspec 밖의 namespace를 썼다. 변경: composer package는 polyspec/orm과 polyspec/orm-dbspec이고, library namespace는 src, testing, bin, 모든 test에서 Polyspec\Orm이며, 확장은 Polyspec\Orm\Dbspec\Native를 등록한다(stub, 생성 arginfo, C 함수 이름). orm-gen은 기본값 없는 필수 --namespace를 그대로 두고, orm이 commit한 clients/php/gen model은 Polyspec\Orm\Tests\Model, test model은 Polyspec\Orm\Tests\<name>, 사용자 설정의 예는 Example\Model을 쓴다. 사용자는 use Orm\…를 use Polyspec\Orm\…로 바꾸고 polyspec/orm(과 polyspec/orm-dbspec)을 require하며, generated model이 Polyspec\Orm\Model과 Polyspec\Orm\StyledValue를 import하므로 모든 PHP model set을 orm-gen으로 다시 생성한다. Go, Rust, TypeScript 생성기는 namespace를 받지 않는다.—근거: clients/php/gen을 다시 생성했고(entity 12개), contracts/symbols/php.json과 php-extension.json을 다시 기록했으며 contracts/interfaces.json과 contracts/validate.go가 새 class를 적는다. test autoloader는 더 긴 Polyspec\Orm\Tests\Model\ prefix를 먼저 본다. 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, make docs-check가 통과하고, server suite는 CI에서 실행된다.[o]
G5.87orm의 Rust crate와 npm package 이름을 polyspec 규칙에 맞추고 PHP lockfile을 고친다. 원인: Rust crate는 orm, orm-build, orm-schema(library orm, orm_build, orm_schema), npm package는 @polyspec/orm-typescript, workspace package는 orm-docs였지만 polyspec package의 이름은 polyspec-<name>과 @polyspec/<name>이다. 그리고 G5.86은 clients/php/composer.json의 이름을 바꾸면서 lock file의 content hash를 바꾸지 않았으므로 make package-check가 "The lock file is not up to date"로 실패했다. 변경: 모든 manifest, 경로, -p 인자, build 도구 template, 문서에서 crate는 polyspec-orm, polyspec-orm-build, polyspec-orm-schema(library polyspec_orm, polyspec_orm_build, polyspec_orm_schema)이고, npm package는 @polyspec/orm, workspace는 @polyspec/orm-workspace이며, clients/php/composer.lock은 이름을 바꾼 composer.json의 hash를 가진다. Go module은 이미 github.com/polyspec/orm이고, 내부 test crate는 이름을 그대로 둔다. 사용자는 polyspec-orm에 의존하고 polyspec-orm-build로 build하며 polyspec_orm:: 경로와 polyspec_orm::models!()를 쓰고(build script가 Rust model을 다시 만든다), @polyspec/orm을 설치하고 출력이 이제 @polyspec/orm을 import하는 orm-gen으로 TypeScript model을 다시 생성한다.—근거: cargo build --workspace --all-targets --all-features와 bench workspace build, make rust-fmt-check, make package-check(TypeScript pack, composer validate, 외부 path dependency로 쓴 Rust crate, Go module), make ts-model-check, make go-model-check, make dbspec-ts-check, TypeScript의 generate, scan, DSN, package 선언 test, 다시 기록한 symbol로 make interface-check, make repo-check, make version-check, make feature-unit-check, make full-run-check, make docs-check가 통과하고, server suite는 CI에서 실행된다.[o]
G5.88push된 범위나 pull request의 제목을 검사한다. 변경: git-check(scripts/git/check.mjs)는 ORM_GIT_RANGE(<base>..<head>)의 commit 제목을 읽고, 그것이 없으면 HEAD 하나를 읽으며, 풀리지 않는 범위는 원인과 범위를 주는 법을 적고 실패한다. CI workflow의 make check step은 ORM_GIT_RANGE를 pull request의 범위나 branch에 더하는 push의 범위로 정한다. 새 branch, 강제 push, 수동 실행은 HEAD를 검사한다. make repo-check는 make check step이 그 범위를 주지 않는 workflow를 실패시킨다. contracts/rules.json의 git.subject-format은 type과 제목 길이를 가지고, commit-msg hook은 제목을 쓸 때마다 검사하며, AGENTS.md는 그 범위를 적는다. scripts/repo/check.test.mjs의 CI setup case와 독립 test case는 써 둔 workflow와 Makefile, 그리고 추적하는 것을 검사한다.—Evidence: Red: 바꾸기 전의 check에서 node --test scripts/git/check.test.mjs는 subjectRange를 import하지 못해 실패했고, workflow 규칙은 ORM_GIT_RANGE가 없는 ci.yml을 거부했다. Green: make git-check가 unit case 7개(범위 case는 start..pushed에서 오류가 없고, start..next에서 소문자 제목 하나를 찾고, HEAD 하나를 읽고, missing..next에서 실패한다)와 HEAD의 제목을 통과한다. make repo-check/unit은 74 case를, node scripts/repo/check.mjs는 통과하고, make full-run-check는 63 case를, make testcase-check/node는 6 case를 통과한다.[o]
G5.89ordered-json에 그 저장소로 의존한다. 변경: clients/rust/orm/Cargo.toml은 ordered-json을 version과 git으로, clients/typescript/package.json은 github:polyspec/ordered-json으로, clients/php/composer.json은 dev-main으로 적으므로 lock 갱신은 저장소의 main을 받는다. Cargo.lock과 package-lock.json은 그것이 푼 revision을 기록한다. docs/codec.md는 저장소 main의 ordered-json 0.0.1을 적는다.—근거: clients/rust와 bench/rust에서 cargo metadata --offline --locked가 통과하고 cargo check -p polyspec-orm --lib --offline --locked가 build된다. clients/typescript에서 npm ls --package-lock-only는 git source의 ordered-json 0.0.1을 보인다. lock file은 게시할 때 cargo update -p ordered-json과 clients/typescript의 npm install로 그 main에 맞춘다.[o]
G5.90ordered-json의 Go module을 release version으로 요구한다. 원인: go.mod는 ordered-json의 module을 commit을 적는 pseudo-version으로 요구했다. ordered-json에 Go module의 version tag가 없었기 때문이다. 완료 기준: go.mod와 go.sum이 github.com/polyspec/ordered-json/packages/ordered-json-go를 tag packages/ordered-json-go/v0.0.7의 v0.0.7로 요구한다.—Evidence: go build ./...와 make go-vet-check가 0으로 끝난다.[o]
G5.91모든 변경을 pull request와 merge queue로 게시한다. 변경: .github/ruleset.json은 GitHub ruleset main(승인 없는 pull request, REBASE의 merge queue, 선형 history, GitHub Actions check gate, test, build, force-push·삭제 금지, bypass actor 없음)과 저장소 설정 allow_rebase_merge, allow_auto_merge, delete_branch_on_merge를 선언하고, make github-ruleset은 이를 적용하며 make github-ruleset-check는 다른 field를 밝히며 실패한다(scripts/github/ruleset.mjs). workflow ci, push-gate, docs-pages는 merge_group에서 실행되고, merge group의 ORM_GIT_RANGE는 그 base와 head다. AGENTS는 pull request 흐름을 적는다. Red: .github/ruleset.json 없이 scripts/github/ruleset.test.mjs는 load되지 않았다. Green: 가짜 GitHub CLI로 그 8개 사례가 통과한다(make github-check, CHECK_TARGETS에 있음).——[o]
G5.93orm의 내부 Rust crate를 polyspec 아래에 둔다. 원인: test, bench, symbol crate는 orm-testcase, orm-case-clock, orm-case-database, orm-tests, orm-bench, orm-interface-symbols(library orm_testcase, orm_case_clock, orm_case_database)였고 polyspec crate의 이름은 polyspec-<name>이다. 또 scripts/version/check.mjs는 Cargo.lock version과 manifest version 고정을 어느 package도 갖지 않는 이름 orm, orm-schema, orm-build로 읽어서 아무것도 읽지 않았다. 변경: crate는 모든 manifest, lock file, -p 인자, Rust 경로, program 경로, 문서에서 polyspec-orm-testcase, polyspec-orm-case-clock, polyspec-orm-case-database, polyspec-orm-tests, polyspec-orm-bench, polyspec-orm-interface-symbols(library polyspec_orm_testcase, polyspec_orm_case_clock, polyspec_orm_case_database)이고, version check는 polyspec-orm package를 읽는다. Red: scripts/version/check.test.mjs의 새 case the version of each Rust package in a lockfile and each version pin of a manifest are read가 실패했다. Green: clients/rust의 cargo check --workspace --all-targets --all-features --offline --locked와 bench/rust의 cargo check --all-targets가 통과하고, symbol 도구는 polyspec-orm-interface-symbols로 build되며, make rust-fmt-check, make version-check(case 4개), make repo-check(case 74개), make testcase-check/rust, make feature-unit-check, tests/interfaces/check와 tests/conformance/check의 go vet이 통과한다.——[o]
G5.94ordered-json의 PHP package를 polyspec 아래의 이름 polyspec/ordered-json으로 require한다. 원인: clients/php/composer.json은 ordered-json/ordered-json을 require했고 PHP client는 OrderedJson\Value, OrderedJson\parse, OrderedJson\stringify를 불렀지만, ordered-json의 main에서 그 package는 namespace Polyspec\OrderedJson의 polyspec/ordered-json이다. 변경: clients/php/composer.json은 polyspec/ordered-json(그 저장소의 dev-main)을 require하고, clients/php/src(Codec, Model), PHP test, tests/codec/check.php, tests/conformance/result_php.php, docs/spec/codec.md(+ko)는 Polyspec\OrderedJson을 쓴다. clients/php/composer.lock은 게시할 때 composer update polyspec/ordered-json으로 그 package에 맞춘다. Red: ordered-json main의 PHP library로 php clients/php/tests/styled_value_test.php와 php tests/codec/check.php가 Call to undefined function OrderedJson\parse()로 실패했다. Green: 그 library로 styled_value_test.php가 case를 모두 통과하고, tests/codec/check.php가 codec/php-vectors와 codec/php-errors를, aes_json_test.php가 SQLite를 통과하며, tests/conformance/result_php.php가 통과하고, 바뀐 모든 file의 php -l이 통과한다. MySQL과 PostgreSQL case는 CI에서 실행된다.——[o]
G5.95ordered-json의 package를 그 package 이름에 따라 요구하고 그 main에 고정한다. 변경: clients/rust/orm/Cargo.toml은 dependency key ordered-json 아래에 crate polyspec-ordered-json을 적어 ordered_json:: 경로를 유지한다. clients/typescript/package.json은 @polyspec/ordered-json에 의존하고, client source와 test는 그 package를 import하며, client 밖의 test는 clients/typescript/node_modules/@polyspec/ordered-json에서 import한다. clients/rust/Cargo.lock, bench/rust/Cargo.lock, clients/typescript/package-lock.json, clients/php/composer.lock은 package를 polyspec/ordered-json의 branch main에서 받고, go.mod는 module github.com/polyspec/ordered-json/go를 그 main으로 요구한다. Red: 이전 Cargo manifest로 main을 resolve하면 "no matching package named ordered-json found"로 실패했고, main은 npm package를 @polyspec/ordered-json으로 게시하며, make codec-check는 Cannot find module '…/node_modules/ordered-json/js/index.js'로, 이전 composer.lock으로는 Call to undefined function Polyspec\OrderedJson\parse()로 실패했다. Green: make ts-check, make interface-check, make rust-check, go build ./...가 통과하고, make codec-check는 G5.96과 함께 통과한다.——[o]
G5.96Rust test feature를 package 이름에 따라 고른다. 원인: crate는 polyspec-orm과 polyspec-orm-build인데 Makefile의 ORM_RUST_TEST_FEATURES는 여전히 orm/test-faults,orm-build/live-db를 적어, --workspace --features $(ORM_RUST_TEST_FEATURES)로 실행하는 모든 cargo test가 compile 전에 실패했다. 변경: ORM_RUST_TEST_FEATURES는 polyspec-orm/test-faults,polyspec-orm-build/live-db이고, 그 위 주석은 polyspec-orm-tests를 적는다. Red: make codec-check가 codec-check/rust에서 none of the selected packages contains these features: orm-build/live-db, orm/test-faults로 실패했다. Green: make codec-check가 통과한다.——[o]
G5.97ordered-json에 그 tag로 의존한다. 변경: clients/rust/orm/Cargo.toml은 github.com/polyspec/ordered-json의 tag v0.0.1을 적고, clients/typescript/package.json은 github:polyspec/ordered-json#v0.0.1에 의존하며, clients/php/composer.json은 그 저장소의 polyspec/ordered-json 0.0.1을 require하고, go.mod는 그 저장소의 tag go/v0.0.1인 github.com/polyspec/ordered-json/go v0.0.1을 요구한다. clients/rust/Cargo.lock, bench/rust/Cargo.lock, clients/typescript/package-lock.json, clients/php/composer.lock, go.sum은 branch main 대신 그 tag를 받는다. Green: go build ./..., make rust-check, make ts-check, make codec-check, make version-check, make repo-check, make package-check가 통과한다.——[o]
G5.98모든 검사를 pull request와 merge group에서 ci.yml로 실행한다. 원인: ci.yml과 push-gate.yml은 다른 event와 함께 모든 push에서 실행되었고, push-gate.yml은 pull request에서 실행되지 않았으며, 문서 검사는 docs-pages.yml의 job build에서 실행되었고 ruleset은 그것을 check build로 요구했다. 변경: ci.yml은 pull_request, merge_group, workflow_dispatch에서 실행하고 모든 검사 job을 가진다: test는 pull request나 merge group의 ORM_GIT_RANGE로 make check를 실행하고, docs는 그 job의 setup과 report upload와 함께 make docs-ci(docs-build, docs-verify-idempotent, docs-static-check)를 실행한다. push-gate.yml은 push(branches-ignore gh-readonly-queue/**), pull_request, merge_group에서 job push-gate로 실행한다. docs-pages.yml은 main의 push와 workflow_dispatch에서 실행하고 make docs-build로 site를 build해 upload하고 deploy만 한다. .github/ruleset.json은 check push-gate, test, docs를 요구한다. make repo-check는 세 workflow의 on: block을 확인하고, 다른 workflow의 push, pull_request, merge_group을 거부하며, setup과 make check 뒤의 step을 job마다 읽는다. Red: repository case every workflow runs on its declared events only가 ci.yml, push-gate.yml, docs-pages.yml의 trigger로 실패했고, make github-check는 ruleset에 ci.yml의 check docs가 없어 실패했다. Green: make repo-check, make github-check, make checklist-check, make docs-rules-check, make version-check, make git-check가 통과한다.——[o]
G5.99docs-pages.yml의 사이트 build 전에 브라우저를 설치한다. 원인: scripts/docs/prepare.mjs는 Chromium으로 Mermaid 그림을 그리는데, docs-pages.yml의 build job은 make install-browsers 없이 make docs-build를 실행해 browserType.launch: Executable doesn't exist로 실패했고 사이트가 게시되지 않았다. 변경: build job은 make docs-build 전에 make install-browsers를 실행하고, make repo-check는 앞 step에서 make install-browsers를 실행하지 않고 make docs-build나 make docs-ci를 실행하는 workflow job에서 실패한다. Red: 새 case every job that builds the site installs the browsers before it가 docs-pages.yml에서 실패했다. Green: make repo-check가 통과한다.——[o]
G5.100Mermaid를 읽는 이의 브라우저에서 그리고, 사이트의 build와 검사를 Node만으로 실행한다. 원인: scripts/docs/prepare.mjs는 VitePress build 전에 docs/**/.md의 모든 Mermaid fence를 headless Chromium으로 docs/public/diagrams/.svg와 docs/.vitepress/generated/diagrams.json에 그렸고, docs/.vitepress/config.mts는 각 fence를 그 SVG의 image로 바꿨으며, make docs-static-check는 build한 사이트를 Chromium으로 열었으므로, 사이트 build와 그 검사에 브라우저가 필요했다. 변경: config.mts는 각 Mermaid fence를 escape한 소스로 <pre class="mermaid" v-pre>에 넣고, docs/.vitepress/theme/index.ts는 page가 mount된 뒤와 route가 바뀐 뒤마다 client에서 mermaid를 import해 그 요소에 실행한다. scripts/docs/prepare.mjs, browser.mjs, steps.mjs, load.mjs와 그 test, target docs-static-check, target install-browsers, ci.yml과 docs-pages.yml의 setup step browsers, scripts/check/ci-setup.mjs의 setup id browsers, 사이트 build 전에 make install-browsers를 요구하던 repository 규칙, dev dependency playwright와 markdown-it을 없앤다. scripts/docs/check.mjs(build 뒤의 make docs-check)는 build한 HTML을 Node로 읽는다: 모든 Markdown page에 HTML page가 있고, 모든 내부 link, stylesheet, image, script 경로가 docs/.vitepress/dist의 file을 가리키며, 모든 fragment가 그 page의 id를 가리키고, page들이 Mermaid 소스를 20개 이상 가진다. make docs-ci는 docs-verify-idempotent와 docs-check를 실행한다. Red: case docs-site/links가 없는 anchor를 가리키는 link가 있는 build page에서(Missing anchor), 없는 page를 가리키는 link가 있는 page에서(ENOENT) 실패했다. Green: 빈 PLAYWRIGHT_BROWSERS_PATH와 설치되지 않은 playwright package로 make docs-build, make docs-check(48 page, 내부 대상 566개, Mermaid 소스 20개), make docs-verify-idempotent(같은 file 229개), make docs-rules-check가 통과한다. make repo-check, make checklist-check, make version-check, make git-check가 통과한다.——[o]
G5.101rust-150-check에서 Rust crate의 package 이름을 써서 의존한다. 원인: scripts/check-rust-150.sh는 Cargo.toml이 package orm과 orm-build에 의존하는 crate를 쓰는데, clients/rust/orm과 clients/rust/orm-build의 crate는 package polyspec-orm과 polyspec-orm-build이므로, cargo check가 생성된 150개 model을 compile하기 전에 "no matching package named orm found"로 실패했다. 변경: 생성되는 Cargo.toml은 build.rs와 lib.rs가 polyspec_orm_build::와 polyspec_orm::으로 부르는 polyspec-orm과 polyspec-orm-build에 의존한다. Red: make rust-150-check가 "no matching package named orm found"로 실패했다. Green: make rust-150-check가 rust-150: 150 generated entities compiled로 통과한다.——[o]
G5.102polyspec_orm이라는 table에 Rust model module polyspec_orm_model을 준다. 원인: 생성된 Rust model은 runtime crate를 상대 path polyspec_orm::으로 부르는데, clients/rust/orm-build/src/generate.rs는 Rust keyword, orm, std, core, alloc이라는 table의 module만 이름을 바꾸므로, table polyspec_orm은 model 안에서 crate를 가리는 module polyspec_orm을 얻었고 생성된 소스가 "cannot find Schema in polyspec_orm"으로 compile되지 않았다. 변경: 이름을 바꾸는 module은 Rust keyword, polyspec_orm, std, core, alloc이다. clients/rust/orm-build/tests/generate.rs의 새 case a_table_named_like_the_runtime_crate_gets_a_model_module_of_its_own은 table polyspec_orm의 model을 생성한다. Red: 그 case가 the model module of table polyspec_orm is polyspec_orm_model로 실패했고, table polyspec_orm의 model을 생성하는 crate가 "cannot find Schema in polyspec_orm"으로 compile되지 않았다. Green: cargo test -p polyspec-orm-build --test generate가 통과하고 그 crate가 compile된다.——[o]
G5.103PHP 확장 parser의 setjmp를 longjmp 전에 바뀌는 지역 변수가 없는 함수에서 부른다. 원인: clients/php-extension/src/parse.c의 run은 setjmp(p->stop_jump)를 부르고 지역 변수 stopped를 그 너머로 유지하므로, -Wextra(-Wclobbered)의 GCC가 parse.c:2093에서 variable 'stopped' might be clobbered by 'longjmp' or 'vfork'로 경고하고, 컴파일러 경고가 있는 build를 실패시키는 clients/php-extension/scripts/build.sh가 Linux에서 dbspec-introspect-php-extension-check, dbspec-introspect-compare-check, dbspec-apply-php-extension-check, dbspec-apply-pairs-check의 확장 build를 실패시켰다. 변경: 새 함수 read_until_stop이 setjmp를 갖고 stop이 되돌아오면 false를 돌려준다. 이 함수에는 setjmp와 longjmp 사이에 바뀌는 지역 변수가 없고(C11 7.13.2.1), run은 그 결과로 stopped를 정한다. 확장의 다른 setjmp인 expression.c의 expression_parse는 setjmp와 longjmp 사이에 tree만 바꾸고, jump 뒤에는 그것을 읽지 않고 돌아간다. Red: GCC 16.2와 config.m4의 flag로 확장을 compile하면 parse.c:2093에서 -Wclobbered로 경고했다. Green: 같은 GCC build에서 parse.c와 expression.c에 경고가 없고, sh clients/php-extension/scripts/build.sh가 library를 게시하며, make dbspec-php-extension-check/vectors, make dbspec-php-extension-check/plan, make dbspec-php-extension-check/declarations가 통과한다.——[o]
G5.104PHP 확장 객체의 bool과 null property를 그 slot에 바로 쓴다. 원인: clients/php-extension/src/convert.c의 obj_put_bool과 obj_put_null은 지역 zval의 type만 정하고 그것을 obj_put에 넘기는데, obj_put의 ZVAL_COPY_VALUE는 한 번도 쓰지 않은 그 zval의 value를 읽는다. GCC 16은 그 함수가 zv_result, out_document, out_table, out_setting, out_plan, out_effect, out_steps에 inline된 곳에서 'v.value.counted' is used uninitialized와 may be used uninitialized로 경고하므로, 컴파일러 경고가 있는 build를 실패시키는 clients/php-extension/scripts/build.sh가 그 컴파일러에서 실패한다. 변경: 새 released_slot은 property의 값을 놓고 그 slot을 돌려준다. obj_put은 값을 그 slot에 복사하고, obj_put_bool과 obj_put_null은 ZVAL_BOOL과 ZVAL_NULL로 그 slot에 type을 쓴다. Red: GCC 16.2와 config.m4의 flag로 확장을 compile하면 convert.c에서 -Wuninitialized와 -Wmaybe-uninitialized 경고 14개가 났다. Green: 모든 소스를 처음부터 GCC 16.2로 build해도 컴파일러 경고가 없고, sh clients/php-extension/scripts/build.sh가 library를 게시하며, make dbspec-php-extension-check/vectors, make dbspec-php-extension-check/plan, make dbspec-php-extension-check/declarations, make dbspec-php-extension-check/mermaid, make dbspec-php-extension-check/apply-cleanup이 통과한다.——[o]
G5.105PHP schema_set test의 외부 문서 set을 test가 부르는 namespace에 생성한다. 원인: clients/php/tests/schema_set_test.php의 externalSets는 set core, member, member_v2, drifted의 model을 namespace ExtCore\Orm, ExtMember\Orm, ExtMemberV2\Orm, ExtDrifted\Orm에 생성하는데, externalDocuments는 post를 \Polyspec\Orm\Tests\ExtMember\ExtPost로 만들므로, client-db-check와 client-pooler-check에서 schema_set/external_documents가 SQLite, MySQL, PostgreSQL에서 Class "Polyspec\Orm\Tests\ExtMember\ExtPost" not found로 실패했다. 변경: externalSets는 PHP test의 다른 생성 set처럼 각 set을 Polyspec\Orm\Tests\<prefix>에 생성하고 autoload한다. Red: php clients/php/tests/schema_set_test.php가 schema_set/external_documents/sqlite, /mysql, /postgres에서 그 오류로 실패했다. Green: php clients/php/tests/schema_set_test.php가 세 database에서 모든 case를 통과한다.——[o]
G5.106interface 검사의 PHP 레코드 fixture를 namespace Polyspec\Orm에 선언한다. 원인: tests/interfaces/check/php_test.go의 TestPHPRecordSourceMutationChangesExtractedWire는 class Validator를 namespace Orm;에 쓰고 그 레코드를 key fixture.php::Polyspec\Orm\Validator::Request#wire로 읽으므로, key가 없어 go-test-check가 PHP record declaration was not extracted: ""로 실패했다. 변경: fixture는 PHP client의 Validator의 namespace인 namespace Polyspec\Orm;을 선언한다. Red: go test ./tests/interfaces/check -run TestPHPRecordSourceMutationChangesExtractedWire가 그 message로 실패했다. Green: 같은 명령이 통과한다.——[o]
G5.107PHP 오류 code를 namespace Polyspec\Orm에 생성한다. 원인: orm-gen errors --lang php(cmd/orm-gen/errors.go)는 namespace Orm;을 쓰는데, 그것이 생성하는 clients/php/src/Code.php는 namespace Polyspec\Orm;을 선언하므로, feature-check의 feature 명령 features/helpers/error-codes가 diff -namespace Polyspec\Orm; +namespace Orm;으로 실패했다. 변경: 생성기는 namespace Polyspec\Orm;을 쓰고, contracts/features.json의 feature error-codes의 path는 Go와 Rust 출력 옆에 clients/php/src/Code.php를 포함한다. Red: error-codes의 명령이 그 diff로 실패했다. Green: error-codes의 명령이 0으로 끝난다.——[o]
G5.108conformance vector conditions_values의 대소문자 무시 이름 조건을 seed된 author 이름에 맞춘다. 원인: bench/sql/seed.*.sql은 author를 author-<i>로 이름 짓고, tests/conformance/vectors.json, vectors.postgres.json, vectors.sqlite.json은 conditions_values의 대소문자 무시 조건 lk에 service 7의 author 12개를 기대하는데, Go, PHP, Rust, TypeScript runner(tests/conformance/runner_go/main.go, runner.php, runner_typescript.mjs, clients/rust/tests/src/conformance.rs)는 어떤 seed 이름에도 없는 부분 문자열 attle-10을 넘기므로, 네 client가 모두 0을 세어 conformance-check와 feature-check의 conformance_verification 명령이 MySQL, PostgreSQL, SQLite에서 expected [1714,0,1000,700,100,12,0,0,100,10], got [1714,0,1000,700,100,0,0,0,100,10]로 실패했다. 변경: 네 runner는 대소문자와 관계없이 author-10과 Author-10에 맞는 uthor-10을 넘기고, 세 vector file은 %uthor-10%를 bind한다. 기대 개수 12와, 소문자인 seed 이름에 맞지 않는 Author-10의 대소문자 구분 조건 lb는 바꾸지 않는다. Red: make run-databases TARGETS=conformance-check가 conditions_values go:DIFF php:DIFF rust:DIFF typescript:DIFF와 conformance: 4 mismatches로 실패했다. Green: make run-databases TARGETS=conformance-check가 MySQL, PostgreSQL, SQLite에서 conditions_values go:ok php:ok rust:ok typescript:ok로 통과한다.——[o]
G5.109preempted timing case에서 schema crate를 package 이름 polyspec-orm-schema로 고른다. 원인: tests/timing/preempted.test.mjs의 case rust: the orm-schema vector tests pass while preempted는 cargo test -p orm-schema를 실행하는데 clients/rust/orm-schema의 crate는 package polyspec-orm-schema이므로, make timing-check와 feature-check의 feature 명령 features/performance_gate/preempted-timing이 "package ID specification orm-schema did not match any packages"로 실패했다. tests/cargo-test.test.mjs의 unit case는 cargo-test에 workspace의 어느 crate의 이름도 아닌 package orm과 library target orm을 준다. 변경: case는 rust: the polyspec-orm-schema vector tests pass while preempted이고 cargo test -p polyspec-orm-schema를 실행한다. cargo-test case는 package polyspec-orm과 library target polyspec_orm을 쓴다. Red: make timing-check가 그 case에서 "package ID specification orm-schema did not match any packages"로 실패했다. Green: make timing-check가 모든 case를 통과하고 node --test tests/cargo-test.test.mjs가 통과한다.——[o]
G5.110문서와 test fixture에서 Rust crate를 package 이름 polyspec-orm과 polyspec-orm-build로 부른다. 원인: Rust crate는 package polyspec-orm과 polyspec-orm-build인데, 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와 그 한국어 page, contracts/features.json과 docs/features.md의 catalog_connection 설명, contracts/interfaces.json의 설명 generated_models는 crate를 orm과 orm-build로 부르고, scripts/check/full-run.test.mjs의 cargo 오류 줄은 orm-build를 부르므로, 그 page를 보고 쓴 의존성은 없는 package를 부른다. 변경: 그 page와 설명은 polyspec-orm과 polyspec-orm-build를 부르고, docs/features.md와 docs/features.ko.md는 scripts/features/build.mjs로 다시 생성하며, cargo 오류 줄은 polyspec-orm-build를 부른다. Green: node scripts/features/build.mjs --check와 node --test scripts/check/full-run.test.mjs가 통과한다.——[o]
G5.111CI suite를 CI group마다 job 하나로 동시에 실행한다. 원인: .github/workflows/ci.yml의 job test는 CHECK_TARGETS의 모든 target을 job 하나에서 차례로 약 55분 동안 실행했고, 그 가운데 25분은 feature-check였으며 그 helper check dbspec-apply-stress 하나가 12분이었고, 모든 setup step은 target이 무엇을 필요로 하든 실행했다. 변경: Makefile은 CI group CI_GROUPS(static, clients, conformance, dbspec, features, stress)와 그 target CI_TARGETS_<group>을 CI 실행 하나에서 잰 target 시간으로 나눠 선언한다. job test는 matrix의 group마다 job 하나로 make check GROUP=<group>을 실행하고, 그것은 GitHub Actions에서만 받으며, 로컬 make check는 모든 target을 실행한다. make ci-group-needs는 group의 target이 contracts/check-inputs.json에 선언한 need를 step output으로 쓰고, 각 setup step은 scripts/check/ci-setup.mjs가 그 step에 선언한 조건에서만 실행한다. runner는 실행의 어느 target도 그 need를 선언하지 않은 건너뛴 setup step을 무시하고, 건너뛴 step이 필요한 target을 not-run으로 기록하며, 어느 target도 필요로 하지 않으면 server와 database를 시작하지 않는다. feature-check는 기능의 coverage와 검증 명령(scripts/features/check.mjs --features)을 실행하고, CHECK_TARGETS의 새 target feature-helper-check와 feature-stress-check가 helper check를 실행하며, 뒤의 것은 dbspec-stress와 dbspec-apply-stress를 실행한다. summary, report artifact와 실행 id는 group을 가진다. make repo-check는 matrix가 CI_GROUPS와 같고, group들이 CHECK_TARGETS의 모든 target을 정확히 한 번 실행하고 다른 target은 실행하지 않으며, 각 setup step의 선언한 조건, step group-needs, group의 summary와 report step을 요구한다. ruleset은 group마다 check test (<group>)를 요구한다. Red: 이전 scripts/check/run.mjs로는 CI group job의 runner case가 실패하고(건너뛴 step을 실패한 setup 단계로 기록하고, database target이 없는 실행에서 database를 만든다, 37개 가운데 3개), scripts/repo/check.test.mjs의 CI group case와 scripts/features/select.test.mjs의 선택 case는 group 규칙과 선택 module 없이 실패한다. Green: node --test scripts/check/full-run.test.mjs(37개 case), make repo-check(78개 unit case), node --test scripts/features/select.test.mjs(6개 case)와 node --test scripts/github/ruleset.test.mjs(8개 case)가 통과한다. 잰 target 시간과 약 2분의 setup, 1.3분의 database 만들기로 본 group마다의 예상 wall time: static 9분, clients 12분, conformance 12.5분, dbspec 11분, features 14.5분, stress 16분이다. dbspec-apply-stress의 case TestApplyStressPlan/mysql이 11분이고 group stress의 하한이다.——[o]
G5.112php.ini 없이 PHP를 실행하는 선언 test에서 PHP 확장이 요구하는 module을 load한다. 원인: clients/php-extension/tests/declarations_test.php는 php -n -d extension=<orm_dbspec library> declarations.php extension을 실행한다. -n은 CI runner의 PHP package의 php.ini가 load하는 shared module도 빼고, 그 가운데 pdo는 src/orm_dbspec.c가 ZEND_MOD_REQUIRED로 요구하므로, PHP는 확장을 load하지 않았고 make dbspec-php-extension-check(feature-check의 기능 명령 schema_definition/schema-php-extension)의 case php-extension/declarations와 php-extension/client가 the extension orm_dbspec is not loaded로 실패했다. 같은 library는 같은 make 실행과 directory의 다음 부분에서 -n 없이 load되었다. PHP가 startup에 적은 이유는 stdout에 갔고 실패는 그것을 보이지 않았다. library 경로는 그 실행의 make process id를 가지고, build는 부분들이 실행되기 전에 rename으로 그것을 publish하며, target의 끝만 그 directory를 지운다. 변경: test는 요구하는 module을 src/orm_dbspec.c에서 읽고, 각각을 php -n으로 확인해 없는 것을 실행 중인 PHP의 extension_dir에서 load한다. 실패한 실행은 stderr와 stdout을 함께 적는다. 새 case php-extension/required-modules는 pdo가 shared module인 PHP와 모든 module이 들어 있는 PHP의 인자, 그리고 실패한 실행이 stdout을 가지는지 확인한다. Red: 이전 test의 인자(없음)로는 case php-extension/required-modules가 a PHP without a built-in pdo gets the arguments []로 실패하고, PHP가 load하지 못하는 library에서 php-extension/declarations의 실패는 stderr만 보였다. Green: make dbspec-php-extension-check/declarations가 두 실행 모두 통과하고(각 5개 case), PHP가 load하지 못하는 library는 stdout: 뒤에 PHP startup 경고를 적고 실패한다.——[o]
G5.113CI 완료 job, tag release workflow와 tag release의 변경 이력 규칙을 더한다. 원인: main은 CI job마다 check 하나를 요구했고, tag를 release하는 workflow가 없었으며, 변경 이력은 release하지 않은 변경을 version section 아래에 두었고, go.mod를 그 directory와 비교하는 검사가 없었다. 변경: G5.113-1부터 G5.113-5까지다. Green: 각 하위 항목의 검사가 통과한다.——[o]
G5.113-1어느 tag도 release하지 않은 변경을 ## Unreleased 아래에 둔다. 원인: CHANGELOG.md와 CHANGELOG.ko.md는 모든 항목을 version section ## 0.0.2와 ## 0.0.1 아래에 두었는데, 어느 tag도 저장소의 version을 release하지 않았고, release는 tag한 version의 section에서 notes를 가져온다. 변경: 두 변경 이력은 모든 항목을 기존 항목 형식대로 맨 위의 ## Unreleased 아래에 둔다. make version-check는 두 file에서 ## Unreleased가 첫 section이자 하나뿐인 release 전 section이고, 다른 section은 모두 VERSION보다 크지 않은 version X.Y.Z이며 새 것이 먼저 오고, 두 언어의 section이 같기를 요구한다. 그래서 VERSION을 X.Y.Z로 바꾸고 ## Unreleased를 새 빈 ## Unreleased 아래의 ## X.Y.Z로 바꾸는 release PR은 통과한다. VERSION과 모든 package file은 첫 release PR이 release하는 version 0.0.2를 유지한다. AGENTS.md는 항목을 ## Unreleased 아래에 쓴다고 적는다. Red: scripts/version/check.test.mjs의 새 case는 이전 변경 이력에서 두 file 모두 the first section is ## 0.0.2로 실패한다. Green: make version-check가 통과한다(5개 case).——[o]
G5.113-2main에서 push gate와 CI 완료 check 하나를 요구한다. 원인: main의 ruleset은 ci.yml의 모든 job의 check, 곧 CI group마다 test (<group>)와 docs를 요구했으므로, group이나 job을 더하거나 이름을 바꾸면 요구하는 check가 바뀌었다. 변경: .github/workflows/ci.yml의 마지막 job ci-passed는 if: ${{ always() }}와 needs: [test, docs]로 실행되어 make ci-passed(scripts/check/ci-passed.mjs)를 실행하고, 그것은 needs의 모든 job의 결과가 success일 때만 통과하며 다른 job을 그 결과와 함께 적는다. make repo-check는 그 job이 있고, 마지막 job이며, always()로 실행되고, ci.yml의 다른 모든 job을 needs로 받고, 그 step 하나를 가지기를 요구한다. .github/ruleset.json은 정확히 check push-gate와 ci-passed를 요구하고, scripts/github/ruleset.test.mjs는 그것을 workflow에서 읽는다. AGENTS.md가 그것을 적는다. Red: scripts/repo/check.test.mjs의 ci-passed case는 이전 ci.yml에서 ci.yml has no job ci-passed로 실패하고, scripts/github/ruleset.test.mjs는 이전 선언에서 실패한다. Green: node --test scripts/check/ci-passed.test.mjs(2개 case), make repo-check(79개 unit case)와 node --test scripts/github/ruleset.test.mjs(8개 case)가 통과한다.——[o]
G5.113-3CI가 통과한 commit의 version tag에서 GitHub Release를 만든다. 원인: 저장소에 release workflow가 없어 main의 tag vX.Y.Z는 release도 asset도 만들지 않았고, tag한 commit이 CI를 통과했는지나 그 version이 manifest와 같은지 아무것도 확인하지 않았다. 변경: .github/workflows/release.yml은 push: tags: ['v*', '**/v*']에서 contents: write, 고정한 action, .github/runner의 runner, 기한 없이, tag를 TAG로 주고 실행하며, Node, Go와 Node 의존성의 setup 뒤에 make 단계 넷을 차례로 실행한다. 각 단계는 recipe가 "$$TAG"를 주는 scripts/release/release.mjs의 단계다. release-verify는 tag한 commit이 origin/main에 있고 그 check push-gate와 ci-passed의 가장 최근 run(gh api repos/<repo>/commits/<sha>/check-runs)이 성공했기를 요구한다. release-versions는 VERSION, clients/typescript/package.json, clients/php/composer.json과 polyspec-orm-schema, polyspec-orm, polyspec-orm-build의 Cargo.toml이 tag의 version을 선언하고(clients/php-extension/composer.json은 version이 없어 tag의 것을 쓴다), go.mod가 github.com/polyspec/orm을 선언하며, CHANGELOG.md와 CHANGELOG.ko.md에 그 section이 있기를 요구하고, 문제마다 file과 두 값을 적는다. release-assets는 polyspec-orm-<version>.tgz(make typescript-build 뒤의 npm pack)와 polyspec-orm-<version>.zip, polyspec-orm-dbspec-<version>.zip(package directory의 git archive)을 .runtime/release/assets에 만들고, Cargo dependency는 crate를 git과 tag로 부르므로 .crate는 만들지 않는다. release-publish는 앞의 세 단계가 성공했을 때만 실행되어 그것과 함께 gh release create <tag> --verify-tag --title <tag> --notes-file <그 section>을 실행한다. tag <dir>/vX.Y.Z는 <dir>/go.mod가 github.com/polyspec/orm/<dir>를 선언하기를 요구하고 아무것도 만들거나 붙이지 않는다. 추적된 manifest는 모두 release하거나 release하지 않는 이유와 함께 적혀 있다. make release-check(scope owner, CI group static)가 unit case를 실행하고, make repo-check는 release.yml의 trigger를 요구한다. Red: scripts/release/release.test.mjs는 scripts/release/release.mjs 없이 실패하고, case a manifest version that differs from the tag는 Cargo manifest를 읽는 함수가 [package]의 첫 줄에서 멈추는 동안 실패했으며, scripts/repo/check.test.mjs의 trigger case는 이전 규칙에서 release.yml runs on push; only ci.yml, push-gate.yml and docs-pages.yml run on these events로 실패한다. Green: make release-check(8개 case: tag 형식, asset 이름, manifest 목록, version 불일치, 빠진 변경 이력 section, 실패했거나 없는 check와 main 밖의 commit, git, gh, make, npm을 대신한 네 단계, Go module tag)와 make repo-check(79개 unit case)가 통과한다.——[o]
G5.113-4AGENTS.md에 release 절차를 적는다. 원인: AGENTS.md와 AGENTS.ko.md는 release를 적지 않아, version을 올리는 pull request, merge된 commit의 tag, release를 게시하는 workflow가 적혀 있지 않았다. 변경: AGENTS.md의 section Releases와 그 한국어 짝은 VERSION과 모든 package file을 X.Y.Z로 바꾸고 ## Unreleased를 새 빈 ## Unreleased 아래의 ## X.Y.Z로 바꾸는 pull request chore(release): Release X.Y.Z (#<체크리스트 ID>), pull request로 올리지 않는 main의 merge된 commit의 tag vX.Y.Z(또는 <dir>/vX.Y.Z), release를 게시하는 .github/workflows/release.yml의 make 단계 넷을 적는다. Red: AGENTS.md에 ## Releases가 없다. Green: make docs-rules-check가 두 언어의 section과 함께 통과한다.——[o]
G5.113-5모든 go.mod가 자기 directory의 module path를 선언하기를 요구한다. 원인: go get은 module을 그 path, 곧 저장소와 그 아래 directory로 찾고, 하위 directory의 Go module은 tag <dir>/vX.Y.Z로 release하는데, go.mod가 선언한 path와 그 directory를 비교하는 검사가 없었다. 변경: make repo-check는 추적된 모든 go.mod가 root에서는 github.com/polyspec/orm, 하위 directory에서는 github.com/polyspec/orm/<dir>를 선언하기를 요구하고, 선언한 path와 요구하는 path를 적는다. 저장소의 go.mod는 root의 하나이고 github.com/polyspec/orm을 선언한다. Red: scripts/repo/check.test.mjs의 새 case는 규칙 없이 실패하고, 규칙은 github.com/polyspec/orm/v2를 선언한 root go.mod, github.com/polyspec/orm을 선언한 packages/orm-go의 go.mod, module 줄이 없는 go.mod를 거부한다. Green: make repo-check가 통과한다(80개 unit case).——[o]
G5.114runner의 unit case를 group으로 나눈 make check recipe와 feature-unit-check의 새 부분에 맞춘다. 원인: G5.111은 check recipe의 guard 앞에 GROUP을 확인하는 make 함수 둘을 두고, $(CHECK_TARGETS) 대신 $(CHECK_RUN_TARGETS)를 실행하며, 부분 feature-unit-check/select를 더했는데, scripts/check/full-run.test.mjs의 case make check and make rerun-failed start with the guard와 a target of independent commands runs each as a part of its own는 이전 recipe와 부분을 기대했으므로 make full-run-check가 65개 case 가운데 2개에서 실패했다. 변경: 첫 case는 두 $(if $(GROUP),...) 줄, 그 뒤의 guard와 $(CHECK_RUN_TARGETS)의 runner, 그리고 GROUP 없는 make -n check가 guard를 첫 명령으로 출력하기를 요구한다. feature-unit-check의 부분은 select를 포함한다. Red: make full-run-check가 그 2개 case에서 실패한다(65개 가운데). Green: make full-run-check가 65개 case를 통과한다.——[o]
G5.115interface 검사의 생성 검사 case에서 CI의 group으로 나눈 make check를 요구한다. 원인: G5.111부터 ci.yml은 CI group마다 한 번 make check GROUP=${{ matrix.group }}을 실행하는데, tests/interfaces/check/prohibited_test.go의 TestCIRequiresGeneratedChecks는 줄 run: make check를 요구했으므로, CI group clients에서 go-test-check가 CI must run make check로 실패했다. 변경: case는 step run: make check GROUP=${{ matrix.group }}과, go-model-check와 ts-model-check를 포함한 CHECK_TARGETS의 모든 target이 CI_GROUPS의 어느 CI group의 target이기를 요구한다. 새 mutation은 group step을 빼거나 go-model-check나 ts-model-check를 그 group에서 빼고, 각각 case를 실패시킨다. Red: 이전 case는 group으로 나눈 ci.yml에서 CI must run make check로 실패한다. Green: go test ./tests/interfaces/check -run '^TestCIRequiresGeneratedChecks$'가 7개 mutation을 모두 거부하며 통과한다.——[o]
G5.116decimal-physical-check의 program을 case 앞에서 기한 없이 build한다. 원인: scripts/decimal-physical-check.mjs는 owner process, 그 가운데 cargo test ... --bin decimal_physical과 go test -tags decimalphysical, 그리고 state reader go run ./tests/conformance/check를 process마다 120초의 기한 아래에서 실행하므로, 그 process는 compile도 그 기한 아래에서 했다. CI group conformance에서는 앞의 어느 target도 Rust test를 build하지 않았고, feature-helper-check의 helper check features/helpers/decimal-physical이 decimal/mysql/rust/1 ... cargo timed out after 120000 ms로 실패했다. 실행의 server와 DECIMAL_ENV는 있었고 그 앞의 Go와 PHP case는 통과했다. 변경: decimal-physical-check의 recipe는 case 앞에서 Rust test binary decimal_physical을 Rust target directory의 lease 아래의 cargo test --no-run으로, clients/go/decimalmodel의 Go test를 go test -c로, state reader를 go build로(둘 다 $(PUBLISH)로 .runtime/bin에 publish하므로 Go build cache가 그것을 가진다), 각각 RUN_LONG으로 기한 없이 build하고, case는 build된 것을 실행한다. Red: group conformance의 CI log가 decimal/mysql/rust/1의 cargo timed out after 120000 ms를 보인다. Green: 세 build 단계가 로컬에서 통과하고(rust-build/decimal-physical 43.9초, go-build/decimal-physical 0.7초, go-build/conformance-state 0.7초) make repo-check가 통과한다.——[o]
G5.117scripts/github/ruleset.mjs의 머리말에 ruleset이 요구하는 check를 적는다. 원인: G5.113-2부터 .github/ruleset.json은 check push-gate와 ci-passed를 요구하는데, scripts/github/ruleset.mjs의 머리말은 여전히 push-gate와 ci.yml의 모든 job을 적었다. 변경: 머리말은 push-gate와, 다른 모든 job이 성공했을 때만 통과하는 ci.yml의 마지막 job ci-passed를 적는다. Red: 머리말이 ci.yml의 모든 job을 적는다. Green: node --test scripts/github/ruleset.test.mjs가 통과한다(8개 case).——[o]
G5.118group-rows와 unselected-column physical 검사의 state reader를 case 앞에서 기한 없이 build한다. 원인: scripts/group-rows-physical-check.mjs와 scripts/unselected-column-physical-check.mjs는 state reader go run ./tests/conformance/check state를 process마다 120초의 기한 아래에서 실행하므로, reader의 차가운 Go build가 그 기한 아래에서 실행되었다. G5.116이 decimal-physical-check에서 없앤 원인과 같다. 변경: Makefile 변수 BUILD_CONFORMANCE_STATE는 reader를 RUN_LONG과 $(PUBLISH)로 .runtime/bin에 기한 없이 build하고, group-rows-physical-check, unselected-column-physical-check, decimal-physical-check는 case 앞에서 그것을 실행한다. Red: group-rows-physical-check와 unselected-column-physical-check의 recipe는 build 단계 없이 case를 시작한다. Green: make -n group-rows-physical-check가 script 앞에 go-build/conformance-state를 출력하고, build가 로컬에서 통과하며(0.7초), make repo-check가 통과한다(80개 unit case).——[o]
G5.119JavaScript test의 하위 process를 case가 주는 변수만으로 실행한다. 원인: scripts/check/full-run.test.mjs의 case make check and make rerun-failed start with the guard는 make -n check를 그것을 실행한 쪽의 환경으로 실행했으므로, CI group static에서는 make check GROUP=static의 GROUP=static을 받고 case가 비운 GITHUB_ACTIONS 없이 실행되어, make가 GROUP을 읽는 check의 줄에서 GROUP runs one CI group of make check on GitHub Actions only로 멈췄다. test의 다른 하위 process 22개도 같은 환경을 받았다: full-run.test.mjs의 stub 실행은 CI job의 ORM_CHECK_RUN_ID와 GITHUB_ACTIONS를 읽었고, push-gate.test.mjs가 실행한 scripts/check/push-gate.mjs는 job의 GITHUB_STEP_SUMMARY에 덧붙였으며, push-gate.test.mjs, scripts/git/check.test.mjs, scripts/github/ruleset.test.mjs, tests/cargo-test.test.mjs의 make 실행은 상위 make의 MAKEFLAGS와 변수를 읽었다. 변경: tests/environment.mjs는 isolatedEnvironment(variables), 곧 PATH, HOME, TMPDIR과 주어진 변수의 환경을 export하고, JavaScript test의 모든 하위 process는 그것으로 실행한다. guard case는 make -n check를 GROUP 없이와 GROUP=static GITHUB_ACTIONS=true로 실행해 둘 다 guard를 먼저 출력하는지, GROUP=static만으로와 GROUP=nope GITHUB_ACTIONS=true로 실행해 둘 다 자기 message로 거부되는지 본다. tests/cargo-test.test.mjs의 cargo case는 cargo에 rust-toolchain.toml의 channel과 CARGO_NET_OFFLINE을 주고, tests/timing/preempted.test.mjs는 자기 program에 Makefile이 export하는 toolchain 변수를 준다. make repo-check가 실행하는 scripts/repo/testcases.mjs의 callerEnvironmentErrors는 .test.mjs file에서 process.env를 하위 환경에 펼치거나, env: process.env를 주거나, env: isolatedEnvironment( 없이 make를 실행하는 줄을 실패시킨다. AGENTS.md가 그 규칙을 적는다. Red: GROUP=static GITHUB_ACTIONS=true make full-run-check에서 guard case가 Makefile:171: *** GROUP runs one CI group of make check on GitHub Actions only로 실패하고, callerEnvironmentErrors는 변경 전 tree의 test file에 오류 23개를 돌려준다. Green: make full-run-check가 CI 변수 없이, 환경에 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>를 두고(summary file은 쓰이지 않는다), 그리고 command line의 GROUP=static으로 case 67개를 통과한다. make git-check/unit(7), make github-check(8), make testcase-check/node(6), make testcase-check/runners(11)가 그 변수가 있을 때와 없을 때 통과한다. make test-servers-check가 통과한다(13). preempted case rust: the polyspec-orm-schema vector tests pass while preempted와 php: dbspec_rules_test passes while preempted가 통과한다. make repo-check가 통과한다(unit case 81개).——[o]
G5.120make check에서 helper check마다 한 번 실행한다. 원인: feature-helper-check는 stress helper를 뺀 모든 helper check를 실행했고, 그 가운데 넷은 make check가 다른 곳에서 하는 일을 다시 했다: conformance-runners는 CHECK_TARGETS의 target인 make conformance-check를, conformance-result는 conformance-check의 선행 target인 make conformance-result-check를, case-database는 CHECK_TARGETS의 target인 make case-database-check를, bench-database는 setup 단계 databases/create가 scripts/check/databases.sh로 실행하는 sh scripts/bench-db.sh를 실행한다. CI group conformance에서 그 넷은 feature-helper-check의 379초 가운데 212초, 84초, 209초, 107초였다. 변경: Makefile 변수 FEATURE_SUITE_HELPERS가 그 네 helper를 적고, feature-helper-check는 FEATURE_STRESS_HELPERS와 함께 그것을 --without-helper로 뺀다. helper의 file이 바뀌면 make owner-check는 그 helper check를 그대로 실행한다. make repo-check가 실행하는 scripts/repo/ci.mjs의 helperRunErrors는 contracts/features.json의 helper마다 make check가 그 check를 실행하는 곳을 모은다: recipe가 (make 변수와 $(addprefix)를 편 뒤) node scripts/features/check.mjs --run으로 그 helper를 고르는 CHECK_TARGETS의 target, target이 make check의 target이나 그 선행 target인 helper 명령 make ... <target>, scripts/check/databases.sh가 실행하는 script의 helper 명령 sh <script>이다. 두 번 검사되거나 한 번도 검사되지 않는 helper를 그 곳을 적고 실패시킨다. AGENTS.md가 그 규칙을 적는다. Red: 변경 전 Makefile에 helperRunErrors는 bench-database, case-database, conformance-result, conformance-runners마다 두 번 실행된다는 오류 4개를 돌려준다. Green: make -n feature-helper-check가 stress helper 2개와 suite helper 4개에 --without-helper를 주고, scripts/features/select.mjs의 selectChecks는 나머지 helper 11개를 고른다. scripts/repo/check.test.mjs의 새 case make check runs the check of every helper once가 mutation(target과 feature-helper-check가 함께 실행하는 helper, setup 단계와 feature-helper-check가 함께 실행하는 helper, feature-helper-check도 실행하는 stress helper, 아무것도 실행하지 않는 helper)과 함께 통과한다. make repo-check가 통과한다(unit case 82개).——[o]
G5.1212000 table 적용의 MySQL을 자기 CI group에서 실행한다. 원인: CI group stress는 feature-stress-check를 실행했고, 그것은 helper dbspec-stress(측정한 두 CI 실행에서 56초와 64초)와 dbspec-apply-stress를 실행했다. make dbspec-apply-stress-bench는 2000 table plan을 MySQL(652초와 675초), PostgreSQL(43초와 44초), SQLite(41초와 38초)에 차례로 적용했으므로 job은 937초와 962초 걸렸다. 변경: Makefile 변수 APPLY_STRESS_DIALECTS(기본 mysql|postgres|sqlite)가 -run '^TestApplyStressPlan$$/^($(APPLY_STRESS_DIALECTS))$$'로 TestApplyStressPlan의 subtest를 고르고, recipe는 빈 값이나 database가 아닌 이름에서 명령 전에 멈춘다. 그런 값은 subtest를 하나도 고르지 않고 통과하기 때문이다. helper dbspec-apply-stress는 dbspec-apply-stress-mysql(APPLY_STRESS_DIALECTS=mysql)과 dbspec-apply-stress-pg-sqlite(APPLY_STRESS_DIALECTS='postgres|sqlite')가 되고, 둘 다 tests/dialects/apply_stress_physical_test.go를 가진다. FEATURE_STRESS_MYSQL_HELPERS와 (dbspec-stress를 담은) FEATURE_STRESS_PG_SQLITE_HELPERS가 목록 하나를 대신하고, target feature-stress-mysql-check(go와 databases가 필요하다)와 feature-stress-pg-sqlite-check가 CHECK_TARGETS, contracts/check-inputs.json에서 feature-stress-check를 대신하며, CI_GROUPS와 ci.yml의 matrix에서 CI group stress-mysql과 stress-pg-sqlite가 stress를 대신한다. scripts/repo/ci.mjs의 ciGroups는 -가 있는 group 이름을 읽는다. make bench는 그대로 세 database에 plan을 적용한다. Red: ciGroups가 CI_TARGETS_stress-mysql을 읽지 않으므로 make repo-check가 새 group을 Makefile CI group stress-mysql declares no CI_TARGETS_stress-mysql로 실패시키고, make dbspec-apply-stress-bench는 database 하나만 실행할 수 없다. Green: make dbspec-apply-stress-bench APPLY_STRESS_DIALECTS='postgres|sqlite'가 TestApplyStressPlan/postgres와 TestApplyStressPlan/sqlite만 실행하고 로컬에서 통과한다(38.0초). make -n dbspec-apply-stress-bench는 -run '^TestApplyStressPlan$/^(mysql|postgres|sqlite)$'를 실행하고, APPLY_STRESS_DIALECTS=와 APPLY_STRESS_DIALECTS='postgres|sqlit'은 자기 message로 멈춘다. make ci-group-needs GROUP=stress-mysql은 server와 그 database만 준비한다. node scripts/features/check.mjs가 manifest를 검사한다. make repo-check(82)와 make feature-unit-check(case 16, 19, 6개)가 통과한다. 측정한 실행에서 기대하는 CI 시간: stress-mysql 약 15분(MySQL 적용만), stress-pg-sqlite 약 5분.——[o]
G5.122make check의 feature-check를 feature-coverage, feature-verify-rust, feature-verify-other로 나누고 각각을 자기 CI group에서 실행한다. 원인: CI group features는 feature-check 하나를 실행했다(측정한 두 CI 실행에서 726초와 793초, job은 902초와 978초). 약 400초가 coverage였고 그 가운데 coverage test binary의 cargo build가 97초였으며, 약 380초가 lane 4개에서 실행한 검증 명령 147개였고 그 cargo build는 Rust target directory의 lease 아래에서 하나씩 실행된다. 변경: contracts/features.json의 모든 검증 명령은 shard를 선언한다. cargo로 Rust를 build하고 실행하는 명령 31개(tests/cargo-test.mjs, cargo build, 그리고 checker가 자기 Rust symbol을 cargo로 build하는 interface self-test)는 rust, 나머지 116개는 other다. scripts/features/check.mjs는 SHARDS의 shard가 없는 명령과, 같은 명령(cwd와 text)을 서로 다른 shard로 선언한 두 기능을 실패시킨다. 그 명령은 한 번 실행되기 때문이다. scripts/features/select.mjs의 --features --shard <shard>는 그 shard의 명령을 고르고, 없는 shard는 오류다. target feature-coverage(scripts/features/coverage.mjs), feature-verify-rust와 feature-verify-other(check.mjs --run --features --shard rust|other)는 각각 feature-check/build 뒤에 실행되며, CHECK_TARGETS, contracts/check-inputs.json에서 feature-check를 대신하고, 같은 이름의 CI group이 CI_GROUPS와 ci.yml의 matrix에서 features를 대신한다. make feature-check는 그 셋을 실행한다. AGENTS.md와 docs/feature-coverage.md가 그 target을 적는다. Red: CI job 하나가 coverage와 모든 검증 명령을 차례로 실행했다. Green: selectChecks가 rust에 명령 31개(서로 다른 것 30개), other에 116개(서로 다른 것 102개, Linux runner의 것 1개), 합해 147개를 고른다. node scripts/features/check.mjs가 통과하고, 명령 하나의 shard를 지우고 model_writes/styled-write-typescript의 shard를 바꾸면 dsn_connection: verification 0 declares the shard undefined; declare one of rust, other와 model_writes/styled-write-typescript: shard rust differs from shard other of model_queries/styled-query-typescript, which runs the same command로 실패한다. make -n이 새 target마다 그 명령을 출력한다. scripts/features/select.test.mjs의 새 case --features --shard runs the feature commands of that shard only와 거부되는 인자 묶음 넷이 통과한다(case 7개). make feature-unit-check(16, 19, 7), make repo-check(82), make full-run-check(67)와 TestCIRequiresGeneratedChecks가 통과한다.——[o]
G5.123cargo test binary를 쓰지 않는 coverage case를 Rust coverage build 동안 실행한다. 원인: scripts/features/coverage.mjs는 첫 case 전에 모든 native program을 build했으므로 database lane 넷이 모두 coverage test binary의 cargo build(측정한 CI 실행에서 97초)를 기다린 뒤 실행했다. 가장 긴 lane인 sqlite는 Rust가 없는 case 219초와 Rust case 76초를 실행했으므로 coverage는 약 97초 + 295초 걸렸다. 변경: buildNative는 state reader와 Go build 뒤에 돌아오고 cargo build를 buildCargo로 시작한다. 그 promise builds.cargoReady는 builds.cargo를 채우고 거부되지 않는다(실패한 build는 그것을 쓰는 case의 오류로 남는다). lane마다 cargo test binary가 없는 case를 먼저, 그다음 builds.cargoReady 뒤에 Rust case를 manifest의 순서대로 실행한다. 모든 case는 여전히 두 번의 실행마다 앞뒤 database state를 비교하므로 순서는 결과를 바꾸지 않고, 임시 directory는 build가 끝난 뒤에만 지운다. Red: scripts/features/coverage.test.mjs의 새 case coverage runs without cargo proceed while the Rust test build runs는 cargo test --no-run이 같은 lane의 PHP case가 FIFO에 쓸 때까지 기다리고 manifest에서 Rust 소유자가 먼저인 case이며, 이 변경이 없는 scripts/features/coverage.mjs에서 deadline 2m0s exceeded and the case did not stop으로 실패한다. Green: 그 case가 통과하고(1.2초), make feature-unit-check/coverage가 통과한다(case 17개). 기대하는 CI 시간: coverage가 build 시간만큼 줄어 가장 긴 lane인 약 300초가 된다.——[o]
G5.124CI group clients를 둘로 나눈다. 원인: group clients는 rust-driver-check(측정한 두 CI 실행에서 32초와 50초), example-check(15초와 41초), client-db-check(221초와 235초), client-pooler-check(180초와 189초), dialect-facts-check(4초와 5초), go-test-check(5초와 6초)를 실행했고, make check는 60초의 databases/create 뒤에 519초와 603초, job은 615초와 697초 걸렸다. 변경: CI_GROUPS와 ci.yml의 matrix에서 clients를 clients-db(rust-driver-check, example-check, client-db-check, dialect-facts-check, go-test-check, 측정 277초와 337초)와 clients-pooler(client-pooler-check)가 대신한다. 자기 job에서 client-pooler-check는 앞에서 client-db-check가 build한 Rust workspace test를 build하므로(rust-build/workspace-tests는 client-db-check에서 136초와 143초, client-pooler-check에서 17초와 19초) 약 330초를 기대한다. CI_GROUPS의 주석은 group마다 실행하는 것과 job의 10분 한도를 적는다. Red: job 하나가 client database suite 둘을 함께 실행해 615초와 697초 걸렸다. Green: make repo-check가 통과하므로(82) CHECK_TARGETS의 모든 target이 정확히 한 group에서 실행된다. make ci-group-needs가 두 group에 Node, Rust, PHP와 server를 준비한다. TestCIRequiresGeneratedChecks, make full-run-check(67)와 make github-check(8)가 통과한다. job setup 약 110초와 databases/create 60초로 기대하는 CI 시간: clients-db 약 8.5분, clients-pooler 약 8.5분.——[o]
G5.125CI group dbspec을 둘로 나눈다. 원인: group dbspec은 case-database-check와 dbspec target 17개를 실행했고, 측정한 두 CI 실행에서 60초의 databases/create 뒤에 target이 414초와 483초, job이 584초와 673초 걸렸다. 변경: CI_GROUPS와 ci.yml의 matrix에 dbspec-apply를 더한다. 그것은 apply target 여섯(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초와 248초)을 실행하고, dbspec은 case-database-check와 DDL, introspection, plan target(측정 163초와 235초)을 맡는다. apply target의 build는 자기 것이므로(Rust apply runner, 실행 directory마다의 PHP 확장, TypeScript build) 나누어도 dbspec-apply-rust-check의 Rust dbspec build 말고는 어느 group에도 build가 늘지 않는다. CI_GROUPS의 주석이 두 group을 적는다. Red: job 하나가 target 18개를 모두 실행해 584초와 673초 걸렸다. Green: make repo-check가 통과하므로(82) CHECK_TARGETS의 모든 target이 정확히 한 group에서 실행된다. TestCIRequiresGeneratedChecks, make full-run-check(67)와 make github-check(8)가 통과한다. job setup 약 110초와 databases/create 60초로 기대하는 CI 시간: dbspec 약 7분, dbspec-apply 약 7.5분.——[o]
G5.126feature-helper-check를 자기 CI group에서 실행한다. 원인: group conformance는 ts-check(측정한 두 CI 실행에서 47초와 51초), ts-min-check(50초와 54초), conformance-check(141초와 189초), feature-helper-check(379초와 353초)를 실행했고, job은 771초와 839초 걸렸다. G5.120이 자기 target에 맡긴 helper check 넷이 없어도 feature-helper-check는 rust-test-helpers(159초와 154초)와 decimal-physical(180초와 129초)을 실행하고, 그 cargo build는 Rust target directory의 lease를 차례로 가지며, 짧은 helper check 아홉(각 25초 미만)도 실행하므로 약 250초에서 310초를 기대한다. 나머지 세 target의 294초, databases/create 60초, job setup 약 110초와 더하면 10분을 넘는다. 변경: CI_GROUPS와 ci.yml의 matrix에 feature-helper-check를 실행하는 feature-helpers를 더하고, conformance는 ts-check, ts-min-check, conformance-check를 맡는다. CI_GROUPS의 주석이 그 group을 적는다. Red: job 하나가 TypeScript, conformance, helper 검사를 함께 실행해 771초와 839초 걸렸다. Green: make repo-check가 통과하므로(82) CHECK_TARGETS의 모든 target이 정확히 한 group에서 실행된다. TestCIRequiresGeneratedChecks, make full-run-check(67)와 make github-check(8)가 통과한다. 기대하는 CI 시간: conformance 약 8분, feature-helpers 약 8분.——[o]
G5.127CI group에 setup 단계 databases/create가 필요로 하는 것을 준비한다. 원인: make ci-group-needs는 group의 setup step을 그 target이 contracts/check-inputs.json에 선언한 need로만 정했다. feature-stress-mysql-check는 go와 databases를 선언하므로 group stress-mysql은 step php, php-sqlite, composer를 건너뛰었는데, databases/create(scripts/check/databases.sh)는 decimal database를 clients/php의 Composer autoload로 scripts/decimal-db-setup.php가 설치한다. CI run 37594907271에서 그것은 PHP Fatal error: Uncaught Error: Failed opening required '.../clients/php/vendor/autoload.php'로 실패했고 feature-stress-mysql-check는 not-run으로 기록되었다. 변경: scripts/check/ci-setup.mjs의 SETUP_NEEDS는 databases가 go, php, composer, server-programs를 필요로 한다고 선언하고, expandNeeds는 그 need를 target의 need에 더한다. groupOutputs와 runner(scripts/check/run.mjs)는 더한 need를 쓰므로, 그런 need의 step이 건너뛰어졌거나 실패하면 runner는 servers와 databases/create를 하지 않고 database target을 그 step과 함께 not-run으로 기록한다. make repo-check가 실행하는 groupSetupErrors는 그 target의 need나 그 need의 setup 단계가 필요로 하는 need를 설치하는 setup step을 output이 건너뛰는 CI group을 실패시킨다. AGENTS.md가 그 규칙을 적는다. Red: 이 변경 전의 groupOutputs로 groupSetupErrors는 Makefile의 CI group에 오류 3개, 곧 stress-mysql의 step php, php-sqlite, composer를 돌려준다. 새 runner case a run without the setup step that databases/create needs records the database targets as not-run은 database를 만들고 target을 실행하는 이 변경 전의 runner에서 실패한다. Green: scripts/repo/check.test.mjs의 새 case every CI group runs the setup steps of its needs and of the setup step databases/create가 CI_GROUPS의 group 12개 모두에서 통과하고, 선언한 need만 보는 대조 output은 php, php-sqlite, composer를 건너뛴다. make ci-group-needs GROUP=stress-mysql이 php=true를 출력한다. make repo-check(83), make full-run-check(68), make feature-unit-check(17, 19, 7)가 통과한다.——[o]
G5.128CI group 이름을 job마다 runner가 쓰는 보고서를 올리도록 짓는다. 원인: runner는 보고서 directory를 .runtime/check/<runName(ORM_CHECK_RUN_ID)>/report로 정하고, scripts/check/report.mjs의 runName은 영문 소문자와 숫자가 아닌 글자를 모두 _로 바꾸는데, ci.yml의 upload step은 .runtime/check/ci_<run id>_<attempt>_<matrix group>/report/를 읽는다. 그래서 G5.121부터 G5.126까지 -로 지은 group(clients-db, clients-pooler, dbspec-apply, feature-coverage, feature-verify-rust, feature-verify-other, feature-helpers, stress-mysql, stress-pg-sqlite)은 없는 directory를 올렸고, CI run 37594907271에서 그 가운데 여덟은 모든 target이 통과했는데도 job이 No files were found with the provided path: .runtime/check/ci_37594907271_1_feature-verify-other/report/로 실패했다. 변경: 아홉 group은 CI_GROUPS, CI_TARGETS_<group>, CI_GROUPS의 주석, ci.yml의 matrix와 G5.121부터 G5.126까지의 변경 이력에서 clients_db, clients_pooler, dbspec_apply, feature_coverage, feature_verify_rust, feature_verify_other, feature_helpers, stress_mysql, stress_pg_sqlite다. scripts/repo/ci.mjs의 ciGroups는 group 이름의 _를 읽는다. make repo-check가 실행하는 ciReportPathErrors는 ci.yml의 job마다 ORM_CHECK_RUN_ID와 upload path:에 run id 1, attempt 1과 CI_GROUPS의 모든 group을 넣고, path가 runName이 주는 보고서 directory가 아니면 실패시킨다. Red: 이 변경 전의 ci.yml과 Makefile에 ciReportPathErrors는 -가 있는 group마다 하나씩 오류 9개를 돌려주고, 첫째는 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: scripts/repo/check.test.mjs의 새 case every CI group uploads the report directory that the runner writes가 통과하고, Makefile에 더한 group stress-x는 실패한다. make repo-check(84), make full-run-check(68), make github-check(8)와 TestCIRequiresGeneratedChecks가 통과하고, make ci-group-needs가 group 12개마다 setup을 출력한다.——[o]
G5.129scripts/check/step.test.mjs의 process 사례가 background 자식이 sleep을 실행할 때까지 기다리게 한다. 원인: step은 sleep 37 &을 시작하고 곧바로 끝났고, fork된 자식은 exec 전까지 shell의 명령줄을 가졌다. CI run 37597456980에서는 step의 process 검사가 먼저 실행되어 보고서가 left 1 processes: 6764 sh -c sleep 37 …이 되었고, left 1 processes: <pid> sleep 37을 기대하는 사례 a passed step that leaves a process in its group fails and the process is killed가 make full-run-check에서 실패했다. sleep 38 사례도 순서가 같았다. 변경: 두 step은 pid를 쓴 뒤 자식의 ps -o args=가 sleep 으로 시작할 때까지 기다리며, 기대하는 보고서 줄은 바꾸지 않는다. Red: CI run 37597456980의 group static에서 67개 통과, 1개 실패. Green: step.test.mjs의 process 사례 다섯 개와 make full-run-check가 통과한다.——[o]
G5.1300.0.2를 release한다. 원인: VERSION과 모든 package file은 0.0.2를 선언하지만 CHANGELOG.md와 CHANGELOG.ko.md는 모든 항목을 ## Unreleased 아래에 두므로, tag v0.0.2의 release 단계 make release-versions가 실패한다. 변경: 두 변경 이력은 ## Unreleased를 새 빈 ## Unreleased 아래의 ## 0.0.2로 바꾸고, version은 0.0.2 그대로다. Red: TAG=v0.0.2 make release-versions가 CHANGELOG.md has no section ## 0.0.2와 CHANGELOG.ko.md has no section ## 0.0.2로 실패한다. Green: TAG=v0.0.2 make release-versions가 통과하고, make version-check, make release-check, make checklist-check, make docs-rules-check가 통과한다.——[o]
G5.131CI job ci-passed에서 저장소를 checkout하고 node를 설치한다. 원인: 이 job은 저장소를 checkout하지 않고 make ci-passed 한 step만 실행했으므로 make가 Makefile을 찾지 못했다. CI run 37597456980과 37598892543에서 다른 모든 job이 통과했을 때도 3초 만에 make: *** No rule to make target 'ci-passed'. Stop.으로 실패했고, scripts/repo/ci.mjs의 ciPassedErrors는 바로 그 한 step을 요구했다. 변경: job은 make ci-passed 전에 actions/checkout@v5와 .node-version의 actions/setup-node@v7을 실행하며, make ci-passed는 setup step 뒤의 모든 step처럼 if: ${{ !cancelled() }}로 실행되고, scripts/repo/ci.mjs의 CI_PASSED_STEPS가 세 step을 선언하므로 make repo-check는 그 step이 없는 ci-passed job을 실패시킨다. Red: 이 변경 전의 ci.yml에서 사례 the job ci-passed runs last under always() and needs every other job이 ci.yml job ci-passed has the steps - name: every job passed | … instead of - uses: actions/checkout@v5 || …로 실패한다. Green: 사례가 통과하고, checkout이 없는 최소 job은 거부되며, 새 clone에서 make ci-passed는 성공한 job 두 개에 0으로 끝나고 실패한 job 하나에 실패한다.——[o]
G5.132release notes를 GitHub 본문 한도 안에 둔다. 원인: make release-publish는 version의 CHANGELOG.md section 전체를 release 본문으로 넘기고, GitHub는 125000자를 넘는 본문을 거부한다. v0.0.2의 release run 37603500083에서(173807자의 section) gh release create가 body is too long (maximum is 125000 characters)로 실패했다. 변경: scripts/release/release.mjs의 releaseNotes는 section이 NOTES_LIMIT(125000)자 이하면 그대로 두고, 넘으면 The changes of X.Y.Z are listed in [CHANGELOG.md](https://github.com/polyspec/orm/blob/<tag>/CHANGELOG.md#XYZ).를 돌려주며, tag는 path segment마다 URL로 encode하고 anchor는 heading ## X.Y.Z의 것이다. AGENTS.md와 AGENTS.ko.md가 이 규칙을 적는다. Red: scripts/release/release.test.mjs가 does not provide an export named 'NOTES_LIMIT'로 load에 실패한다. Green: 사례 the notes are the changelog section up to the GitHub body limit and otherwise one line that links the section이 통과한다: 정확히 125000자인 section은 그대로 남고, 한 글자 더 긴 section은 v0.0.2와 Go module tag에서 맞는 URL의 한 줄 notes가 되며, make release-publish는 그 줄을 notes.md에 쓴다.——[o]
G5.1330.0.3을 release한다. 원인: v0.0.2의 release가 make release-publish에서 실패했고(G5.132) 있는 tag는 옮기지 않으므로, 수정은 다음 version으로 release한다. 변경: VERSION, Rust manifest 9개와 그 =0.0.3 path dependency, Cargo.lock 3개의 orm package 항목, clients/php/composer.json과 그 composer.lock의 content hash, TypeScript package와 lockfile root, contracts/features.json의 contract_version, PHP extension의 ORM_DBSPEC_VERSION, README, SECURITY, AGENTS, plan 쌍과 PHP README가 0.0.3을 선언하고, 두 변경 이력은 ## Unreleased를 새 빈 ## Unreleased 아래의 ## 0.0.3으로 바꾼다. Red: TAG=v0.0.3 make release-versions가 VERSION declares 0.0.2, the tag declares 0.0.3과 두 변경 이력에 없는 section으로 실패한다. Green: TAG=v0.0.3 make release-versions가 통과하고, make version-check, make release-check, make checklist-check, make docs-rules-check가 통과한다.——[o]
G5.134네 client가 ordered-json 0.0.2를 그 release에서 받는다. 원인: clients/typescript/package.json은 github:polyspec/ordered-json#v0.0.1을 적었고, npm은 clients/typescript/package-lock.json에서 이를 commit을 가진 git+ssh://git@github.com/polyspec/ordered-json.git URL로 풀었으므로, git을 끄거나 GitHub에 SSH로 접근할 수 없는 npm ci가 EALLOWGIT Fetching packages of type "git" have been disabled로 실패했다. 0.1까지 polyspec package의 dependency 규칙은 다른 polyspec package를 GitHub tag와 release archive로 받는다. 변경: clients/typescript/package.json은 https://github.com/polyspec/ordered-json/releases/download/v0.0.2/polyspec-ordered-json-0.0.2.tgz를 적고, npm이 lockfile 항목을 그 resolved와 integrity로 다시 만들었다. go.mod는 github.com/polyspec/ordered-json/go v0.0.2를 요구하고(go.sum은 go mod tidy), clients/rust/orm/Cargo.toml은 tag v0.0.2의 =0.0.2를 받으며(clients/rust/Cargo.lock과 bench/rust/Cargo.lock은 cargo update -p polyspec-ordered-json), clients/php/composer.json은 0.0.2를 요구한다(composer.lock은 composer update polyspec/ordered-json). ordered-json은 v0.0.1과 v0.0.2 사이에 version, 변경 이력, release 도구만 바꾸었다. make repo-check가 실행하는 scripts/repo/node.mjs의 npmGitSourceErrors는 dependency spec이 git+, git:, ssh:, github:로 시작하거나 GitHub shorthand owner/repo#ref인 추적된 package.json과, resolved가 git+, git:, ssh:, github:로 시작하는 추적된 package-lock.json을 실패시킨다. docs/codec.md와 docs/codec.ko.md가 각 client의 version과 출처를 적는다. Red: 이 변경 전의 package.json과 lockfile에서 node scripts/repo/check.mjs가 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와 clients/typescript/package.json dependencies @polyspec/ordered-json is github:polyspec/ordered-json#v0.0.1, a git source; …로 실패하고, 그 lockfile의 npm ci --allow-git=none이 EALLOWGIT로 실패한다. Green: make repo-check가 통과하고(87개 사례, release archive, git spec, git resolved URL에 대한 scripts/repo/check.test.mjs의 새 사례 세 개 포함), clients/typescript의 npm ci --allow-git=none이 빈 npm cache로 @polyspec/ordered-json 0.0.2를 설치하며, ordered-json 시험이 통과한다: TypeScript codec-vector.mjs와 styled-value.mjs, SQLite에서 Go go test ./clients/go/orm의 codec, styled, JSON 시험, Rust polyspec-orm의 codec, value, model 시험과 model_output, PHP styled_value_test.php와 runtime_model_test.php.——[o]
G5.135TypeScript conformance runner는 OrderedJSON을 package의 진입점으로 import한다. 원인: tests/conformance/result_typescript.mjs와 result_typescript.test.mjs는 저장소 tree의 경로인 clients/typescript/node_modules/@polyspec/ordered-json/js/index.js를 import했다. OrderedJSON 0.0.2의 release archive는 package 자체를 담고 그 진입점은 package root의 index.js이므로, CI run 37606293644에서 conformance-result-check/typescript와 feature-coverage의 TypeScript 실행이 .../@polyspec/ordered-json/js/index.js에 대한 ERR_MODULE_NOT_FOUND로 실패했다. 변경: 두 파일은 package exports의 import 진입점인 .../@polyspec/ordered-json/index.js를 import한다. Red: node --test tests/conformance/result_typescript.test.mjs가 js/index.js 경로에 대한 ERR_MODULE_NOT_FOUND로 실패한다. Green: make conformance-result-check/typescript가 5개 사례를 통과한다.——[o]
G5.136release asset의 polyspec dependency를 정확한 version으로 선언한다. 원인: polyspec는 0.1까지 registry에 publish하지 않으므로 project는 내려받은 release asset을 함께 설치하고, asset은 이름과 정확한 version으로 서로를 찾는다. make release-assets의 npm tarball은 @polyspec/ordered-json을 그 release tarball의 URL로 적었고, Composer zip polyspec-orm-<version>.zip은 Composer가 root package에서만 읽는 clients/php/composer.json의 vcs repository를 담았으며, polyspec-orm-dbspec-<version>.zip에는 artifact repository가 필요로 하는 version이 없었다. packed manifest를 확인하는 것은 없었다. 변경: scripts/release/release.mjs의 buildAssets는 npm package를 packedPackage의 package.json으로 pack하고(그것은 @polyspec/* dependency의 release tarball URL을 그 tag의 version으로 바꾼다), Composer directory마다 packedComposer의 composer.json으로 archive한다(그것은 repositories를 빼고 version을 release의 version으로 둔다). 저장소 tree의 manifest는 그대로다. 각 asset에서 packed manifest를 다시 읽고, 정확한 version이 아닌 @polyspec/*나 polyspec/* dependency(URL, file:, link:, workspace:, git source, @dev, dev-*, 범위), Composer의 repositories, release의 version과 다른 Composer version이 있으면 packedManifestErrors로 release를 거부한다. make package-check가 실행하는 scripts/release/install-check.mjs는 HEAD의 asset을 buildAssets로 만들어 저장소 밖의 임시 directory에 설치한다: npm은 모든 tarball을 file: dependency로 두고 @polyspec registry를 닿지 않는 주소로 두며, Composer는 zip의 artifact repository를 쓴다. docs/packaging.md와 docs/packaging.ko.md가 이 설치를 설명한다. Red: v0.0.3의 release asset에 packedManifestErrors를 실행하면 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, polyspec-orm-dbspec-0.0.3.zip declares the version (none)를 보고한다. Green: make release-check가 scripts/release/release.test.mjs의 새 case 두 개와 함께 통과하고, node scripts/release/install-check.mjs가 npm으로 @polyspec/orm과 @polyspec/ordered-json 0.0.2를, Composer로 polyspec/orm과 polyspec/ordered-json 0.0.2를 설치하고 둘을 load한다.——[o]
G5.1370.0.4를 release한다. 원인: v0.0.3의 release asset은 npm tarball에서 ordered-json을 URL로 선언하고, polyspec/orm zip에 vcs repository를 담으며, polyspec/orm-dbspec zip에 version이 없고(G5.136), 있는 tag는 옮기지 않으므로, 수정은 다음 version으로 release한다. 변경: VERSION, Rust manifest 9개와 그 =0.0.4 path dependency, Cargo.lock 3개의 orm package 항목, clients/php/composer.json과 그 composer.lock의 content hash(composer update --lock), TypeScript package와 lockfile root, contracts/features.json의 contract_version, PHP extension의 ORM_DBSPEC_VERSION, README, SECURITY, AGENTS, plan 쌍과 PHP README가 0.0.4를 선언하고, 두 변경 이력은 ## Unreleased를 새 빈 ## Unreleased 아래의 ## 0.0.4로 바꾼다. Red: TAG=v0.0.4 make release-versions가 VERSION declares 0.0.3, the tag declares 0.0.4와 두 변경 이력에 없는 section으로 실패한다. Green: TAG=v0.0.4 make release-versions가 통과하고, make version-check, make release-check, make checklist-check, make docs-rules-check가 통과한다.——[o]
G5.138tree의 manifest를 그대로 release한다. 원인: buildAssets가 packed 사본을 바꿨으므로(packedPackage는 @polyspec/ordered-json의 release tarball URL을 0.0.2로, packedComposer는 repositories를 빼고 version을 더했다) release한 package가 tree의 manifest와 달랐다. tree의 clients/typescript/package.json은 URL을 적었고, clients/php/composer.json은 vcs repository를 담았으며, clients/php-extension/composer.json에는 version이 없었다. 변경: scripts/release/release.mjs의 buildAssets는 manifest를 그대로 pack하고, packed manifest가 commit의 원본과 다르거나 manifestErrors(정확한 version이 아닌 polyspec dependency, Composer repositories, release의 version과 다른 Composer version)를 어기면 거부한다. scripts/release/release.test.mjs의 case가 tree의 release하는 manifest에 manifestErrors를 실행하고, make release-versions는 release하는 모든 composer.json에 version을 요구한다. clients/typescript/package.json은 @polyspec/ordered-json 0.0.2를 요구한다. private root package.json은 workspaces에 clients/typescript를 두고, overrides로 release v0.0.2의 tarball에서 ordered-json을 받으며, typescript와 @types/node를 뺀다. .npmrc는 install-strategy=nested로 workspace dependency를 clients/typescript/node_modules에 두고, root package-lock.json이 이전 version을 가진 유일한 lockfile이며, clients/typescript/package-lock.json은 지운다. clients/php/composer.json에는 repositories가 없고, clients/php-extension/composer.json은 0.0.4를 선언하며, private root composer.json은 clients/php를 path repository에서, ordered-json을 release v0.0.2의 zip을 담은 package repository에서 vendor-php에 설치한다(root의 vendor는 Go module의 vendor directory다). composer.lock은 root로 옮기고, PHP test는 vendor-php/autoload.php를 load하고 vendor-php/bin/orm-gen을 실행하며, make install은 root에서 npm ci와 composer install을 실행하고, scripts/check/downloads.mjs는 root lockfile과 vendor-dir을 읽으며, scripts/version/check.mjs는 root package-lock.json의 workspace 항목과 clients/php-extension/composer.json을 읽는다. scripts/release/install-check.mjs는 빈 npm cache, COMPOSER_HOME, Composer cache로 asset을 offline 설치한다: npm은 orm tarball과 설치된 dependency에서 pack한 tarball로, Composer는 zip의 artifact repository와 lock한 package의 path repository로 설치한다. docs/packaging.md와 docs/packaging.ko.md가 설치와 개발 배치를, docs/codec.md와 docs/codec.ko.md가 ordered-json의 출처를 설명한다. Red: manifest 변경 전 node --test scripts/release/release.test.mjs가 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로 실패하고, 이전 commit의 manifest에서 node scripts/release/install-check.mjs가 polyspec-orm-0.0.4.zip declares repositories와 polyspec-orm-dbspec-0.0.4.zip declares the version (none)으로 거부한다. Green: make release-check(case 12개), make version-check, make repo-check, make client-unit-check, node scripts/check/downloads.mjs, clients/typescript/tests/package-declarations.mjs, npm_config_offline=true와 COMPOSER_DISABLE_NETWORK=1의 node scripts/release/install-check.mjs가 통과한다.——[o]
G5.139release asset을 commit한 소비자 lock으로 설치한다. 원인: scripts/release/install-check.mjs는 저장소에 설치된 node_modules에서 third-party npm package를 pack하고 Composer package를 vendor-php directory에서 주었으므로, 설치는 소비자가 내려받는 것이 아니라 로컬 설치를 썼다. 변경: tests/release-install/npm(polyspec package를 release tarball 이름의 file: dependency로 나열한 package.json과 package-lock.json)과 tests/release-install/composer(artifact repository assets와 shasum을 가진 ordered-json 0.0.2 zip의 package repository를 둔 composer.json과 composer.lock)가 소비자 project다. install-check.mjs는 HEAD의 asset을 만들고, fixture와 asset을 저장소 밖의 임시 directory에 복사하며, @polyspec/ordered-json의 tarball을 그 GitHub release에서 받고, 빈 --cache와 --@polyspec:registry=http://127.0.0.1:9/로 npm ci를, 빈 COMPOSER_HOME과 COMPOSER_CACHE_DIR로 composer install을 실행한 뒤 package를 load한다. make install-release-fixtures가 fixture lock을 쓴다(install-check.mjs lock). 이 저장소가 만드는 package의 integrity와 shasum은 commit마다 내용이 바뀌므로 lock에서 뺀다. scripts/version/check.mjs는 fixture의 orm version을 읽고, AGENTS.md와 AGENTS.ko.md는 검사가 결과가 시간에 따라 달라지는 registry 조회를 하지 않으며 lock이 고정한 download는 설치라고 적고, docs/packaging.md와 docs/packaging.ko.md가 이 검사를 설명한다. Red: 이전 commit의 설치 검사는 tarball을 clients/typescript/node_modules에서 pack하고 path repository를 vendor-php에서 준다. Green: npm_config_offline=true COMPOSER_DISABLE_NETWORK=1 node scripts/release/install-check.mjs가 통과하고(fixture에서 빈 cache로 npm ci와 composer install), make release-check, make version-check, make repo-check, make docs-rules-check가 통과한다.——[o]
G5.140interface 검사가 typescript를 workspace에서 load하고, private root의 scope를 선언한다. 원인: G5.138이 root package.json의 devDependencies에서 typescript를 뺐으므로, 저장소 root에서 typescript를 import하는 tests/interfaces/typescript.mjs가 CI run 37612782635의 conformance-check, feature-coverage, feature-verify-rust에서 ERR_MODULE_NOT_FOUND Cannot find package 'typescript'로 실패했고, feature-unit-check는 추적하는 .npmrc와 composer.json 때문에 every tracked file selects a behaviour test or declares its scope로 실패했다. 변경: tests/interfaces/typescript.mjs는 clients/typescript/package.json의 createRequire로 typescript를 load하고, contracts/check-inputs.json은 .npmrc와 composer.json을 scope suite로 선언한다. Red: CI run 37612782635가 그 오류로 실패한다. Green: node tests/interfaces/typescript.mjs . clients/typescript/src가 선언을 출력하고, make feature-unit-check, make repo-check, make release-check가 통과한다.——[o]
G5.141네 client가 ordered-json 0.0.3을 쓰고, release asset을 artifact repository만으로 설치한다. 원인: ordered-json 0.0.2 zip은 version을 선언하지 않았으므로 Composer artifact repository가 그것을 읽지 못했고, tests/release-install/composer의 설치 fixture는 ordered-json을 package repository로 주었다. ordered-json 0.0.3은 version을 선언하는 zip과 함께 release되었다. 변경: clients/typescript/package.json과 clients/php/composer.json은 0.0.3을 요구하고, go.mod는 github.com/polyspec/ordered-json/go v0.0.3을 요구하며(go.sum은 go mod tidy), clients/rust/orm/Cargo.toml은 tag v0.0.3의 =0.0.3을 받고(clients/rust/Cargo.lock과 bench/rust/Cargo.lock은 cargo update -p polyspec-ordered-json), root overrides는 release v0.0.3의 tarball을 받으며(package-lock.json은 npm install), root composer.json은 그 zip을 shasum과 함께 받는다(composer.lock은 composer update polyspec/ordered-json). Composer fixture는 artifact repository assets만 선언하고, scripts/release/install-check.mjs가 거기에 만든 zip과 그것이 요구하는 polyspec package마다 GitHub release에서 받은 zip을 넣으며, make install-release-fixtures가 두 fixture lock을 썼다. docs/spec/codec.md, docs/codec.ko.md, docs/operations/packaging.md, docs/packaging.ko.md는 ordered-json 0.0.3을 적는다. docs/checklist.ko.md의 G5.139 줄은 문서 규칙에 맞게 고쳤다. Red: artifact repository가 ordered-json 0.0.2 zip을 Package polyspec/ordered-json has no version defined로 거부한다. Green: npm_config_offline=true COMPOSER_DISABLE_NETWORK=1 node scripts/release/install-check.mjs가 빈 cache로 두 fixture를 설치하고, commit에 적은 TypeScript, PHP, Go, Rust의 ordered-json test가 통과한다.——[o]
G5.142npm workspace를 기본 hoisted 배치로 설치한다. 원인: .npmrc가 install-strategy=nested를 두어 clients/typescript의 dependency를 clients/typescript/node_modules에 두었고, 그 directory 안의 경로가 그 배치로 풀렸다. 변경: .npmrc와 contracts/check-inputs.json의 그 항목을 지우고, package-lock.json은 같은 package version으로 hoisted 배치를 기록한다. scripts/typescript/build.mjs는 typescript/bin/tsc를 clients/typescript/package.json의 createRequire로 찾고, clients/typescript/tests/package-declarations.mjs는 @polyspec/ordered-json과 @types/node를 clients/typescript/package.json에서 findPackageJSON으로, tsc를 createRequire로 찾으며, tests/conformance/result_typescript.mjs와 그 test는 @polyspec/ordered-json을 package specifier로 import하고, contracts/features.json의 decimal-typescript-types 명령은 npm exec --workspace clients/typescript -- tsc -p tsconfig.json --noEmit을 실행한다. docs/packaging.md와 docs/packaging.ko.md는 .npmrc를 적지 않는다. Red: hoisted 배치에서 node scripts/typescript/build.mjs가 clients/typescript/node_modules/typescript/bin/tsc is missing으로 끝나고, node --test tests/conformance/result_typescript.test.mjs가 clients/typescript/node_modules/@polyspec/ordered-json/index.js의 ERR_MODULE_NOT_FOUND로 실패하며, decimal-typescript-types 명령이 FAIL typescript-types로 끝난다. Green: build가 0으로 끝나고, result test가 5개 중 5개 통과하며, 명령이 PASS typescript-types로 끝나고, node clients/typescript/tests/package-declarations.mjs, make repo-check, node scripts/features/build.mjs --check가 통과한다.——[o]
G5.143wall clock이 뒤로 가지 않는 동안 TypeScript client clock이 뒤로 가지 않게 한다. 원인: clients/typescript/src/clock.ts의 wallMicros는 monotonic clock에 anchor를 더하고, 읽은 값이 Date.now()에서 1밀리초보다 멀어지면 anchor를 밀리초 단위의 Date.now()로 옮겼다. monotonic clock이 wall clock보다 빠르면 wall 밀리초보다 1.9 ms 뒤인 값 다음에 다음 밀리초 시작의 값, 곧 0.9 ms 이른 값이 나왔다. CI run 37640344084에서 insert 직후 읽은 SQLite now 조건이 저장된 created_ts보다 일러 clock_now_condition/sqlite가 row 11 with created_ts <= now: 0 rows로 실패했고, 다음 statement의 secondsLater(0)은 맞았다. Go, PHP, Rust는 docs/protocol.md가 요구하는 대로 system의 wall clock을 읽는다. 변경: wallMicros는 Date.now()가 알리는 밀리초 밖의 값을 그 밀리초의 가까운 끝으로 옮기고 anchor도 함께 옮긴다. 그래서 모든 값이 wall clock의 밀리초 안에 있고, wall clock이 뒤로 가지 않는 동안 뒤로 가지 않는다. clients/typescript/tests/clock.mjs의 clock_wall_micros_order case는 database 없이 자기가 정한 wall clock과 monotonic clock으로 실행되고, contracts/features.json은 clock.ts를 clock-typescript의 input으로 선언한다. Red: 변경 전 case가 reading 1791472402032900 at wall 1791472402031 and monotonic 5001.9 lies within the millisecond of the wall clock와 reading 1791472402032000 is not earlier than the previous reading 1791472402032900로 실패한다. Green: case가 통과하고, clock-typescript, clock-php, clock-rust(test 9개), writes-orm-go가 SQLite, MySQL, PostgreSQL에서 통과한다.——[o]
T43Python client를 더한다. 원인: 이 저장소는 Go, PHP, Rust, TypeScript client를 내놓는다; Python program도 model DSL, codec, dbspec 표면, MySQL·PostgreSQL·SQLite driver를 자기 client library 하나로 쓴다. 완료 기준: T43.1부터 T43.6까지 끝난다; 공유 codec vector와 세 database의 conformance vector가 Python에서 통과하고, client 언어를 나열하는 모든 선언이 python을 나열한다. clients/python/pyproject.toml의 polyspec-ordered-json 의존은 ordered-json 저장소의 tag v0.0.4를 가리키고 그 tag가 생기면 활성화된다; 그 전까지 Python test는 ../ordered-json/python/src의 형제 checkout에서 package를 import한다. 이력: 커밋 a6183322와 feaf90ba는 본문이 비어 있다. T43.4는 a6183322의 대기에서 d988a0f0의 완료로 바뀌었고 그 사이에 상태 커밋이 없으며, 구현은 a6183322, 9ed5dddc, f8d2981d에 있다. 재시도: 하위 항목이 모두 완료되거나 우회되고, python이 든 ordered-json tag가 있어 T43.6이 CI에서 실행된다. 증거(로컬): 모든 child가 끝났다. 데이터베이스 row는 push 뒤 CI job이 확인하고, 거기서 실패하면 새 행이 된다.——[o]
T43.1Python package와 생성기를 갖춘다. 원인: Python client는 clients/python에 있고 PEP 420 namespace package polyspec.orm, 배포 이름 polyspec-orm, requires-python >= 3.11, py.typed 표시, unittest 단위 test, dbspec 문서를 읽어 clients/python/src/polyspec/orm/models/의 model module을 쓰는 model 생성기 orm-gen를 가진다. 완료 기준: orm-gen gen --schema <document.dbs> --out <directory>가 dbspec document set에서 model module을 쓰고, --check가 없거나 다른 file을 보고하고, Python 단위 test가 통과한다.—증거: polyspec.orm.dbspec가 dbspec 문서를 parse·검사·출력하고(parse_dbspec, emit_dbspec, dbspec_manifest, read_dbspec_file), polyspec.orm.engine가 document set의 runtime model을 만들며(model_of_documents, parse_document_set, model_of_manifest), polyspec.orm.names가 chain 문법을 해석하고(parse_chain, parse_order, split_pair, op_allowed), clients/python/bin/orm-gen gen --schema schema/bench.dbs --out <dir>가 TypeScript 생성과 같은 manifest hash의 models.py에 12 model을 쓴다; --check는 없는 file을 exit 1로 보고하고 생성된 file에서 통과한다; scan이 스캔한 source가 model에 부르는 method 이름을 검사한다(Author.not_a_model_method가 생성을 실패하게 한다). python clients/python/tests/dbspec_cases.py가 공유 dbspec 사례(canonical 10, normalize 10, invalid 100, hashes 5, sets 7, files 4)를 통과하고 python -m unittest discover -s clients/python/tests -p 'test_*.py'가 26 사례를 통과한다. chain의 질의 실행(get, gets, transaction)은 T43.3이고, 고정 column method와 chain 조건이 T43.3이 실행할 query 상태를 만든다. Red: package가 없을 때 orm-gen이 ModuleNotFoundError: No module named 'polyspec.orm'로 끝났고, chain 해석이 Or 접두 검사에서 두 글자를 읽어 or_gt_read_count를 CONFIG: GtReadCount is not a column of author로 거부했다. Green: 두 명령이 통과한다.[o]
T43.2값 함수와 codec을 구현한다. 원인: query는 orm의 값 함수로 clock과 날짜 표현을 나타내고, styled column은 codec 단계로 저장 cell을 decode하고 encode한다. 완료 기준: 값 함수와 column 함수가 다른 client와 같은 IR을 만든다; encode, decode, hostEncode, hostDecode, blindIndex, CodecError가 docs/spec/codec.md를 따른다; tests/codec/vectors.json의 공유 codec vector와 AES vector가 통과한다.—증거: polyspec.orm.values, polyspec.orm.styled_value, polyspec.orm.codec, polyspec.orm.aes가 TypeScript client의 표면을 구현한다(orm.now()부터 orm.months_later()까지, orm.day_of_week()부터 orm.date()까지, gz·base64·serialize·yaml·ordered_json 단계, aes·hex·ip host style, PHP serialize decoder, codec 값 모델 뒤의 YAML 1.2 core 해석, AES-256-GCM 저장 bytes ORM-AES2\\0); python clients/python/tests/codec_vector.py가 codec vector 80개(json 16, serialize 16, serialize+base64 16, serialize+gz 16, yaml 16)를 전부 decode하고, deterministic 30개를 byte 단위로 다시 인코딩하며, 모든 vector의 round trip과 14개 decode·encode 거절, AES 고정 vector를 확인하고, python -m unittest discover -s clients/python/tests -p 'test_*.py'가 20 사례를 통과한다. Red: polyspec.orm.codec가 없을 때 vector 실행이 ModuleNotFoundError로 실패했다. Green: 두 명령이 통과한다.[o]
T43.3Db와 model chain을 구현한다. 원인: Python program은 scheme이 database를 고르는 DSN URI 하나로 연결하고, 생성된 model chain으로 query한다. 완료 기준: Db.connect와 Db.connectSchema가 mysql, postgres, sqlite scheme을 받는다; abstract model chain이 다른 client와 같은 request를 만든다; transaction이 isolation, readOnly, timeoutMs, retry, audit을 받는다.—증거: polyspec.orm.database가 DSN URI 하나로 SQLite, MySQL, PostgreSQL 연결에서 plan된 statement를 실행하고(parse_dsn은 모르는 parameter, UTC가 아닌 timezone, sqlite _txlock을 거부한다), Db.transaction이 isolation, readOnly, timeoutMs, retry, audit을 받아 transaction에 번호를 매기고 겹친 호출을 savepoint로 만들며, polyspec.orm.events가 모든 statement의 statement event(sql, binds, kind, tables, elapsed, transaction, error)를 모든 subscriber에게 publish한다; model chain이 get, gets, getCount, getSum, getAvg, group의 getsCount, getsPage, create, creates, update(true), save, delete(true), restore, getQuery를 실행하고, join이 column을 조립하며, relation이 step을 읽고, 자기 connection을 가진 relation이 main row 뒤에 붙고, audit 대상 table의 쓰기가 transaction의 audit 기록을 bind하며, forUpdate가 transaction 안에서 SQLite row lock을 잡는다. clients/python/.venv/bin/python -m unittest discover -s clients/python/tests -p 'test_*.py'가 clients/python/tests/test_sqlite.py의 SQLite end-to-end test(write cycle, group과 page, duplication·save를 곁든 creates, transaction rollback과 그 event, recursive delete를 곁든 relation, 자기 connection의 relation, restore, row lock, transaction의 audit 기록)를 포함해 39 사례를 통과한다. Red: 첫 실행에서 snake가 getServiceMemberModels의 Pascal 단어를 나누지 않아 AttributeError: 'NoneType' object has no attribute 'length'로 실패했고, creates, restore, statement event가 없었다. Green: 명령이 통과한다.[o]
T43.4dbspec 표면을 구현한다. 원인: schema 문서를 읽고, 그리고, 비교하고, 계획하는 일이 library를 부르는 process 안에서 실행된다. 완료 기준: parseDbspec, emitDbspec, renderDbspec, introspectDbspec, diffPlan과 다른 client의 dbspec 표면 나머지가 Python에 있고 tests/dbspec의 공유 사례에서 일치한다.—증거: polyspec.orm.dbspec이 parse_dbspec, emit_dbspec, render_dbspec을 export한다; polyspec.orm.dbspec.introspect가 introspect_dbspec을, polyspec.orm.dbspec.plan_diff가 diff_plan을, polyspec.orm.dbspec.apply가 apply_plans와 finalize_plans를, polyspec.orm.dbspec.mermaid가 export_mermaid와 import_mermaid를, polyspec.orm.dbspec.compare가 compare_schemas를 export한다. python clients/python/tests/dbspec_cases.py가 tests/dbspec/cases.json의 canonical, normalize, invalid, hashes, sets, files 사례(각 10, 10, 100, 5, 7, 4건)를 돌리고 통과한다. dbspec_ddl.py는 6건 131 문장, dbspec_plans.py는 50건, dbspec_introspect.py는 sqlite 3건, dbspec_apply.py는 sqlite 4 시나리오, dbspec_mermaid.py는 13건이 일치한다. Red: polyspec.orm.dbspec.mermaid를 import하면 _TRIM_SPACE 정규식에서 re.PatternError: missing ), unterminated subpattern으로 실패했고, Mermaid driver는 진단을 메시지 필드까지 비교하고 round trip을 옮겨지지 않은 객체 목록의 텍스트와 비교했다. Green: 정규식 group을 닫았고, driver는 [rule, line, column]과 옮겨지지 않은 객체, table·column·key 골격을 TypeScript driver와 같은 방식으로 비교하며, 위의 명령이 모두 통과한다.[o]
T43.5모든 언어 선언에 python을 등록한다. 원인: 저장소의 언어 선언, 검사기, CI가 client 언어를 나열하고 python이 그 하나로 들어간다. 완료 기준: contracts/symbols/python.json, contracts/validate.go와 contracts/interfaces.json의 언어 표, conformance와 dbspec 비교의 python runner, contracts/features.json의 feature, version 선언, check input, CI group이 python을 담는다; 관련 검사가 모두 통과한다. 재시도: T43.5-5가 완료된다(arm64 기기나 CI에서 php 확장이 로드되고 make dbspec-compare-check가 통과한다). 증거(로컬): 모든 child가 끝났다. 데이터베이스 row는 push 뒤 CI job이 확인하고, 거기서 실패하면 새 행이 된다.——[o]
T43.5-1Python client를 dbspec 비교에 넣는다. 원인: tests/dbspec/compare/runners.mjs는 각 client를 공유 dbspec vector에 실행하고 모든 출력이 Go 출력과 같아야 하며, Python runner는 아직 없다. 완료 기준: tests/dbspec/compare/python.py가 공유 case, stress 문서, ddl, plan, Mermaid vector에 대해 tests/dbspec/compare/check.mjs의 줄 형식을 출력하고, 출력이 Go runner 출력과 byte 단위로 같으며, TypeScript runner와 같은 잘못된 input을 거부하고, runners.mjs가 python으로 등록한다. Red: runner가 없을 때 python3 tests/dbspec/compare/python.py ...는 can't open file로 종료 코드 2를 냈다. Green: 공유 case와 20 table stress 문서에서 runner 출력이 Go runner 출력과 byte 단위로 같다(3385줄, cmp가 차이를 내지 않음). ORM_PYTHON=clients/python/.venv/bin/python node --test --test-name-pattern=python tests/dbspec/compare/runners.test.mjs가 21개 test를 통과하며, 변형된 모든 input의 거부를 포함한다.——[o]
T43.5-1-1Python interpreter 준비를 검사 밖으로 뺀다. 원인: dbspec-compare-check의 준비 단계가 python3 -m venv와 pip install로 Python client 의존성을 내려받았으나, 검사는 내려받지 않으며 내려받기는 make install의 일이다. 완료 기준: make install이 고정 버전 의존성을 담은 clients/python/.venv를 만들고, dbspec-compare-check의 준비 단계는 clients/python/.venv/bin/python의 존재만 확인하며 없으면 run make install로 실패한다. Red: make -n dbspec-compare-check/prepare가 python3 -m venv와 pip install을 나열했다. Green: 준비 단계는 pip install을 나열하지 않는다. make python-venv-check는 venv가 있으면 0으로, PYTHON_VENV=/nonexistent/venv이면 2로 끝나며 run make install을 알린다.——[o]
T43.5-2Python client를 conformance 비교에 넣는다. 원인: conformance checker는 Go, PHP, Rust, TypeScript runner를 bench database에서 실행해 결과를 비교하며, Python runner는 아직 없다. 완료 기준: tests/conformance/runner_python.py가 tests/conformance/vectors*.json의 모든 vector를 Go runner와 같은 출력 형태로 실행하고, checker가 python으로 등록하며, conformance result check가 통과한다.—근거: T43.5-2-1부터 T43.5-2-4가 닫혔다. Python conformance 런너가 SQLite bench에서 0을 반환하고 벡터 26개가 Go 출력과 바이트 단위로 같으며(T43.5-2-2), make conformance-result-check/python이 0을 반환하고, python 검사기가 등록돼 있다(T43.5-2-4). MySQL과 PostgreSQL 실행은 CI 전용(T43.6)이며 여기서 주장하지 않는다.[o]
T43.5-2-1MySQL과 PostgreSQL server 없이 SQLite bench database를 시딩한다. 원인: scripts/bench-db.sh는 BENCH_MYSQL_DSN, BENCH_POSTGRES_DSN, BENCH_SQLITE_DSN을 모두 요구하고 세 database를 Go model로 시딩하므로, 로컬 SQLite 실행에는 시딩된 파일이 없다. 완료 기준: 다른 database와 같은 row로 SQLite bench 파일을 시딩하는 명령이 문서화되거나, 이 행이 SQLite vector는 CI에서만 실행된다고 적는다. Red: env -u BENCH_MYSQL_DSN -u BENCH_POSTGRES_DSN BENCH_SQLITE_DSN=sqlite://<dir>/bench.sqlite sh scripts/bench-db.sh sqlite가 BENCH_MYSQL_DSN is required로 종료 코드 1을 냈다. Green: 같은 명령이 종료 코드 0으로 끝나고 file에 schema/bench.dbs의 table을 쓴다(author 100000행, service 101행). 알 수 없는 방언은 종료 코드 2, SQLite DSN이 없으면 1로 끝난다. MySQL과 PostgreSQL 시딩은 방언 인수로 계속 쓸 수 있고, 인수가 없는 실행은 scripts/check/databases.sh가 부르는 대로 세 DB를 모두 시딩한다.——[o]
T43.5-2-2conformance runner용 Python bench model을 생성한다. 원인: runner는 실행 중에 schema/bench.dbs의 model이 필요하고, clients/python/bin/orm-gen gen은 model을 자기 directory에 쓴다. 완료 기준: Makefile 단계가 conformance-check의 실행 directory에 model을 쓰고, runner가 그곳에서 import한다. 우회: make 대상과 model 생성은 커밋했고(300897e2, make conformance-python-models가 종료 코드 0, model 12개 import 확인), runner의 import는 아직 쓰지 않았다. 원인: 조정자가 감사 항목 T43.5-5부터 T43.5-9를 먼저 닫으라고 순서를 바꿨다. 재시도: T43.5-9가 완료 상태가 된 뒤 runner에서 model을 import한다.—근거: make conformance-python-models가 0을 반환하고 models.py를 .runtime/run/conformance-python-models에 쓴다. tests/conformance/runner_python.py의 load_models는 --models로 받은 디렉터리(conformance 검사에서는 ORM_PYTHON_MODELS 값, tests/conformance/check/main.go가 요구함)에서 그 model을 import한다. scripts/bench-db.sh sqlite로 seed한 SQLite bench DB에서 런너가 0을 반환하고, 벡터 26개의 65,854바이트 출력이 저장된 Go 출력(go-full.json)과 바이트 단위로 같다. 막힌 상태에 적힌 원인(런너가 아직 model을 import하지 않는다)은 더 이상 맞지 않는다.[o]
T43.5-2-326개 vector를 Python runner로 옮긴다. 원인: tests/conformance/vectors.json의 26개 vector 체인은 tests/conformance/runner_typescript.mjs가 손으로 쓰며, Python runner는 runner_go의 출력 형태를 내야 한다. 완료 기준: tests/conformance/runner_python.py가 database expectation file마다 모든 vector를 실행하고, 출력이 같은 database의 Go runner 출력과 같으며, vector가 쓰는 모든 동작이 Python client에 있다.—증거: Python runner는 tests/conformance/runner_python.py다. 새로 시딩한 SQLite bench database(scripts/bench-db.sh sqlite)에서 python tests/conformance/runner_python.py --models .runtime/run/conformance-python-models --dsn sqlite://<file>는 26개 vector를 한 프로세스로 실행하고 종료 코드 0으로 끝나며, 출력은 go run ./tests/conformance/runner_go -dsn sqlite://<file>의 출력과 바이트 단위로 같다(65,854바이트, cmp 차이 없음). 하위 항목 T43.5-2-3-1부터 T43.5-2-3-9까지 각각 Red와 Green 증거를 갖는다. MySQL과 PostgreSQL vector는 CI에서만 실행되며, 검사기 등록은 T43.5-2-4다.[o]
T43.5-2-3-1Python client에 db.utils()를 둔다. 원인: vector aes_status와 transactions는 utils().lock, utils().set_local, utils().local, utils().aes().status를 부르는데, Python client에는 이 중 어느 것도 없다. 완료 기준: 네 호출이 Go client와 같은 동작을 하고, aes_status와 transactions vector가 Go 출력과 같다. Red: 외부 PYTHONPATH가 없고 새 TMPDIR인 환경에서 python -m unittest test_utils가 종료 코드 1로 끝났고 5개 오류가 났다. 각 오류는 AttributeError: 'Db' object has no attribute 'utils'다. Green: 같은 실행이 종료 코드 0으로 끝나고 5개 test가 OK이며 TMPDIR에 항목을 0개 남긴다. vector aes_status와 transactions는 새로 시딩한 SQLite bench database에서 한 프로세스로 실행했다. aes_status는 Go 출력과 같다. transactions는 결과가 같고, statement의 차이는 orm__row_lock table 생성뿐이다. 이 생성은 프로세스의 첫 transaction에 나타나며, 전체 실행에서는 Go 실행과 같은 위치에 놓인다. MySQL GET_LOCK, PostgreSQL pg_advisory_xact_lock과 set_config은 CI에서만 실행된다.——[o]
T43.5-2-3-2alias 열의 선언된 이름을 지킨다. 원인: add_column_start_dt_alias_start_year(orm.year())의 결과 키가 StartYear이고 값이 null인데, Go 출력은 start_year에 연도 값이 있다. 완료 기준: columns와 functions vector에서 키는 선언된 alias이고 값은 함수 결과이며 Go 출력과 같다. Red: test tests/test_alias_columns.py는 처음에 별칭 key 목록을 확인했고 AssertionError: Lists differ: ['StartYear', 'name', 'seq', ...]로 실패했다. Green: add-column 함수가 별칭을 snake(alias)로 저장한 뒤, 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_alias_columns가 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. bench database에서 vector columns와 functions의 결과 값은 Go 출력과 같다. 별칭 key는 start_year이고 값은 2026이며 months는 [6, 6]이다. functions의 statement는 $NOW bind에서만 다르며, 이것은 T43.5-2-3-6 항목이다.——[o]
T43.5-2-3-3하위 쿼리 열을 만든다. 원인: add_column_read_total(lambda u: Author().sum_read_count()...)가 _subquery(core.py)에서 없는 key를 지우려 해 KeyError 'columns'로 실패한다. 완료 기준: subqueries vector가 Go 출력과 같다. Red: 외부 PYTHONPATH가 없고 새 TMPDIR인 환경에서 python -m unittest test_subqueries가 종료 코드 1로 끝나며, 열이 없는 집계 하위 쿼리에서 _subquery가 KeyError: 'columns'를 냈다. Green: 없는 columns key를 pop하고 하위 쿼리 열 이름을 snake_case(ReadTotal이 아니라 read_total)로 저장한 뒤, 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. bench database에서 vector subqueries는 result와 statements가 Go 출력과 같다.——[o]
T43.5-2-3-4terminal-by 조건을 기존 조건에 AND로 잇는다. 원인: service_seq(7) 뒤의 get_count_by_is_close(True)가 CONFIG 'condition without and/or after another condition'으로 실패하는데, Go 출력은 AND로 잇는다. 완료 기준: terminal_reuse vector가 Go 출력과 같다. Red: 외부 PYTHONPATH가 없고 새 TMPDIR인 환경에서 python -m unittest test_terminal_by가 종료 코드 1로 끝나며 CONFIG: condition without and/or after another condition을 냈다. Green: terminal-by 메서드는 조건을 기존 조건에 AND로 잇고, 쿼리의 복사본에서 실행하므로 쿼리는 자기 조건을 유지한다. 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. bench database에서 vector terminal_reuse는 result와 statements가 Go 출력과 같다.——[o]
T43.5-2-3-5조인되거나 관계된 model을 결과에 넣는다. 원인: 조인 행의 to_array와 관계 행의 to_array가 Go 출력에 있는 중첩 model(service_model, service_member_models)을 빠뜨린다. 완료 기준: joins와 relations vector가 Go 출력과 같다. Red: 외부 PYTHONPATH가 없고 새 TMPDIR인 환경에서 python -m unittest test_related_output이 종료 코드 1로 끝나며 KeyError: 'service_model'을 냈다. 행 출력이 관련 model을 빠뜨리기 때문이다. Green: 행 출력은 관련 model을 그 행의 출력으로, collection을 그 목록으로 쓴다. 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. Python 단위 test 전체(48개)가 통과한다. bench database에서 vector joins와 relations는 result와 statements가 Go 출력과 같다.——[o]
T43.5-2-3-6statement event에 $NOW와 $SECRET을 쓴다. 원인: docs/operations/usage.md는 실행기 시각 값을 $NOW, 비밀을 $SECRET으로 적는데, Python client(database.py)는 <now>와 <secret>을 쓴다. 완료 기준: now_defaults의 event가 Go 출력과 같고, 코드가 규격을 따른다. Red: 외부 PYTHONPATH가 없고 새 TMPDIR인 환경에서 python -m unittest test_event_placeholders가 종료 코드 1로 끝났다. AssertionError: '$NOW' not found in [...]이며, INSERT event가 시각 bind를 <now>로 보이기 때문이다. Green: 시각 bind는 $NOW로, 바인드된 비밀은 $SECRET으로 보인다(docs/operations/usage.md 규격). 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. bench database에서 vector functions는 Go 출력과 같다. vector now_defaults는 result가 같고, statements의 차이는 이 프로세스가 먼저 실행하는 orm__row_lock table 생성뿐이다. 전체 실행은 Go 실행과 같은 위치에 둔다.——[o]
T43.5-2-3-7new_label로 붙인 값을 model에 남긴다. 원인: new_label('created')는 Go client가 간직하고 행 출력에 쓰는 label이라는 값을 붙이는데, Python client는 그것을 버린다. 완료 기준: write_cycle vector가 Go 출력과 같다. Red: 외부 PYTHONPATH가 없고 새 TMPDIR인 환경에서 python -m unittest test_new_values가 종료 코드 1로 끝나며 KeyError: 'label'을 냈다. 만든 model이 new_label로 붙인 값을 출력하지 않기 때문이다. Green: 출력은 model에 붙여진 값을 쓴다. 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. Python 단위 test 전체(50개)가 통과한다. bench database에서 vector write_cycle은 result와 statements가 Go 출력과 같다.——[o]
T43.5-2-3-8remove_all_columns()에서 기본 열을 뺀다. 원인: Python client에서 Author().connect(db).remove_all_columns().get_by_seq(42)는 seq, user_seq, service_seq, service_region_seq, service_member_seq, name을 돌려주는데, Go client는 선택한 열만 돌려준다. vector가 열을 골라 비교하므로 이 차이가 가려져 있다. 완료 기준: remove_all_columns().add_column_name().get_by_seq(42)가 Go client처럼 seq와 name만 돌려주고, 정확한 key 목록을 확인하는 단위 test가 있다. 정정: 위의 원인은 틀렸다. docs/dsl.md는 removeAllColumns()가 기본키와 외래키만 남긴다고 적고, Go 계획기는 PrimaryKey || ForeignKey인 열을 남기며(engine/planner/planner.go), vector relations의 Go 출력에도 author의 service_seq, user_seq 등 키 열이 남아 있다. Python 계획기는 이미 같은 규칙이므로 client를 바꿀 필요가 없다.—증거: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_remove_columns가 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. 이 test는 문서화된 규칙의 회귀 가드다.[o]
T43.5-2-3-9SQLite 행 잠금 statement의 tables에 orm__row_lock을 적는다. 원인: vector transactions의 행 잠금 INSERT(INSERT INTO "orm__row_lock" ... ON CONFLICT)는 Python event에서 tables가 []이고 Go event에서는 ["orm__row_lock"]이며, 26개 vector 전체 비교에서 이것이 유일한 차이다(Python 65802바이트, Go 65854바이트). 완료 기준: 그 statement의 event tables가 Go client처럼 orm__row_lock을 담고, transactions의 statements가 Go 출력과 같다. Red: 외부 PYTHONPATH가 없고 새 TMPDIR인 환경에서 python -m unittest test_row_lock_event가 종료 코드 1로 끝났다. AssertionError: Lists differ: [] != ['orm__row_lock']이며, SQLite 행 잠금 INSERT event가 table을 적지 않기 때문이다. Green: statement event가 orm__row_lock을 적고, 같은 실행이 종료 코드 0으로 끝나며 test 1개가 OK이고 항목을 0개 남긴다. Python 단위 test 전체(52개)가 통과한다. 새로 시딩한 SQLite bench database에서 26개 vector 전체 실행은 Go 출력과 바이트 단위로 같다(65,854바이트, cmp 차이 없음).——[o]
T43.5-2-4conformance checker에 python을 등록한다. 원인: tests/conformance/check/main.go는 go, php, rust, typescript 네 언어를 요구하고, conformance-result-check에는 Python 단계가 없다. 완료 기준: requiredLanguages와 runOne이 python을 담고, runner interpreter는 ORM_PYTHON에서 오며 설정되지 않으면 해결 방법을 밝히는 메시지로 실패하고, conformance-result-check에 python 단계가 있으며, conformance result 검사가 통과한다. Red: 단계가 있기 전 make conformance-result-check/python은 "No rule to make target"으로 종료 코드 2였다. python을 필수 언어로 둔 뒤 go test ./tests/conformance/check -run TestCompleteLanguageEvidence는 conformance requires 5 language outputs, got 4로 실패했다. test가 언어 네 개를 고정해 두었기 때문이다. Green: make conformance-result-check/python이 종료 코드 0으로 끝난다(result_python.py 7개 test OK). go vet ./tests/conformance/check가 종료 코드 0이다. go test ./tests/conformance/check -count=1이 종료 코드 0이다. make -n conformance-check/run과 make -n conformance-result-check가 종료 코드 0이다. 검사기는 ORM_PYTHON과 ORM_PYTHON_MODELS로 Python runner를 실행하며, 두 값은 conformance-check/run이 설정하고 conformance-python-models가 만든다. 전체 conformance 실행은 MySQL과 PostgreSQL이 필요하므로 CI에서 실행된다.——[o]
T43.5-5공유 dbspec 비교 runner 목록에 python을 등록한다(감사 항목 1). 원인: 감사에서 feat/python-T43의 d988a0f0 시점 tests/dbspec/compare/runners.mjs는 python 없이 go, php, typescript, rust, php-extension만 나열했다. feat/contract-T43.5의 커밋 be5903f1이 다섯 INPUTS를 쓰는 python runner를 추가했다. 완료 기준: T43.5를 담는 모든 branch가 runner 목록에 python을 두고, tests/dbspec/compare/check.mjs가 그것을 실행하며, runner 여섯 개의 출력이 같다. Red: git show d988a0f0:tests/dbspec/compare/runners.mjs는 python runner를 0개 나열하고, HEAD는 1개를 나열한다(커밋 be5903f1). 우회: 여섯 runner 중 다섯 개가 로컬에서 Go 출력과 같다(각 3385줄, cmp): go, python, php, typescript, rust. php-extension runner는 검증하지 못했다. 기본 flag로 만든 로컬 build는 arm64 PHP에서 x86_64로 적재되고, arm64 flag를 명시한 재빌드는 권한 시스템이 거부했다. 원인: 이 기기의 php-extension build. 재시도: 확장이 PHP 바이너리에 맞게 build되는 기기나 CI에서 make dbspec-compare-check를 실행한 뒤 T43.5-5를 닫는다. 재개: 재시도 조건이 성립한다. CI 실행 37803770504(commit fcebb854)의 job feature_verify_other(113403210727)가 Python runner와 php-extension build를 포함해 make dbspec-compare-check를 실행했고, 단계 schema-compare가 통과했다(PASS equal outputs agree, PASS the unchanged vectors run in every runner). 이 branch의 최종 head에서는 같은 job이 그 실행을 반복한다. 증거(로컬): make dbspec-compare-check가 0으로 끝난다: tests/dbspec/compare/check.test.mjs가 통과하고 go, php, typescript, rust, php-extension, python runner의 출력이 같다(dbspec-compare/agreement: 12 run 일치).——[o]
T43.5-6Python test가 만든 임시 file과 directory를 지운다(감사 항목 2). 원인: clients/python/tests/test_orm_gen.py, test_sqlite.py, dbspec_apply.py가 임시 file과 directory를 만들고, test_sqlite.py의 tearDownClass는 database만 닫는다. 완료 기준: 임시 경로마다 tempfile.TemporaryDirectory로 만들거나 addCleanup으로 지우며, 새 TMPDIR에서 Python 단위 test를 한 번 실행한 뒤 그 directory에 항목이 없다. Red: sibling ordered-json을 PYTHONPATH로 준 한 번의 실행 뒤 임시 directory에 test_orm_gen.py는 5개, test_sqlite.py는 11개, dbspec_apply.py는 3개 항목이 남았다. Green: 같은 실행이 종료 코드 0으로 끝나고(test_orm_gen.py 6개 test OK, test_sqlite.py OK, dbspec_apply.py가 sqlite 시나리오 4개 일치를 출력), 항목을 0개 남긴다. 하위 프로세스의 import 문제는 별도 항목 T43.5-6-1이다.——[o]
T43.5-6-1Python test의 orm-gen 하위 프로세스에 sibling ordered-json을 넘긴다. 원인: test_orm_gen.py와 test_sqlite.py는 polyspec.ordered_json을 import하는 orm-gen을 하위 프로세스로 실행하는데, test는 그 경로를 자기 sys.path에만 넣는다. PYTHONPATH가 없으면 하위 프로세스가 ModuleNotFoundError로 실패한다. 완료 기준: 두 file의 하위 프로세스 환경에 sibling ordered-json source 경로가 들어 있고, PYTHONPATH를 주지 않아도 두 file이 통과한다. Red: 외부 PYTHONPATH가 없는 깨끗한 환경(env -u PYTHONPATH)에서 python -m unittest test_orm_gen과 test_sqlite가 종료 코드 1로 끝났다. 하위 프로세스가 polyspec.ordered_json을 찾지 못해 CalledProcessError가 났다. Green: 같은 실행이 종료 코드 0으로 끝나고(test_orm_gen 6개 test OK, test_sqlite 13개 test OK), 새 TMPDIR에 항목을 0개 남긴다. orm-gen 하위 프로세스는 자기 PYTHONPATH에 sibling ordered-json source 경로를 받는다(두 file의 ORM_GEN_ENV).——[o]
T43.5-7Python Mermaid 결과를 정확히 비교한다(감사 항목 3). 원인: clients/python/tests/dbspec_mermaid.py는 Mermaid text를 마지막 줄 끝 없이 비교하고, export 진단이 비어 있음을 확인하지 않으며, invalid case의 document가 None인지 확인하지 않고, version 1을 확인하지 않는다. clients/typescript/tests/dbspec-mermaid.mjs는 이 넷을 모두 확인한다. 완료 기준: dbspec_mermaid.py가 이 넷을 dbspec-mermaid.mjs와 같게 확인하고, 실패한 검사는 python -O에서도 사라지지 않도록 assert 대신 명시적 오류로 끝나야 한다. Red: 커밋된 driver는 export 줄을 splitlines로 비교해서 마지막 줄 끝을 비교하지 않았다. version, export 진단이 비어 있음, invalid 사례의 document 없음을 확인하지 않았고, assert를 썼다. Green: clients/python/.venv/bin/python clients/python/tests/dbspec_mermaid.py가 종료 코드 0으로 끝나고 mermaid cases: 13 (2 export, 6 import, 4 invalid, 1 round trip)을 출력한다. 잘못된 version은 CheckFailed로 끝난다(mermaid vectors version: expected 1, got 2). python -O 실행도 종료 코드 0이다. 검사가 assert를 쓰지 않기 때문이다.——[o]
T43.5-8docs/features.md에 Python client를 선언한다(감사 항목 4). 원인: T43.5-4는 contracts/features.json만 나열하고 docs/features.md는 나열하지 않으며, 이 문서에는 Python 열이나 행이 없다. 완료 기준: docs/features.md와 docs/features.ko.md가 다른 client를 나열하는 곳에 python을 나열하고, make docs-check가 종료 코드 0으로 끝난다. 우회: docs/features.md와 docs/features.ko.md는 scripts/features/build.mjs가 contracts/features.json에서 생성하며, 생성기는 go, php, rust, typescript만 나열한다. Python 열은 아직 증거가 없는 기능별 결과를 적게 되고, 손으로 고치면 다음 생성에서 덮어써진다. 원인: 기능별 Python 상태에는 T43.5-2와 T43.6의 conformance 증거가 필요하다. 재시도: T43.5-2와 T43.6이 그 증거를 기록한 뒤 contracts/features.json의 clients와 생성기에 python을 추가하고 node scripts/features/build.mjs를 실행한다.—근거: 생성기가 python을 나열하고(scripts/features/build.mjs), docs/features.md와 docs/features.ko.md는 기능 18개 행마다 python을 planned로 나열한다. 다시 만든 문서에서 make docs-check가 0을 반환한다(커밋 7fe76490).[o]
T43.5-9T43 이력 사실을 기록한다(감사 항목 5). 원인: 감사에서 커밋 a6183322와 feaf90ba의 본문이 비어 있어 무엇을, 왜 바꿨는지 적히지 않았고, T43.4가 a6183322에서 대기 상태였다가 작업 상태를 거치지 않고 d988a0f0에서 완료 상태가 된 것을 확인했다. 완료 기준: 커밋은 바꾸지 않고, 이 사실들을 여기에 기록한다. 증거(기록된 사실, 커밋은 바꾸지 않는다): 커밋 a6183322의 본문은 비어 있다. 커밋 feaf90ba의 본문도 비어 있다. T43.4는 a6183322에서 대기였고 d988a0f0에서 완료되었으며, 둘 사이의 체크리스트 커밋은 d988a0f0 하나뿐이라 T43.4의 작업 상태 커밋이 없다. T43.4의 구현은 a6183322, 9ed5dddc, f8d2981d에 있고, 완료는 d988a0f0에 있다.——[o]
T43.5-3Python client를 interface 선언에 넣는다. 원인: tests/interfaces/check는 선언된 언어마다 모든 rule, record, owner의 symbol 목록과 signature를 요구한다. 완료 기준: Python 추출기가 clients/python/src의 공개 symbol을 읽고, contracts/symbols/python.json과 contracts/interfaces.json의 symbol_hashes 항목이 그 목록을 담으며, 모든 rule, record, owner가 Python symbol을 가리키고, contracts/validate.go의 languages와 outputs에 python이 있고, make interface-check가 통과한다.—근거: 하위 항목(T43.5-3-1부터 T43.5-3-9와 그 하위)이 모두 닫혔고, make interface-check가 python을 포함한 언어 5개로 0을 반환하며, Python interface 테스트(12개)가 통과한다.[o]
T43.5-3-1Python symbol 추출기와 snapshot을 추가한다. 원인: tests/interfaces/check는 언어마다 공개 symbol을 native 추출기로 읽는데, Python 추출기와 contracts/symbols/python.json이 없다. 완료 기준: tests/interfaces/python.py가 clients/python/src의 symbol JSON을 contracts/symbols/typescript.json과 같은 형태로 출력하고, contracts/symbols/python.json이 그 목록을 담으며, python은 아직 검사 대상 언어가 아니므로 검사기가 통과한다. Red: 추출기가 있기 전 python -m unittest tests/interfaces/test_python_extractor.py는 can't open file ... tests/interfaces/python.py로 종료 코드 1이었다. Green: 추출기가 생긴 뒤 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. 추출기는 clients/python/src의 공개 declaration을 tests/interfaces/typescript.mjs와 같은 형태로 보고한다. symbol 524개를 contracts/symbols/python.json에 저장했다. 검사기는 아직 python을 검사하지 않는다(T43.5-3-6).——[o]
T43.5-3-2contracts/validate.go에 python을 적는다. 원인: contracts/validate.go의 languages와 outputs는 네 client 언어를 나열하고 공통 결과마다 native 반환 형을 준다. 완료 기준: 모든 outputs와 inputs 항목이 python 형을 주며, go test ./contracts가 종료 코드 0으로 끝난다. languages에 python을 넣는 일은 T43.5-3-6에서 한다. 규칙 반복문이 나열된 각 언어에 대해 모든 규칙의 native 항목을 요구하기 때문이다. Red: 변경 전 기준선은 git show HEAD:contracts/validate.go에 outputs와 inputs 표의 "python" 항목이 0개인 것이다. Green: 변경 뒤에는 outputs 항목 42개와 inputs 항목 36개가 모두 python 형을 준다(grep -c "\"python\"" contracts/validate.go가 python 항목이 있는 줄 78개가 나온다). gofmt -l contracts/validate.go는 아무것도 출력하지 않고, go vet ./contracts와 go test ./contracts -count=1이 종료 코드 0으로 끝난다. languages는 여기서 바꾸지 않으며, python은 T43.5-3-6에서 나열한다. 규칙 반복문이 나열된 각 언어의 모든 규칙에 native 항목을 요구하기 때문이다. 완료 기준 문장도 이 분할에 맞게 고쳤다.——[o]
T43.5-3-3모든 rule에 python native 항목을 준다. 원인: contracts/interfaces.json의 rule 57개는 언어마다 native 항목을 가지며 python 항목이 없다. 완료 기준: 각 rule이 Python 추출기가 보고하는 python symbol과 signature를 적고, python은 아직 검사 대상이 아니므로 검사기가 통과한다.—증거: 하위 항목 T43.5-3-3-1(entity 규칙 22개), T43.5-3-3-2(연결과 도구 규칙 13개), T43.5-3-3-3(dbspec 규칙 22개)이 닫혔으므로, contracts/interfaces.json의 규칙 57개가 모두 스냅샷의 Python symbol을 가리키는 python native를 가지며, 서명은 스냅샷과 같고 매개변수와 반환 형은 공통 표와 같다. tests/interfaces에서 python -m unittest test_python_natives가 test 5개 OK로 종료 코드 0이다. record(T43.5-3-4)와 owner(T43.5-3-5)는 아직 하지 않았다.[o]
T43.5-3-3-1entity 규칙 22개에 Python native 항목을 준다. 원인: 각 entity 규칙은 Python Model 메서드를 가리키며, interface 규칙은 스냅샷이 기록한 symbol과 서명을 python native 항목으로 가져야 한다. 완료 기준: 각 entity 규칙의 python native가 contracts/symbols/python.json에 있는 symbol을 지정하고 서명이 그 스냅샷 값과 같다(공백을 뺀 비교). tests/interfaces/test_python_natives.py가 확인한다. Red: tests/interfaces에서 python native가 있기 전 python -m unittest test_python_natives는 AssertionError: unexpectedly None : Model.connect has no python native로 종료 코드 1이었다. native를 넣은 뒤 validate.go 표까지 보는 test는 AssertionError: '' != 'Self'로 종료 코드 1이었다(entity 규칙 17개에 반환 주석이 없거나 반환 형이 dict였다). Green: Model 메서드가 공통 표의 형을 갖고(Self, Collection[Self], GroupRows, gets_page는 Page[Self]이며 Page는 gets_page가 돌려주는 dict의 하위 클래스, delete는 None), 스냅샷 contracts/symbols/python.json을 다시 추출했고(symbol 1201개, 생성 model 포함), entity 규칙 22개의 python native가 그것과 같으며, python -m unittest test_python_natives가 종료 코드 0으로 끝난다. 회귀: Python 전체 test(70개)가 종료 코드 0이고, SQLite 26개 vector 출력은 Go 출력과 바이트 단위로 같으며(65,854바이트), make python-model-check가 종료 코드 0이다.——[o]
T43.5-3-3-2Db, Utils, SchemaUtils, AesUtils 규칙 13개에 Python native 항목을 준다. 원인: 각 규칙은 연결, 도구, schema나 AES 도구의 Python 메서드를 지정한다. 완료 기준: 각 규칙의 python native가 스냅샷의 symbol과 서명과 같다. tests/interfaces/test_python_natives.py가 확인한다. Red: tests/interfaces에서 native가 있기 전 python -m unittest test_python_natives.ConnectionAndToolRuleNativesTest는 AssertionError: unexpectedly None : Db.connect has no python native로 종료 코드 1이었다. Green: 연결, 도구, schema 도구, AES 도구 규칙 13개가 Db, Utils, SchemaUtils, AesUtils의 메서드를 가리키는 python native를 갖고, 서명은 다시 추출한 스냅샷과 같으며, 매개변수와 반환 형은 공통 표와 맞는다(주석은 공통 표의 형이다. 예: Db, Utils, Callable[[], T], list[str]). python -m unittest test_python_natives(두 test 클래스)가 종료 코드 0으로 끝난다. 회귀: Python 전체 test(70개)가 종료 코드 0이고, SQLite 26개 vector 출력은 Go 출력과 바이트 단위로 같으며(65,854바이트), make python-model-check가 종료 코드 0이다.——[o]
T43.5-3-3-3Dbspec 규칙 22개에 Python native 항목을 준다. 원인: 각 규칙은 Python dbspec 함수를 지정한다. 완료 기준: 각 규칙의 python native가 스냅샷의 symbol과 서명과 같다. tests/interfaces/test_python_natives.py가 확인한다.—증거: 세 그룹 T43.5-3-3-3-1, T43.5-3-3-3-2, T43.5-3-3-3-3가 모두 닫혔으므로, dbspec 규칙 22개가 모두 Python 함수를 가리키는 python native를 갖는다. 서명은 스냅샷과 같고, 매개변수와 반환 형은 공통 표와 같다. tests/interfaces에서 python -m unittest test_python_natives가 test 5개 OK로 종료 코드 0이다.[o]
T43.5-3-3-3-1파일, parse, emit, manifest, render, introspection, 외부 문서에 관한 dbspec 규칙 8개에 Python native 항목을 준다. 원인: Dbspec.readFile, readBytes, parse, emit, manifest, render, introspect, externalDifferences는 공통 표의 형에 대한 주석이 없는 Python 함수를 지정한다. 완료 기준: 각 규칙의 python native가 스냅샷과 같은 symbol과 서명을 갖고, 매개변수와 반환 형이 공통 표와 같다. tests/interfaces/test_python_natives.py가 확인한다. Red: tests/interfaces에서 native가 있기 전 python -m unittest test_python_natives.DbspecRuleNativesTest는 AssertionError: unexpectedly None : Dbspec.readFile has no python native로 종료 코드 1이었다. Green: 파일, parse, emit, manifest, render, introspection, 외부 문서에 관한 dbspec 규칙 8개가 read_dbspec_file, read_dbspec_bytes, parse_dbspec, emit_dbspec, dbspec_manifest, render_dbspec, introspect_dbspec, external_differences를 가리키는 python native를 갖고, 서명은 스냅샷과 같으며, 매개변수와 반환 형은 공통 표와 같다(introspection 결과 표 항목은 introspect_dbspec이 돌려주는 dict로 바로잡았다). python -m unittest test_python_natives가 test 3개 OK로 종료 코드 0이다. 회귀: Python 전체 test(70개)가 종료 코드 0이고, SQLite 26개 vector 출력은 Go 출력과 바이트 단위로 같으며(65,854바이트), make python-model-check가 종료 코드 0이고, go test ./contracts가 종료 코드 0이다.——[o]
T43.5-3-3-3-2plan, 비교, upgrade step에 관한 dbspec 규칙 8개에 Python native 항목을 준다. 원인: Dbspec.parsePlan, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps는 공통 표의 형에 대한 주석이 없는 Python 함수를 지정한다. 완료 기준: T43.5-3-3-3-1과 같이, 이 규칙 8개에 대해 확인한다. Red: tests/interfaces에서 native가 있기 전 python -m unittest test_python_natives.DbspecRuleNativesTest.test_plans_comparison_upgrade_group는 AssertionError: unexpectedly None : Dbspec.parsePlan has no python native로 종료 코드 1이었다. Green: plan, 비교, upgrade step에 관한 dbspec 규칙 8개가 parse_plan, emit_plan, chain_plans, diff_plan, compare_schemas, installed_differences, add_tables_and_columns_steps, plan_steps를 가리키는 python native를 갖고, 서명은 스냅샷과 같으며, 매개변수와 반환 형은 공통 표와 같다(plan 결과 여섯 개는 이 함수들이 돌려주는 dict이므로 표 항목을 dict로 맞췄다). python -m unittest test_python_natives가 test 4개 OK로 종료 코드 0이다. 회귀: Python 전체 test(70개)가 종료 코드 0이고, SQLite 26개 vector 출력은 Go 출력과 바이트 단위로 같으며(65,854바이트), make python-model-check가 종료 코드 0이고, go test ./contracts가 종료 코드 0이다.——[o]
T43.5-3-3-3-3apply, 복구, rollback, finalize, Mermaid 변환에 관한 dbspec 규칙 6개에 Python native 항목을 준다. 원인: Dbspec.apply, recover, rollback, finalize, exportMermaid, importMermaid는 공통 표의 형에 대한 주석이 없는 Python 함수를 지정한다. 완료 기준: T43.5-3-3-3-1과 같이, 이 규칙 6개에 대해 확인한다. Red: tests/interfaces에서 native가 있기 전 python -m unittest test_python_natives.DbspecRuleNativesTest.test_apply_recovery_rollback_finalize_mermaid_group는 AssertionError: unexpectedly None : Dbspec.apply has no python native로 종료 코드 1이었다. Green: apply, recovery, rollback, finalization, Mermaid 변환에 관한 dbspec 규칙 6개가 apply_plans, recover_plans, rollback_plans, finalize_plans, export_mermaid, import_mermaid를 가리키는 python native를 갖고, 서명은 스냅샷과 같으며, 매개변수와 반환 형은 공통 표와 같다(apply handler는 Callable[[dict], None]의 별칭이다). python -m unittest test_python_natives가 test 5개 OK로 종료 코드 0이다. 회귀: Python 전체 test(70개)가 종료 코드 0이고, SQLite 26개 vector 출력은 Go 출력과 바이트 단위로 같으며(65,854바이트), make python-model-check가 종료 코드 0이고, go test ./contracts가 종료 코드 0이다.——[o]
T43.5-3-4모든 record에 python native 항목을 준다. 원인: contracts/interfaces.json의 record 19개는 언어마다 native 항목을 가지며 python 항목이 없다. 완료 기준: 각 record가 python 파일, symbol, field를 적고, 검사기가 통과한다.—근거: T43.5-3-4-1과 T43.5-3-4-2가 닫혔으므로 interfaces.json의 record 19개마다 python 파일, symbol, 필드가 있다(clients/python/src/polyspec/orm/ir.py::<id>, #wire 필드는 test_python_natives.RecordNativesTest가 확인); make interface-check가 0을 반환한다.[o]
T43.5-3-4-1interface 추출기가 Python TypedDict record를 읽는다. 원인: record 검사는 각 record의 #wire symbol(필드마다 wire 형과 @flatten base)을 읽는데, tests/interfaces/python.py는 #wire symbol을 만들지 않는다. 완료 기준: TypedDict class가 필드마다 wire 형을 주고(int는 integer, str은 text, bool은 bool, list of X는 list<X>, dict of X는 map<X>, 이름은 그 이름, X | None과 NotRequired of X는 X), base에 대해 @flatten을 준다. tests/interfaces/test_python_extractor.py가 확인한다.—근거: Red: tests/interfaces에서 python -m unittest test_python_extractor가 #wire를 내기 전에 KeyError: 'pkg/mod.py::Base#wire'로 1을 반환했고, base에 #wire를 준 뒤에는 TypedDict가 아니라 record를 상속하는 Request(Base)가 KeyError: 'pkg/mod.py::Request#wire'로 1을 반환했다. Green: TypedDict를 직접 상속하거나 같은 모듈의 record를 상속하는 class가 base에 대한 @flatten과 함께 #wire를 내고, 같은 명령이 테스트 2개 OK로 0을 반환한다. 클라이언트에는 아직 TypedDict가 없어 스냅샷 contracts/symbols/python.json은 바뀌지 않는다. 회귀: tests/interfaces에서 python -m unittest discover가 테스트 7개 OK로 0을 반환하고, make checklist-check, make docs-rules-check, make version-check가 0을 반환한다.[o]
T43.5-3-4-2IR record 19개를 TypedDict로 선언하고 Python native를 준다. 원인: contracts/interfaces.json의 record 19개는 언어마다 native 항목을 가지며, python 항목이 없다. 완료 기준: polyspec/orm/ir.py가 각 record를 필드와 base를 가진 TypedDict로 선언하고, 각 record의 python native #wire가 필드와 같다. tests/interfaces/test_python_natives.py가 확인한다.—근거: Red: tests/interfaces에서 python -m unittest test_python_natives.RecordNativesTest가 record에 python native가 없을 때 AssertionError: unexpectedly None : IRRequest has no python native로 1을 반환했다. Green: ir.py가 record 19개를 TypedDict로 선언하고(필드 not이 키워드라 IRGroup은 함수형 형식), base(예: IRQuery)는 @flatten으로 펼치며, interfaces.json의 record마다 clients/python/src/polyspec/orm/ir.py::<id> native가 있고 그 #wire가 base를 펼친 필드와 같다. python -m unittest test_python_natives가 테스트 7개 OK로 0을 반환한다. 회귀: tests/interfaces에서 python -m unittest discover가 테스트 9개 OK로 0을 반환하고, make interface-check, go test ./contracts, make python-model-check, make checklist-check, make docs-rules-check, make version-check가 0을 반환한다. 스냅샷은 ir.py에서 온 키 118개만 늘었다.[o]
T43.5-3-5모든 owner에 python native 항목을 준다. 원인: contracts/interfaces.json의 owner 16개는 언어마다 native 항목을 가지며 python 항목이 없다. 완료 기준: 각 owner가 python symbol과 field를 적고, 검사기가 통과한다.—근거: T43.5-3-5-1이 닫혔으므로 interfaces.json의 owner 16개마다 python symbol과 필드가 있다. make interface-check가 0을 반환한다(검사기는 아직 4개 언어만 확인하며, python은 T43.5-3-6에서 추가한다).[o]
T43.5-3-5-1owner 16개에 Python 형과 native를 준다. 원인: contracts/interfaces.json의 owner 16개는 언어마다 class와 필드를 가지며, python 항목이 없다. 완료 기준: 각 owner가 필드 이름이 python 필드와 같은 Python class 또는 TypedDict를 갖고, 그것을 가리키는 python native를 가진다. tests/interfaces/test_python_natives.py가 확인한다.—근거: T43.5-3-5-1-1, T43.5-3-5-1-2, T43.5-3-5-1-3이 닫혔으므로 interfaces.json의 owner 16개마다 필드가 owner의 python 필드와 같은 Python class 또는 TypedDict와, 그것을 지정하는 python native가 있다(test_python_natives.OwnerNativesTest가 확인하며, 인터페이스 suite에서 테스트 12개 OK).[o]
T43.5-3-5-1-1Page와 AESRotationStatus를 필드가 있는 Python owner로 선언한다. 원인: interfaces.json의 owner 16개 중 이 둘은 python 항목이 없다. 완료 기준: 각각 필드 이름이 owner의 python 필드와 같은 Python class 또는 TypedDict가 있고, 이를 가리키는 python native가 있으며, tests/interfaces/test_python_natives.py가 확인한다.—근거: Red: tests/interfaces에서 python -m unittest test_python_natives.OwnerNativesTest가 native가 없을 때 AssertionError: unexpectedly None : Page has no python native로 1을 반환했다. Green: Page(model.py)는 필드 items, totalCount, totalPages, page, perPage를 클래스 annotation으로 선언하며, 런타임을 바꾸지 않고 dict 키와 맞는다. AESRotationStatus(utils.py)는 current, total, pending, versions를 가진 dataclass다. interfaces.json의 owner 두 개가 이 Python symbol과 필드를 가리키고, 테스트가 스냅샷의 #field symbol을 그 필드와 비교한다. 테스트 모듈이 테스트 8개 OK로 0을 반환한다. 회귀: 인터페이스 suite가 테스트 10개 OK로 0을 반환하고, make python-model-check, make interface-check, go test ./contracts가 0을 반환한다. 스냅샷은 키 5개만 늘었다.[o]
T43.5-3-5-1-2계획 owner 7개를 필드가 있는 Python owner로 선언한다. 원인: DbspecDiagnostic, DbspecManifest, DbspecPlan, DbspecPlanStep, DbspecEffect, DbspecNullCheck, DbspecChange에 python 항목이 없다. 완료 기준: 각각 필드 이름이 owner의 python 필드와 같은 Python class 또는 TypedDict가 있고, 이를 가리키는 python native가 있으며, tests/interfaces/test_python_natives.py가 확인한다.—근거: Red: tests/interfaces에서 python -m unittest test_python_natives.OwnerNativesTest가 native가 없을 때 AssertionError: unexpectedly None : DbspecDiagnostic has no python native로 1을 반환했다. Green: owner 7개의 native가 dbspec/model.py의 Python 이름을 지정한다(DbspecDiagnostic은 dataclass, 필드 from이 키워드인 DbspecPlan은 함수형 TypedDict, DbspecPlanStep, DbspecEffect, DbspecNullCheck, DbspecChange는 TypedDict)과 dbspec/manifest.py(DbspecManifestResult, 필드는 class annotation). 필드는 스냅샷의 #field symbol과 같고, 추출기가 이제 함수형 TypedDict의 필드도 읽는다. 테스트 모듈과 인터페이스 suite가 0을 반환하고, make python-model-check, make interface-check, go test ./contracts, make dbspec-compare-check/unit이 0을 반환한다. 스냅샷은 키 45개만 늘었다.[o]
T43.5-3-5-1-3차이와 apply owner 7개를 필드가 있는 Python owner로 선언한다. 원인: DbspecDifference, DbspecUnsupported, DbspecTableRename, DbspecColumnRename, DbspecColumnName, DbspecApplyEvent, DbspecApplyError에 python 항목이 없다. 완료 기준: 각각 필드 이름이 owner의 python 필드와 같은 Python class 또는 TypedDict가 있고, 이를 가리키는 python native가 있으며, tests/interfaces/test_python_natives.py가 확인한다.—근거: Red: tests/interfaces에서 python -m unittest test_python_natives.OwnerNativesTest가 native가 없을 때 AssertionError: unexpectedly None : DbspecDifference has no python native로 1을 반환했다. Green: owner 7개의 native는 dbspec 코드가 쓰는 dict 키를 필드로 한다. DbspecDifference, DbspecUnsupported(별칭 Unsupported = dict를 대신하며 Unsupported는 이 클래스를 지정), DbspecTableRename, DbspecColumnRename, DbspecColumnName, DbspecApplyEvent는 dbspec/model.py의 TypedDict이고, DbspecApplyError(apply.py)는 필드를 class annotation으로 선언한다. 필드는 스냅샷의 #field symbol과 같고, 인터페이스 suite에서 테스트 12개 OK로 0을 반환한다. 회귀: make python-model-check, make interface-check, go test ./contracts, make dbspec-compare-check/unit이 0을 반환하고 dbspec 모듈이 import된다. 스냅샷은 키 35개만 늘었다.[o]
T43.5-3-6interface manifest와 검사기에 python을 활성화한다. 원인: tests/interfaces/check/main.go는 정확히 네 언어와 네 symbol hash를 요구하고, 추출기 dispatch와 언어 목록에 네 언어만 있다. 완료 기준: contracts/interfaces.json이 python을 roots와 symbol hash와 함께 담고, 검사기가 다섯 언어를 요구하며 Python 추출기를 부르고, make interface-check가 종료 코드 0으로 끝난다.—근거: Red: 검사기가 python을 모를 때 make interface-check가 FAIL interfaces/python: unsupported language python으로 2를 반환했고, 추출기 분기를 추가한 뒤에는 소스 변이 단계가 python parser returned too few fixture declarations로 실패했다. go test ./contracts는 drift 테스트에 python의 이전 시그니처가 없어 python에 대해 a matching native snapshot could silently change Collection to scalar로 1을 반환했다. Green: validate.go가 python을 나열하고, manifest가 python의 roots와 contracts/symbols/python.json의 hash를 나열하며, 검사기가 언어 5개를 요구하고 tests/interfaces/python.py를 실행하며 python의 금지 호출 패턴, 확장자, 변이 픽스처를 갖는다. make interface-check가 PASS interfaces/python과 함께 0을 반환하고, ./contracts와 ./tests/interfaces/check에 대한 go vet과 go test가 0을 반환한다. 회귀: Python 인터페이스 suite가 테스트 12개 OK로 0을 반환하고, make python-model-check와 make docs-rules-check가 0을 반환한다.[o]
T43.5-3-7Python client에 Utils.schema()를 추가한다. 원인: TypeScript와 Go client는 schema 도구를 utils().schema()로 내놓고, 규칙 목록은 Utils.schema를 적는다. Python Utils에는 schema()가 없다. 완료 기준: db.utils().schema()가 SchemaUtils 객체를 돌려주고, 접근자의 단위 test가 통과한다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_schema_utils가 종료 코드 1로 끝났다. ImportError: cannot import name 'SchemaUtils' from 'polyspec.orm.utils'다. Green: Utils.schema()가 연결의 SchemaUtils를 돌려준 뒤, 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다.——[o]
T43.5-3-8Python client에 SchemaUtils.register(schema)를 추가한다. 원인: TypeScript client는 set을 등록하기 전에 manifest text가 선언된 hash로 해시되는지 확인하고, 아니면 CONFIG로 실패하며 아무것도 등록하지 않는다. Python Db.register는 확인하지 않는다. 완료 기준: hash가 맞지 않으면 CONFIG로 실패하고 아무것도 등록하지 않으며, 같은 set을 다시 등록해도 아무것도 바뀌지 않는다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_schema_utils.SchemaRegisterTest가 종료 코드 1로 끝났다. 두 test 모두 AttributeError: 'SchemaUtils' object has no attribute 'register'다. Green: register가 hash를 확인하고(맞지 않으면 CONFIG, 아무것도 등록하지 않음), 이미 등록된 set은 그대로 둔 뒤, 같은 실행이 종료 코드 0으로 끝나고 test 3개(접근자 test 포함)가 OK이며 항목을 0개 남긴다.——[o]
T43.5-3-9Python client에 SchemaUtils.install(schema)를 추가한다. 원인: TypeScript client는 document set을 dialect에 맞게 render하고, 소유한 table이 하나도 없으면 만들고, 모두 있으면 아무것도 만들지 않으며, 일부만 있으면 CONFIG로 실패하고, database를 다시 읽어 차이를 모두 CONFIG로 알리며, transaction 안의 MySQL을 거부한다. 완료 기준: 같은 schema에서 SQLite 동작이 TypeScript client와 같고, 각 경우를 단위 test가 확인한다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_schema_install이 종료 코드 1로 끝났다. 각 test가 AttributeError: 'SchemaUtils' object has no attribute 'install'이다. Green: install이 driver에 맞게 set을 render하고, 빈 database에 소유 table을 만들고, 모두 있으면 아무것도 만들지 않으며, 일부만 있는 set과 다른 database를 거부한 뒤(메시지는 TypeScript client의 것이다: unsupported_column author.junk: declared type INTEGER with CHECK "" has no dbspec type), 같은 실행이 종료 코드 0으로 끝나고 test 3개가 OK이며 항목을 0개 남긴다. Python 전체 test(64개)도 종료 코드 0이다. MySQL 경로(transaction 안의 CONFIG와 session 문장)는 CI에서만 실행된다.——[o]
T43.5-3-13생성된 Python bench model을 커밋하고 검사한다. 원인: Go와 TypeScript client는 schema/bench.dbs에서 만든 model을 커밋하지만(clients/go/model, clients/typescript/src/models), Python client는 커밋하지 않는다. 계획은 이것을 clients/python/src/polyspec/orm/models/에 둔다. 완료 기준: clients/python/src/polyspec/orm/models/models.py가 schema/bench.dbs의 orm-gen gen 출력이고, make python-model-check가 orm-gen gen --check를 실행해 종료 코드 0으로 끝난다. Red: 대상이 있기 전 make python-model-check는 종료 코드 2("No rule to make target")였다. Green: 생성된 bench model이 clients/python/src/polyspec/orm/models/models.py에 커밋되어 있고(model 12개, manifest sha256:74501d5f..., TypeScript client와 같은 manifest hash), make python-model-check가 종료 코드 0으로 끝난다. models.py를 변조하면 종료 코드 2로 실패하고, 원래 파일로 되돌리면 다시 종료 코드 0이다.——[o]
T43.5-3-14contracts/validate.go가 Python 서명을 읽게 한다. 원인: 규칙 검사는 native 서명의 매개변수 목록과 반환 형을 읽고, PHP와 TypeScript는 :를, Rust는 ->를 지운다. Python 서명은 receiver로 self를 두고 반환 형 앞에 ->를 쓰는데, 검사는 이를 읽지 않는다. 완료 기준: contracts/validate.go가 self receiver와 -> 반환 형의 Python 서명을 다른 client와 같은 입력과 출력으로 읽고, go test ./contracts가 종료 코드 0으로 끝난다. Red: pythonParts가 있기 전 go test ./contracts -run TestPythonSignatureReadsLikeTheOtherClients -count=1은 undefined: pythonParts로 종료 코드 1이었다. Green: pythonParts는 self를 receiver로, "->"를 반환 표시로 읽고, 검사 반복문이 python에 이를 쓰며, go test ./contracts -count=1이 종료 코드 0으로 끝난다. go vet ./contracts는 종료 코드 0이고 gofmt -l contracts/는 아무것도 출력하지 않는다.——[o]
T43.5-3-15Python client에 Model.and와 Model.or를 명시적으로 정의한다. 원인: 규칙 목록은 Model.and와 Model.or를 적는데, Python Model은 이것을 __getattr__로만 만들며 정적 symbol 스냅샷에 나타나지 않는다. 완료 기준: and_와 or_가 Model의 메서드로 같은 조건 동작을 하고, contracts/symbols/python.json이 이를 나열한다. 정정: 위의 원인은 틀렸다. 스냅샷은 clients/python/src/polyspec/orm/model.py::Model.and_와 Model.or_(각각 and, or의 Python 이름)를 나열하므로 Model 메서드는 이미 있으며, 앞선 확인은 camelCase 이름과 맞춰 봤다.—증거: 스크립트가 규칙 목록의 각 이름을 snake_case Python snake_case로 바꿔 contracts/symbols/python.json에서 entity 규칙 22개를 모두 찾는다. client 변경은 필요 없다.[o]
T43.5-3-16Python client에 Model.getCount, getSum, getAvg, getsCount, getsPage를 명시적으로 정의한다. 원인: 규칙 목록이 이를 적는데, Python Model은 __getattr__로만 처리한다. 완료 기준: get_count, get_sum, get_avg, gets_count, gets_page가 같은 결과를 내는 Model 메서드이고, 스냅샷이 이를 나열한다. 정정: 위의 원인은 틀렸다. 스냅샷은 clients/python/src/polyspec/orm/model.py::Model.get_count(-> int), get_sum, get_avg, gets_count, gets_page(-> dict)를 명시적 메서드로 나열한다. 그러므로 규칙 목록의 이름은 Python에서 snake_case이고, 메서드는 있다.—증거: 스크립트가 규칙 이름을 snake_case로 바꿔 contracts/symbols/python.json에서 각각 찾는다. client 변경은 필요 없다.[o]
T43.5-3-17Python client에 Model.getQuery와 Model.toArray를 명시적으로 정의한다. 원인: 규칙 목록이 이를 적는데, Python Model은 get_query를 __getattr__로 처리하고, 행 출력은 스냅샷에 규칙용으로 나열되지 않은 to_array로 쓴다. 완료 기준: get_query와 to_array가 같은 결과를 내는 Model 메서드이고, 스냅샷이 이를 나열한다. 정정: 위의 원인은 틀렸다. 스냅샷은 clients/python/src/polyspec/orm/model.py::Model.get_query(-> dict)와 Model.to_array(-> dict)를 명시적 메서드로 나열하며, entity 규칙은 이를 snake_case로 찾는다.—증거: 스크립트가 규칙 이름을 snake_case로 바꿔 contracts/symbols/python.json에서 둘 다 찾는다. client 변경은 필요 없다.[o]
T43.5-3-18Python client에 AesUtils.rotate를 정의한다. 원인: 규칙 목록은 AesUtils.rotate를 적는데, Python AES 모듈의 rotate 함수는 AesUtils의 메서드가 아니다. 완료 기준: AesUtils.rotate(model, keyring)이 현재 key version이 아닌 model의 행을 다시 암호화하고 그 수를 돌려주며, TypeScript client와 같고, 단위 test가 이를 확인한다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_aes_rotate가 종료 코드 1로 끝났다. AttributeError: 'AesUtils' object has no attribute 'rotate'다. Green: AesUtils.rotate가 현재 key version이 아닌 model의 행을 현재 key로 다시 암호화하고 현재 version을 준 뒤(1000행씩 읽고 transaction 하나에서), 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다. 행 세 개가 회전되고, 두 번째 실행은 0개를 회전하며, Python 전체 test가 종료 코드 0이다. PostgreSQL과 MySQL 형식의 회전은 CI에서만 실행된다.——[o]
T43.5-3-9-1SQLite의 지원하지 않는 column 메시지에서 CHECK 문장을 TypeScript client처럼 적는다. 원인: Python introspection은 CHECK가 없는 column에 with CHECK None을 쓰는데, TypeScript client는 with CHECK ""를 쓴다(빈 check에 JSON.stringify를 적용한 결과). 그래서 SchemaUtils.install의 차이 문장이 다르다. 완료 기준: CHECK가 없는 column의 메시지가 TypeScript client처럼 declared type INTEGER with CHECK "" has no dbspec type이고, 단위 test가 그 문장을 확인한다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_introspect_messages가 종료 코드 1로 끝났다. 'declared type INTEGER with CHECK None has no dbspec type' != 'declared type INTEGER with CHECK "" has no dbspec type'다. Green: SQLite introspection이 CHECK 문장을 JSON으로 적고 없는 check를 빈 문자열로 적은 뒤, 같은 실행이 종료 코드 0으로 끝나고 test 1개가 OK이며 항목을 0개 남긴다.——[o]
T43.5-3-10Python client에 SchemaUtils.addTablesAndColumns(schema)를 추가한다. 원인: TypeScript client는 설치된 set에 더 새로운 생성 schema version의 table과 column을 더하고, 그 밖의 변경은 CONFIG로 거부한다. 완료 기준: 같은 입력에서 SQLite 동작이 TypeScript client와 같고, 추가와 거부된 변경을 단위 test가 확인한다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_add_tables_and_columns가 종료 코드 1로 끝났다. 두 test 모두 AttributeError: 'SchemaUtils' object has no attribute 'add_tables_and_columns'다. Green: SchemaUtils.add_tables_and_columns가 추가 전용 step을 실행한 뒤(SQLite는 외래키를 끄고 BEGIN IMMEDIATE transaction 하나에서), 같은 실행이 종료 코드 0으로 끝나고 test 2개가 OK이며 항목을 0개 남긴다. 추가된 이름은 note와 users.email이고, null이나 기본값이 없는 column은 문장 실행 전에 SCHEMA_DIFFERS로 거부된다. Python 전체 test(69개)가 종료 코드 0이다. PostgreSQL과 MySQL 경로는 CI에서만 실행된다.——[o]
T43.5-3-11Python client에 Dbspec.installedDifferences를 추가한다. 원인: TypeScript client는 live document, 지원하지 않는 객체, set의 대상 document를 비교해 차이를 모두 나열하며, 규칙 목록이 이 함수를 적는다. 완료 기준: 공유 case에서 TypeScript client와 같은 차이를 돌려주고, 같은 set과 다른 set을 단위 test가 확인한다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_installed_differences가 종료 코드 1로 끝났다. ImportError: cannot import name 'installed_differences' from 'polyspec.orm.dbspec'다. Green: polyspec/orm/dbspec/installed.py의 installed_differences가 생긴 뒤, 같은 실행이 종료 코드 0으로 끝나고 test 5개가 OK이며 항목을 0개 남긴다. 기대 줄은 TypeScript client가 같은 입력에서 내는 줄이다(add_column users.email, drop_column users.note, unsupported_trigger users.tg: not modelled, 같은 set과 set 밖 객체는 차이 없음).——[o]
T43.5-3-12Python client에 Dbspec.addTablesAndColumnsSteps를 추가한다. 원인: TypeScript client는 live document와 대상으로부터 upgrade의 추가 전용 step을 얻고, 추가할 수 없는 차이를 알린다. 완료 기준: 같은 입력에서 TypeScript client와 같은 추가 이름, step, 차이를 돌려주고, 추가와 거부된 변경을 단위 test가 확인한다. Red: 깨끗한 환경(env -u PYTHONPATH, 새 TMPDIR)에서 python -m unittest test_add_tables_steps가 종료 코드 1로 끝났다. ImportError: cannot import name 'add_tables_and_columns_steps' from 'polyspec.orm.dbspec'다. Green: polyspec/orm/dbspec/add_tables.py의 add_tables_and_columns_steps가 생긴 뒤, python -m unittest test_add_tables_steps test_installed_differences가 종료 코드 0으로 끝나고 test 8개가 OK이며 항목을 0개 남긴다. 추가된 이름은 note와 users.email이고, null이나 기본값이 없는 column의 거부 문장은 TypeScript client의 것이며, SQLite 재구성의 문장 13개는 같은 입력에서 TypeScript client가 쓰는 문장과 같다.——[o]
T43.5-4features, version, check, CI group에 python을 선언한다. 원인: contracts/features.json, make version-check의 DECLARATIONS, contracts/check-inputs.json, scripts/check/ci-setup.mjs가 client 언어를 나열한다. 완료 기준: 이 선언들이 python을 담고, 해당 검사가 통과하며, CI group python이 Python 검사를 실행한다.—근거: T43.5-4-1(features.json과 docs/features.md가 python을 선언하고 생성되며, make docs-check가 0을 반환한다)과 T43.5-4-2(version 선언, check 입력, CI group python)가 닫혔다.[o]
T43.5-4-1features.json과 docs/features.md에 python을 통과한 검사의 근거가 있는 만큼만 선언한다. 원인: contracts/features.json과 생성기 scripts/features/build.mjs가 클라이언트 언어로 go, php, rust, typescript만 나열한다. 완료 기준: 통과한 검사가 근거를 주는 기능에서만 python을 나열하고, scripts/features/build.mjs가 docs/features.md와 docs/features.ko.md를 손으로 고치지 않고 다시 만들며, make docs-check가 0을 반환한다.—근거: contracts/features.json은 기능 18개 모두에서 python을 planned로 선언한다. 기능별 python 검증이 실행된 적이 없으므로 pass를 주장하지 않으며, scripts/features/build.mjs와 check.mjs는 planned 기능의 검증 명령을 실행하지 않는다. scripts/features/build.mjs가 python을 나열하고, node scripts/features/build.mjs가 docs/features.md와 docs/features.ko.md를 다시 만들며, make docs-check가 0을 반환한다. make feature-unit-check/docs와 make feature-unit-check/select가 0을 반환한다. make feature-unit-check/coverage와 make feature-unit-check/owners는 깨끗한 HEAD에서도 같은 실패로 2를 반환한다(cargo가 없고 DB 상태 리더가 서버 없이 실행됨). make feature-check는 .runtime/servers/env가 없어 여기서 실행할 수 없다(make test-servers가 DB 서버를 띄운다). 이것들은 환경 실패이며, 여기서 통과로 주장하지 않는다.[o]
T43.5-4-2version 선언, 검사 입력, CI group에 python을 선언한다. 원인: make version-check의 version DECLARATIONS, contracts/check-inputs.json, scripts/check/ci-setup.mjs가 클라이언트 언어를 각각 나열한다. 완료 기준: 이 선언들이 python을 나열하고, make version-check와 그에 막힌 검사들이 0을 반환하며, CI group python이 Python 검사를 실행한다.—근거: T43.5-4-2-1(make version-check가 clients/python/pyproject.toml을 읽고 선언 31개가 0.0.4로 일치)과 T43.5-4-2-2(check-inputs가 python-check를 선언하고 CI group python이 실행)가 닫혔다. make version-check, make python-check, make repo-check가 0을 반환한다.[o]
T43.5-4-2-1version 검사에 python 버전을 선언한다. 원인: make version-check는 client package의 version을 읽는데(scripts/version/check.mjs DECLARATIONS), clients/python/pyproject.toml은 그 목록에 없다. 완료 기준: DECLARATIONS가 clients/python/pyproject.toml의 version을 읽고, make version-check가 python 선언을 세어 0을 반환한다.—근거: DECLARATIONS가 clients/python/pyproject.toml의 version(version = "0.0.4")을 읽고, make version-check가 version: 31 declarations agree on 0.0.4를 출력하며 0을 반환한다. node --test scripts/version/check.test.mjs가 0을 반환한다.[o]
T43.5-4-2-2check 입력과 CI group에 python을 선언한다. 원인: contracts/check-inputs.json이 make check의 target과 경로를 나열하고, Makefile의 CI_GROUPS와 scripts/check/ci-setup.mjs에는 python group이 없다. 완료 기준: python 검사마다 contracts/check-inputs.json에 target과 입력이 있고, CI group python이 Python 3.11로 그것들을 실행하며, make checklist-check와 group 검사가 0을 반환한다.—근거: make python-check(clients/python/tests의 Python 단위 테스트, tests/interfaces의 interface 테스트, conformance 결과 검사, 생성 model 검사를 실행하는 Makefile target)가 테스트 70, 12, 7개 OK로 0을 반환한다. contracts/check-inputs.json이 Python 입력에 대해 python-check(owner scope, needs python)를 선언하고, CI_GROUPS와 CI_TARGETS_python이 group python을 나열한다. scripts/check/ci-setup.mjs가 setup 단계 python과 python-install을 대응시키고, .github/workflows/ci.yml은 그 group에서 setup-python으로 .python-version을 쓰고 make python-install을 실행한다. make repo-check와 make feature-unit-check가 0을 반환한다. make check GROUP=python은 GitHub Actions에서만 실행되므로(Makefile 189행) group 자체는 여기서 실행하지 않는다. 환경: 이 변경 이전에 repo-check와 feature-unit-check는 cargo가 PATH에 없어(rustup의 cargo bin 디렉터리), Go 1.27.1이 설치돼 있는데 .go-version은 1.27.0을 선언하고, Node v26.7.0이 실행되는데 .node-version은 26.8.1을 선언해서 2를 반환했다. 해결은 PATH와 Go toolchain(GOTOOLCHAIN=go1.27.0으로 go1.27.0을 내려받음)이며 저장소 밖에서 한 것이다. 범위: scripts/repo/scripts.mjs는 Python client의 runner(clients/python, Python interface 테스트, Python conformance와 dbspec runner)를 도구 언어 규칙에서 면제한다. 명세가 Python 구현을 Python runner로 시험한다고 정하기 때문이며, 다른 Python 프로그램은 금지된 채로 남는다. scripts/features/owners.test.mjs, scripts/repo/check.test.mjs, contracts/python_signature_test.go(testcase.Start)의 기대값은 새 need와 출력을 따른다.[o]
T43.6conformance vector와 CI job을 돌린다. 원인: database에 의존하는 기능은 모든 client에서 세 database의 실행 증거를 요구한다. 완료 기준: python runner가 각 database 기대 file의 26 vector를 실행하고 다른 client와 일치한다; CI job python이 3.11과 .python-version의 version에서 Python 검사를 돌린다. 막힘: 하위 T43.6-3은 ordered-json checkout에 막혀 있다(CI conformance 그룹과 python job이 형제 ordered-json을 import하는데 ci.yml에는 그것을 checkout하는 단계가 없다). 하위 T43.6-1은 같은 checkout과, 이 머신이 할 수 없는 3.11 실행에 막힌 것으로 기록했다. 재시도: 소유자가 ordered-json ref를 정하고 ci.yml에 checkout 단계가 생긴 뒤, CI python job을 3.11과 .python-version에서 실행하고 conformance 그룹의 python MySQL과 PostgreSQL 행을 실행한다. 그다음 T43.6-1, T43.6-3과 이 부모를 닫는다. 증거(로컬): 모든 child가 끝났다. 데이터베이스 row는 push 뒤 CI job이 확인하고, 거기서 실패하면 새 행이 된다.——[o]
T43.6-1Python 검사를 .python-version의 version과 나란히 Python 3.11에서 CI matrix leg로 실행한다. 원인: CI group python은 .python-version(3.14) 하나만 준비하므로 requires-python >= 3.11의 최저 release가 CI에서 실행되지 않았다. 완료 기준: .github/workflows/ci.yml이 group python을 3.11과 .python-version에서 실행하고, scripts/check/ci-setup.mjs가 두 leg를 적으며, make repo-check가 0을 반환한다. Red: 단위 test the python group runs on Python 3.11 and on .python-version(scripts/repo/check.test.mjs)은 leg가 생기기 전에 실패한다. 막힘 해제: T43.7-11의 ordered-json checkout(commit 3eb575bc)이 ci.yml에 있고 tag v0.0.4가 존재한다.—증거: 단위 test는 커밋된 tree에서 통과한다. 3.11 leg의 CI(job test (python, _py311, 3.11))는 대기 중이다. 증거(로컬): make repo-check/unit이 the python group runs on Python 3.11 and on .python-version test와 함께 0으로 끝나고 make repo-check가 0으로 끝난다.[o]
T43.6-1-1python group의 보고서 run id와 path에 matrix leg suffix를 선언합니다. Python 3.11 leg(_py311)가 같은 group python을 다른 leg와 공유하므로, ORM_CHECK_RUN_ID, 보고서 path, artifact 이름에 ${{ matrix.leg }}가 필요합니다. scripts/repo/ci.mjs(GROUP_RUN_ID, AFTER_GROUP_CHECK)는 group만 선언해서 "a workflow runs only the summary and the report upload after make check"가 실패합니다. Red: scripts/repo/check.test.mjs의 기대 4개가 group만 씁니다. Green: ${{ matrix.leg }}를 넣은 선언.—검증: node --test --test-name-pattern="make check|report|summary" scripts/repo/check.test.mjs, 그다음 CI (static). 증거(로컬): make repo-check/unit이 ${{ matrix.leg }}를 적는 expectation과 함께 0으로 끝나고 make repo-check가 0으로 끝난다.[o]
T43.6-2Python runner로 SQLite 벡터 26개를 실행하고 Go 출력과 맞춘다. 원인: conformance 검사는 클라이언트 4개를 비교하는데, Python runner가 SQLite 기대 파일에 대해 기록된 실행이 없었다. 완료 기준: SQLite bench에서 Python runner가 벡터 26개로 0을 반환하고, 출력이 같은 DB의 Go 출력과 같으며, 명령과 exit code가 기록된다.—근거: scripts/bench-db.sh sqlite로 seed한 SQLite bench에서 Python runner가 0을 반환하고 벡터 26개를 내며, 출력 65,854바이트가 저장된 Go 출력 go-full.json과 바이트 단위로 같다(커밋 10a6922a에서 실행했고, 이 커밋에서도 같은 model로 다시 실행해 cmp가 0을 반환했다).[o]
T43.6-3Python runner의 MySQL과 PostgreSQL 벡터 행을 실행한다. 원인: MySQL과 PostgreSQL DB는 test server가 필요하고, 이 머신에서는 make test-servers가 ProxySQL과 PgBouncer를 시작하지 못한다(ProxySQL은 Homebrew formula가 없고, 설정에 필요한 Homebrew tap은 신뢰되지 않았다). 완료 기준: 각 행은 이 머신에서 CI 전용이라고 적고, CI job python이 make test-servers와 같은 server 목록을 쓰는 Linux runner에서 그 행을 실행한다. feature-check도 같은 로컬 한계와 같은 CI 증거를 가진다. 막힘, 통과 아님. 원인: 이 머신에서 MySQL과 PostgreSQL 벡터 행과 make feature-check는 CI에서만 실행된다. make test-servers는 Homebrew formula가 없는 ProxySQL을 시작하지 못하고, Homebrew 설정은 소유자만 신뢰할 수 있는 tap에 막혀 있다(brew trust gcenx/wine은 여기서 실행하지 않았다). CI conformance 그룹은 두 드라이버에서 conformance-check/run을 python 런너로 실행하지만, conformance-python-models는 PYTHONPATH=../ordered-json/python/src로 orm-gen을 실행하고 .github/workflows/ci.yml은 ordered-json 형제 checkout을 받지 않는다. 그래서 Python 행은 CI에서도 통과할 수 없으며, T43.5-2-4가 python을 추가한 뒤로 conformance 그룹이 영향을 받는다. 재시도: ci.yml에 소유자가 정한 ref(명세는 tag v0.0.4)로 ordered-json을 checkout하는 단계를 넣은 뒤, CI conformance 그룹의 python MySQL과 PostgreSQL 행이 증거이며, 그 CI 실행으로 이 항목을 닫는다. 재개: 재시도 조건이 성립한다. ci.yml이 tag v0.0.4의 ordered-json을 checkout한다(commit 3eb575bc). CI 실행 37803770504(commit fcebb854)가 Python 행의 실패를 보였고, 하위 항목이 원인별로 적는다. 모든 하위 항목이 끝나고 최종 head의 conformance job이 MySQL과 PostgreSQL의 Python 행을 통과할 때 이 행을 닫는다. 증거(로컬): 로컬—증거: 모든 child가 끝났고 go test ./tests/conformance/check와 make python-check가 0으로 끝난다. MySQL과 PostgreSQL의 Python row는 push 뒤 CI conformance job이 실행하고, 거기서 실패하면 새 행이 된다.[o]
T43.6-3-1CI 실행 37803770504가 보고한 MySQL과 PostgreSQL Python 행의 decimal 원인을 기록한다. 원인: Python runner가 집계 read_total(read_count의 SUM)의 decimal cell을 decimal.Decimal로 받았다. job test (conformance)(113403210873)에서 conformance/mysql/python이 ValueError: invalid derived integer로 실패했고(tests/conformance/runner_python.py 172행), physical result 검사가 TestPhysicalResultRunners/postgres/python을 OrmError: CODEC_DECODE: decimal cell has type Decimal로 실패했다. 완료 기준: 같은 decimal cell을 정확한 문자열로 돌려주어(T43.7-19, commit b33f994e) MySQL과 PostgreSQL의 Python 행이 최종 head의 conformance job에서 통과한다. Red: commit fcebb854의 실행 37803770504에 있는 위 두 줄. Green: 최종 head의 conformance job이 conformance/mysql/python, conformance/postgres/python, TestPhysicalResultRunners/*/python을 통과한다. 증거(로컬): make python-check가 DECIMAL과 NUMERIC cell을 정확한 문자열로 돌려주는 test_driver_cells.py와 함께 0으로 끝난다.——[o]
T43.6-3-2conformance 검사가 한 언어가 실패한 뒤에도 database의 모든 언어와 모든 database를 실행한다. 원인: runDatabase가 must로 첫 실패 언어에서 멈추어, CI 실행 37803770504(commit fcebb854)의 conformance/mysql/python이 PostgreSQL 행보다 먼저 MySQL database를 멈추었고, 실패한 database가 뒤의 database를 멈추었다. AGENTS는 모든 언어를 실행하고 각 실패를 적을 것을 요구한다. 완료 기준: runLanguages가 실패 뒤에도 database의 모든 언어를 실행하고, runDatabase가 오류를 돌려주어 다음 database가 실행되며, 실행은 실패한 database와 언어를 모두 적고 실패한다. Red: tests/conformance/check/main_test.go의 TestRunLanguagesRunsEveryLanguageAfterAFailure가 첫 실패에서 멈춘 구현에서 ran [go php]로 실패한다. Green: 같은 test가 통과한다.—증거: 단위 test는 커밋된 tree에서 통과한다. 최종 head의 CI conformance job은 대기 중이다. 증거(로컬): go test ./tests/conformance/check -run TestRunLanguages가 0으로 끝난다(TestRunLanguagesRunsEveryLanguageAfterAFailure 통과).[o]
T43.6-3-4TestRunLanguagesRunsEveryLanguageAfterAFailure를 testcase.Start로 시작합니다. tests/conformance/check의 모든 Go test가 그렇게 합니다. 원인: T43.6-3-2가 추가한 test에 case가 없어서 static job의 저장소 검사가 "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"가 이 test를 지목합니다. Green: case 시작.—검증: node --test --test-name-pattern="every Go and Rust test starts its case" scripts/repo/check.test.mjs, 그다음 CI (static). 증거(로컬): make repo-check/unit이 every Go and Rust test starts its case with the shared testcase package test와 함께 0으로 끝난다.[o]
T43.6-3-3Python client의 utility 문장이 PostgreSQL의 $n marker를 쓰게 합니다. 원인: utils.py가 PostgreSQL advisory lock과 set_config에 엔진이 쓰지 않는 ? marker를 썼고, 그 문장이 바인딩 없이 driver에 전달되었습니다(conformance/postgres/python에서 "the statement has no markers for 1 values"). Red: clients/python/tests/test_utils_statements.py가 존재하지 않는 POSTGRES_ADVISORY_LOCK과 POSTGRES_SET_LOCAL을 import합니다. Green: $1과 $2를 쓰는 상수, utility 메서드가 사용.—검증: test_utils_statements.py, 그다음 CI (test (conformance)). 증거(로컬): packages/orm-python/tests/test_utils_statements.py가 0으로 끝나고 make python-check가 0으로 끝난다.[o]
T43.7Python client: CI 실행 후속 작업 (T43.7-1부터 T43.7-19까지의 그룹)——[o]
T43.7-1clients/python/pyproject.toml의 package-data 키를 polyspec.orm으로 바꾼다. 원인: 키 orm은 어떤 package도 가리키지 않아서 setuptools가 polyspec.orm의 wheel에 py.typed를 넣지 않는다. 완료 기준: [tool.setuptools.package-data]의 모든 키가 clients/python/src 아래 __init__.py가 있는 package를 가리키고, 키 polyspec.orm이 py.typed를 담는다. Red: 새 test clients/python/tests/test_packaging.py가 키 orm에서 실패한다. Green: 이 test가 통과하며, 이 항목에서 실행하는 유일한 test다.——[o]
T43.7-2scripts/release/release.mjs에서 clients/python/pyproject.toml을 분류한다. 원인: 이 file은 MANIFESTS와 맞지만 RELEASED와 NOT_RELEASED 어느 쪽에도 없어서 unlistedManifests가 보고한다. 완료 기준: file이 NOT_RELEASED에만 들어가며, 이유는 Rust crate 항목과 같은 표현으로 쓴다. 즉 Python client는 git tag로 소비되고 release archive가 없다. Red: scripts/release/release.test.mjs의 every tracked manifest is released or listed as not released with a reason test가 이 file에서 실패한다. Green: 그 test가 통과한다.——[o]
T43.7-3blocked T43 행의 상태를 commit bcfb1a5a와 맞춘다. 원인: 그 commit message "Mark the blocked T43 rows as bypassed"는 T43, T43.5, T43.5-5, T43.6-1, T43.6을 적지만, diff는 T43, T43.5, T43.6-1만 진행 중에서 우회 상태로 바꾸고 T43.6-3은 적지 않는다. 완료 기준: CHANGELOG 항목이 각 행의 실제 상태를 적고, checklist는 T43, T43.5, T43.5-5, T43.6, T43.6-1, T43.6-3에 원인과 재시도 조건을 붙인 우회 상태를 둔다. 문서만 바꾼다. Green: node scripts/checklist/check.mjs와 node scripts/docs/rules.mjs가 통과한다.——[o]
T43.7-4AGENTS.md와 docs/operations/packaging.md에 Python release 형식을 적는다. 원인: AGENTS.md의 Releases와 docs/operations/packaging.md는 Python client가 어떻게 release되는지 적지 않는다. 완료 기준: 두 문서와 그 .ko.md 짝이 Python client는 git tag로 소비되고 release archive가 없으며 version 올림 pull request가 clients/python/pyproject.toml을 설정한다고 적는다. Green: node scripts/docs/rules.mjs가 통과하고, 영어와 한국어 문서가 같은 사실을 적는다.——[o]
T43.7-5Python client의 ordered-json 의존성을 release에 고정한다. 원인: polyspec-ordered-json이 없는 tag에 고정되어 있어서 Python test가 sibling checkout에서 ordered-json을 import했다. 완료 기준: packages/orm-python/pyproject.toml이 ordered-json@v0.0.7#subdirectory=packages/ordered-json-python을 가리키고, Makefile, ci.yml, Python test가 packages/ordered-json-python/src를 읽는다.—Evidence: make python-check가 0으로 끝난다.[o]
T43.7-6docs/checklist.ko.md의 T43.7-3 행에서 금지 표현을 뺀다. 원인: CI의 docs-rules 검사가 T43.7-3 한국어 행의 낱말 하나를 금지 표현으로 거부해서, commit 08093f71에서 static 작업이 실패했다. 완료 기준: 한국어 행이 같은 사실을 그 낱말 없이 적고, 영어와 한국어 행이 같은 상태와 같은 내용을 유지한다. Red: node scripts/docs/rules.mjs가 그 낱말이 있는 docs/checklist.ko.md에서 실패한다. Green: node scripts/docs/rules.mjs가 통과한다.——[o]
T43.7-7Makefile에서 python-install을 오프라인 규칙 안에 둔다. 원인: CI의 static 작업이 the Makefile runs every check offline and downloads only in the install targets test에서 실패한다. python-install이 python3 -m venv와 pip install에 $(ONLINE)을 쓰는데, 이 규칙의 install target이 아니기 때문이다. 완료 기준: $(ONLINE)을 쓰는 recipe가 규칙이 정한 install target이다. 그 목록을 바꾸려면 먼저 AGENTS.md와 test에서 이유와 함께 바꾼다. Red: test가 python-install의 두 줄에서 실패한다. Green: 규칙이 정한 목록으로 test가 통과한다. 증거(로컬): make runner-check가 the Makefile runs every check offline and downloads only in the install targets test와 함께 0으로 끝난다. python-install은 install-python이다.——[o]
T43.7-8physical result runner에 python runner를 등록한다. 원인: tests/conformance/check/result_physical_test.go의 TestPhysicalResultRunners는 go, php, rust, typescript, python 언어를 요구하지만 physical runner는 go, php, rust, typescript뿐이어서, commit 08093f71에서 CI conformance 작업이 실패했다. 완료 기준: physical result runner에 python이 들어가고 test가 통과한다. Red: TestPhysicalResultRunners가 두 목록으로 실패한다. Green: TestPhysicalResultRunners가 통과한다. 증거(로컬): go test ./tests/conformance/check와 go vet -tags physical ./tests/conformance/check가 0으로 끝난다. resultRunnerLanguages가 python을 담는다.——[o]
T43.7-9dbspec 비교를 실행하는 feature 작업에 Python client interpreter를 만든다. 원인: 작업 feature_verify_other는 features/schema_definition/schema-compare를 실행하고, 이 단계는 python-venv-check를 거쳐 clients/python/.venv가 필요하다. 그런데 그 작업의 setup이 python-install을 실행하지 않아서, commit 08093f71에서 status 2로 끝난다. 완료 기준: 그 작업의 setup이 interpreter를 만들거나, feature가 interpreter가 필요하다고 선언하며, schema-compare가 그 작업에서 통과한다. Green: 작업 feature_verify_other가 features/schema_definition/schema-compare를 통과한다. 증거(로컬): make feature-unit-check가 0으로 끝나고(feature-verify-other가 contracts/check-inputs.json에 need python을 선언한다) schema-compare의 명령인 make dbspec-compare-check가 0으로 끝난다.——[o]
T43.7-10Rust catalog case native_updates_lock_compare_verify_and_rollback를 probe 시점과 무관하게 만든다. 원인: 작업 feature_verify_rust가 commit 78499c68에서 commit completed before detached-owner probe: true로 실패했다. 같은 작업은 commit 08093f71에서 통과했고, 그 사이 commit들은 Rust source를 바꾸지 않았다. 완료 기준: 그 case가 commit 순서와 probe 순서를 event나 결정적 검사로 정하며, CI 실행마다 작업에서 통과한다. Red: 작업에서 위 message로 case가 실패한다. Green: 수정을 담은 commit의 CI 실행에서 feature_verify_rust의 case가 통과한다. 증거(로컬): packages/orm-rust/orm-build/tests/row_mutation.rs의 tokio::select!에 biased;를 두어 probe event가 첫 branch가 되므로 순서가 무작위 선택에 기대지 않는다. make rust-check/check, make rust-check/clippy-live-db, make rust-fmt-check가 0으로 끝난다. case 자체는 push 뒤 CI의 feature_verify_rust가 실행한다.——[o]
T43.7-11CI의 python group에서 ordered-json을 릴리스 v0.0.4로 checkout합니다. Python 검사는 ../ordered-json에서 polyspec.ordered_json을 import하는데(PYTHONPATH), ci.yml에 이를 checkout하는 단계가 없어서 group이 ModuleNotFoundError로 실패합니다. python group이 실행될 때만 polyspec/ordered-json을 ref v0.0.4로 ../ordered-json에 checkout합니다.—검증: CI python group (test (python)). 증거(로컬): make repo-check가 0으로 끝난다: ci.yml이 python group에서 polyspec/ordered-json을 ref v0.0.7로 ordered-json에 checkout한다.[o]
T43.7-12scripts/check/ci-setup.mjs에 ordered-json checkout 단계를 python 필요 조건과 함께 선언합니다. 이 선언이 없으면 다른 group에서 단계가 실패할 때 check runner가 "CI step ordered-json failed and declares no need"로 중단합니다.—검증: CI (test (static)). 증거(로컬): make repo-check와 make runner-check가 0으로 끝난다: ci-setup.mjs가 step ordered-json을 need python에 대응시킨다.[o]
T43.7-13ordered-json checkout을 workspace 안에 둡니다. Actions는 workspace 밖 checkout 경로(../ordered-json)를 거부하므로, checkout 경로는 ordered-json이고 변수 ORDERED_JSON이 이를 가리킵니다(ci.yml이 설정하며, Makefile 기본값은 sibling checkout).—검증: CI python group (test (python)). 증거(로컬): make repo-check가 0으로 끝난다: checkout 경로는 workspace 안의 ordered-json이고 ci.yml이 ORDERED_JSON을 정한다.[o]
T43.7-14physical result 검사에 Python interpreter와 읽을 model을 줍니다. TestPhysicalResultRunners/DB/python이 "ORM_PYTHON and ORM_PYTHON_MODELS are unset"으로 실패합니다. conformance-result-physical-check가 이 변수를 설정하지 않고 model도 만들지 않기 때문입니다. target이 conformance-python-models에 의존하고, conformance-check처럼 ORM_PYTHON과 ORM_PYTHON_MODELS를 넘깁니다.—검증: CI (test (conformance)). 증거(로컬): make conformance-python-models와 make repo-check가 0으로 끝난다. conformance-result-physical-check는 conformance-python-models에 의존하고 ORM_PYTHON과 ORM_PYTHON_MODELS를 준다.[o]
T43.7-15T43.7-14 행의 데이터베이스 subtest를 꺾쇠 괄호 자리표시자 대신 DB로 적습니다. 문서 build가 꺾쇠 괄호 자리표시자를 HTML 요소로 읽어 "Element is missing end tag"로 실패합니다.—검증: CI (docs). 증거(로컬): make docs-check와 make docs-rules-check가 0으로 끝난다.[o]
T43.7-16ORM_PYTHON으로 Python client interpreter를 실행하는 모든 명령에 PYTHONPATH를 넘깁니다. dbspec-compare-check, conformance-check, conformance-result-physical-check의 runner가 PYTHON_PATH 없이 interpreter를 띄워서 polyspec.ordered_json이 없어지고, conformance와 feature-verify-other job이 실패합니다.—검증: CI (test (conformance), test (feature_verify_other)). 증거(로컬): make dbspec-compare-check와 make repo-check가 0으로 끝난다. 명령은 ORM_PYTHON과 함께 PYTHONPATH=$(PYTHON_PATH)를 준다.[o]
T43.7-17dbspec-compare-check의 inputs 단위 실행에 PYTHONPATH와 ORM_PYTHON을 넘깁니다. tests/dbspec/inputs.test.mjs가 Python runner를 실행하는데 이 runner는 ORM_PYTHON이 필요하고, 변수가 없으면 rejects case가 "ORM_PYTHON names no Python interpreter"로 실패합니다.—검증: CI (test (feature_verify_other)). 증거(로컬): make dbspec-compare-check가 0으로 끝난다(inputs unit run이 PYTHONPATH와 ORM_PYTHON을 받는다).[o]
T43.7-18엔진 SQL의 placeholder를 Python 드라이버 형식으로 바꿉니다. 엔진은 MySQL에 ?, PostgreSQL에 $n을 쓰는데, PyMySQL과 psycopg는 %s와 %(name)s를 요구합니다. 그래서 MySQL과 PostgreSQL vector가 "not all arguments converted"와 "the query has 0 placeholders"로 실패합니다. 변환은 clients/python/src/polyspec/orm/placeholders.py에 있고, 단위 테스트는 clients/python/tests/test_placeholders.py입니다.—검증: python3 clients/python/tests/test_placeholders.py, 그다음 CI (test (conformance)). 증거(로컬): make python-check가 test_placeholders.py와 함께 0으로 끝난다.[o]
T43.7-18-1Python client에서 다른 module이 import하는 helper를 private으로 만듭니다. 원인: CI의 interface 검사(job test (feature_verify_rust), 단계 interfaces/python, 실행 37803770504, commit fcebb854)가 placeholders.py::for_mysql과 placeholders.py::for_postgres를 허용되지 않은 public 이름 목록에 보고했다. 같은 부류로 driver_cells.py::mysql_conversions와 driver_cells.py::postgres_loaders가 있으며, 이는 T43.7-19가 client에 더한 것이고 contracts/symbols/python.json에 없다. 완료 기준: 네 helper의 이름이 _로 시작하고, client의 public 이름이 contracts/symbols/python.json의 이름과 같다. Red: tests/interfaces/test_python_extractor.py의 test_public_names_match_the_symbol_file가 네 이름 모두에서 실패한다. Green: 같은 test가 통과하고, 그 단위 실행이 이 변경의 유일한 실행이다. CI job test (feature_verify_rust)가 최종 head에서 interfaces/python을 통과한다. 증거(로컬): tests/interfaces/test_python_extractor.py(make python-interface-unit-check)가 test_public_names_match_the_symbol_file과 함께 0으로 끝난다. T43.7-18-2가 네 helper를 다시 공개 이름 목록에 등록했다.——[o]
T43.7-18-2다른 module이 import하는 Python client helper를 contracts/symbols/python.json에 등록하고, T43.7-18-1의 private 이름 변경을 되돌립니다. 원인: T43.7-18-1은 symbol snapshot 검사를 통과하려고 helper를 private으로 만들었습니다. database.py가 placeholders.py와 driver_cells.py에서 import하는 module 수준 이름은 공개 이름 목록에 등록하는 것이 맞습니다. Red: 네 이름에 대해 test_public_names_match_the_symbol_file이 실패합니다. Green: 등록 항목.—검증: clients/python/tests/test_placeholders.py, test_driver_cells.py, 그다음 CI (test (feature_verify_other)). 증거(로컬): make python-check와 make python-interface-unit-check가 0으로 끝나고 make interface-check가 0으로 끝난다.[o]
T43.7-18-3T43.7-18-2가 contracts/symbols/python.json을 바꾼 뒤 Python client의 symbol hash를 contracts/interfaces.json에 기록합니다. 원인: symbol_hashes.python이 네 공개 helper를 추가하기 전 파일을 가리켜서 conformance-check와 feature-verify-rust가 "symbol snapshot hash differs"로 실패합니다. Red: 옛 hash에서 Python client의 interface 검사가 실패합니다. Green: 현재 파일의 hash.—검증: CI (test (conformance), test (feature_verify_rust)). 증거(로컬): make interface-check가 현재 contracts/symbols/python.json의 symbol hash로 0으로 끝난다.[o]
T43.7-19Python 드라이버의 DECIMAL과 NUMERIC cell을 정확한 문자열로 돌려줍니다. PyMySQL과 psycopg는 decimal.Decimal을 주는데 코덱이 이를 읽지 못합니다("decimal cell has type Decimal", postgres). TypeScript와 PHP 클라이언트는 문자열을 받습니다. 변환은 clients/python/src/polyspec/orm/driver_cells.py에 있고 database.py에 연결됩니다. Red: clients/python/tests/test_driver_cells.py가 모듈이 없어 ModuleNotFoundError로 실패했습니다.—검증: clients/python/.venv/bin/python clients/python/tests/test_driver_cells.py, 그다음 CI (test (conformance)). 증거(로컬): make python-check가 test_driver_cells.py와 함께 0으로 끝난다.[o]
T44공유 도구 저장소 polyspec/kit v0.0.9를 씁니다. 원인: 저장소의 push 검사, 문서·commit 검사, release와 dependency 도구는 polyspec/kit이 소유한 도구의 사본이고, package는 다른 저장소가 쓰는 packages/<repository>-<language>/가 아니라 clients/에 있습니다. 완료 기준: scripts/kit, tests/kit, kit.json, .kit/kit.lock.json이 kit v0.0.9의 vendored 사본이고 여기서 고치지 않습니다. 저장소는 config/*.json과 자체 product code에서만 다른 저장소와 다르고, T44.1–T44.4이 완료됩니다.—Evidence: make kit-sync KIT_TAG=v0.0.9가 unchanged를 출력하고, make kit-check, make kit-test, make documents-check, make dependency-policy-check가 0으로 끝납니다. 우회: T44.4는 0.0.5의 version을 올리는 commit이 필요합니다. 재시도: release 0.0.5를 준비합니다.[o]
T44.1clients/<directory>를 go, npm(TypeScript), php, php-extension, python, rust마다 packages/orm-<kind>로 옮깁니다. 원인: 다른 저장소의 package는 packages/<repository>-<kind>/에 있고, release, owner, dependency 설정이 그 directory를 씁니다. 완료 기준: 여섯 directory가 history와 함께 새 경로에 있고, 모든 참조(Makefile, workflow, script, test, contracts/, 문서, manifest)가 새 경로를 쓰며, 언어와 directory의 대응은 scripts/features/packages.mjs이고, Go module github.com/polyspec/orm은 module path 그대로 저장소 root에 남습니다.—Evidence: git grep 'clients/'가 이전 변경의 기록만 출력하고, 옮긴 경로를 읽는 unit 검사(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.2kit을 vendor하고 push 검사, 전체 실행 guard, 문서·commit 검사, release 단계, crate download 검사, ruleset의 workflow와 이 도구들의 Make target을 kit으로 바꿉니다. 원인: scripts/check/push-gate.mjs, hooks.mjs, ci-passed.mjs, full-run.mjs, scripts/checklist, scripts/git, scripts/github, scripts/release, scripts/check/downloads.mjs의 crate 부분은 kit 도구의 사본이고, 0.x에는 pull request, merge queue, ruleset이 없습니다. 완료 기준: scripts/kit, tests/kit, kit.json, .kit/kit.lock.json이 kit v0.0.9의 vendored 사본이고(make kit-sync KIT_TAG=v0.0.9가 unchanged를 출력), Makefile이 scripts/kit/kit.mk를 include하며, checklist가 docs/plans/execution/checklist.md(와 .ko.md)에 있고, config/checklist.json, commits.json, documents.json, release.json, toolchain.json, owner-checks.json이 있으며, 사본과 그 test와 .github/ruleset.json이 사라지고, make check는 kit의 guard이고 make check-run은 이 저장소의 runner이며 그 기록은 scripts/check/record.mjs(.runtime/check/<run id>/record.json)이고, workflow가 main으로의 push와 수동 실행에서 돌며, AGENTS.md와 AGENTS.ko.md가 0.x workflow, 도구의 소유, 배치를 적고, 모든 문서 쌍에 doc-id와 source-sha256 marker가 있습니다.—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, make docs-check가 0으로 끝나고, make documents-check가 통과합니다.[o]
T44.2-1make documents-check가 site 문서의 heading anchor를 읽게 합니다. 원인: 문서는 VitePress가 쓰는 anchor(#_1-5-restore)와 한국어 heading이 {#external-documents}로 선언하는 id로 link하는데 kit v0.0.4는 그것을 읽지 않았습니다(21개 anchor로 가는 link 74개). 완료 기준: siteLinks가 설정되면 kit v0.0.9가 VitePress slug와 {#id}를 읽습니다.—Evidence: make documents-check가 0으로 끝납니다.[o]
T44.3dependency 정책을 선언하고 dependency review를 기록합니다. 원인: 저장소에는 dependency 검사가 없었습니다. 완료 기준: config/dependency-policy.json이 root composer.json(PHP 8.4.0), Python manifest, typescript 6(typescript 7에는 JavaScript API가 없다)과 @types/node 22(engines.node >=22.16.0)의 예외를 선언하고, config/toolchain.json이 cargoAudit와 govulncheck를 선언하며, dependency는 최신 stable release이고 config/dependency-review.json이 review를 기록합니다.—Evidence: make dependency-policy-check와 make dependency-policy-mutation-check가 0으로 끝나고 둘 다 CHECK_TARGETS의 target입니다.[o]
T44.4release archive의 consumer를 선언합니다. 원인: kit은 tag의 archive를 commit한 manifest와 lock(config/release.json의 consumers)으로 깨끗한 npm·Composer project에 설치합니다. 완료 기준: config/release.json이 @polyspec/orm과 polyspec/orm의 smoke 명령을 담은 consumers를 선언하고, tests/release-consumer/npm/package.json과 tests/release-consumer/composer/composer.json이 manifest를 담으며, 둘 다 notReleased에 있습니다.—Evidence: make release-coverage, make kit-check, make release-versions TAG=v0.0.4가 0으로 끝납니다. lock은 release version의 archive의 이름을 적으므로 0.0.5의 release commit이 쓰고(local tag에서 make release-assets와 make release-consumer-lock, 그다음 tag 삭제) make release-consumer를 실행합니다. 그 commit은 또 main의 CI에서 conformance와 feature job의 데이터베이스 row가 통과하기를 요구한다.[o]
T45b278092f의 CI 실행 37858088534에서 실패한 job 세 개를 고칩니다. 원인: test (static), test (feature_coverage), test (clients_db)가 실패했습니다. 완료 기준: T45.1–T45.3이 끝납니다.—Evidence: T45.1–T45.3이 끝났다.[o]
T45.1job test (static)에서 kit-test가 임시 항목을 남기지 않게 합니다. 원인: check runner는 단계마다 자기 TMPDIR을 주고 항목을 남긴 통과 단계를 실패로 봅니다. kit v0.0.9의 tests/kit/release-consumer-sandbox.mjs는 npm install --package-lock-only마다 inspect에서 folder stub-npm-*을 만들고 지우지 않아서 check/kit-test가 "left 112 temporary entries"로 실패합니다(로컬의 node scripts/check/run.mjs - kit-test도 같은 112개). 완료 기준: kit v0.0.10이 folder를 지운다.—Evidence: make kit-sync KIT_TAG=v0.0.10의 두 번째 실행이 unchanged를 출력하고, make kit-check가 0으로 끝나며, node scripts/check/run.mjs - kit-test가 0으로 끝난다(PASS check/kit-test, 남은 임시 항목 없음).[o]
T45.2TestCoverageModelGenerationCheck가 Go client를 새 경로로 복사하게 합니다. 원인: test가 module 복사본에서 clients/go/model을 join하는데 T44.1이 그것을 packages/orm-go/model로 옮겨서, test (feature_coverage)가 "open .../clients/go/model/author.go: no such file or directory"로 실패했습니다. 완료 기준: test가 packages/orm-go/model을 join합니다. Red: go test -tags featurecoverage ./packages/orm-go/model -run TestCoverageModelGenerationCheck가 그 message로 1로 끝납니다. Green: 같은 명령이 0으로 끝납니다.——[o]
T45.3TestCIRequiresGeneratedChecks가 CI의 group 명령에 새 target 이름을 적게 합니다. 원인: test는 ci.yml에 run: make check GROUP=${{ matrix.group }} 줄을 요구하는데 T44.2가 그것을 make check-run GROUP=...로 바꿔서, test (clients_db)가 go-test-check에서 실패했습니다. 완료 기준: test가 make check-run GROUP=${{ matrix.group }}를 적습니다. Red: go test ./tests/interfaces/check -run TestCIRequiresGeneratedChecks가 "CI must run make check for each CI group"로 1로 끝납니다. Green: go test ./tests/interfaces/check가 0으로 끝납니다.——[o]
T460.0.5를 release합니다. 원인: T43–T45의 작업이 main에 있고 그 CI가 통과했습니다. 완료 기준: config/release.json의 모든 manifest, lock file, VERSION, version을 적는 문서가 0.0.5를 선언하고, changelog의 ## Unreleased가 새 빈 ## Unreleased 아래의 ## 0.0.5로 바뀌며, config/dependency-review.json이 review를 다시 기록하고, tests/release-consumer가 0.0.5 archive의 lock을 담으며, push-gate.yml은 branch의 push에서만 돌고 tag의 push에서는 돌지 않습니다.—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, make documents-check가 나중에 지운 local tag로 0으로 끝납니다. maintainer는 CI가 통과한 뒤 이 commit에 tag한다.[o]
T47Stress 기한. 원인: TestApplyStressPlan이 22000개의 MySQL step에 총 12분의 고정 기한을 썼고, 31% 느린 runner는 20000 step에 10분 21초가 걸려 context deadline exceeded로 실패했습니다(이전 실행은 7분 54초). 완료 기준: case의 기한을 step 수에서 정하고, 2분 안에 step을 적용하지 못하는 database는 그 한도를 밝히는 원인과 함께 실패하며, plan은 모든 step을 유지합니다.—증거: go test ./internal/testcase -run 'TestStepBudget|TestStallGuard'와 go vet -tags physical ./tests/dialects가 0으로 끝나고, MySQL에서의 case 실행은 CI가 확인한다.[o]
T48Stress helper의 소유. 원인: T47이 budget과 stall guard를 어떤 check도 고르지 않는 tests/dialects/stress_budget.go와 그 test에 두어 CI에서 feature-unit-check가 every tracked file selects a behaviour test or declares its scope로 실패했다. 완료 기준: 추적하는 모든 file이 check를 고르거나 scope를 선언하고, helper는 testcase-check가 소유하는 internal/testcase에 있다.—증거: make feature-unit-check와 make testcase-check가 0으로 끝난다.[o]
T49PHP와 TypeScript client가 가진 0.0.5의 동작 세 가지를 Python client에 줍니다. 원인: release 0.0.5의 Python client에서 문서에 use 줄이 있는 set의 install이 AttributeError로 실패하고, driver 오류가 ORM 오류가 아니라 그 driver의 예외로 빠져나가며, docs/spec/protocol.md "Test faults"의 rollback fault를 켜는 test entry point가 없습니다. 완료 기준: T49.1–T49.3이 끝납니다.—Evidence: T49.1–T49.3이 끝났고 make python-unit-check가 test 109개 OK로 0을 냅니다.[o]
T49.1문서에 use 줄이 있는 set을 install합니다. 원인: packages/orm-python/src/polyspec/orm/dbspec/compare.py의 is_schema_text가 plain 문서의 use 줄을 dict로 만들었고 emit_document는 u.document를 읽으므로, use audit { audit }처럼 외부 문서를 쓰는 set의 utils().schema().install(schema)가 AttributeError: 'dict' object has no attribute 'document'로 실패했습니다. 완료 기준: use 줄은 packages/orm-npm/src/dbspec/compare.ts의 isSchemaText가 만드는 것처럼 comment가 없고 table을 정렬해 중복 없이 담은 DbspecUse 값입니다. use 줄이 있는 schema text는 schema text이고, install은 SQLite에서 set을 만든 뒤 확인합니다. Red: packages/orm-python/tests에서 python -m unittest test_schema_install_uses가 오류 2개 AttributeError: 'dict' object has no attribute 'document'로 1을 내고 끝납니다. Green: 같은 명령이 test 2개 OK로 0을 내고, make python-unit-check가 test 91개 OK로 0을 내며, tests/dbspec/compare/python.py는 공유 case, ddl, plan, Mermaid vector에 대해 변경 전후로 같은 3385줄을 출력합니다.——[o]
T49.2driver 오류를 ORM 오류 code로 바꿉니다. 원인: statement의 sqlite3.IntegrityError, PyMySQL 오류, psycopg 오류가 그대로 빠져나가지만, PHP(OrmException::fromDriver)와 TypeScript(driver.ts의 driverError) client는 driver message와 driver 오류를 cause로 가진 DUPLICATE_KEY, FOREIGN_KEY, CONSTRAINT, DEADLOCK, LOCK_NOT_AVAILABLE, CANCELED, READ_ONLY, CONNECTION_LOST, DRIVER를 돌려줍니다(docs/spec/protocol.md "Errors"). 완료 기준: Python 연결 세 가지의 모든 statement가 같은 표의 code, message <driver>: <driver message>, cause인 driver 오류를 가진 OrmError를 돌려줍니다. SQLite case는 database file에서 실행하고, MySQL과 PostgreSQL 행은 server 없이 PyMySQL과 psycopg의 오류 값에 같은 표를 실행합니다. Red: packages/orm-python/tests에서 python -m unittest test_driver_errors가 case마다 driver의 예외 그대로인 오류 38개(sqlite3.IntegrityError나 sqlite3.OperationalError 8개, PyMySQL 오류 17개, psycopg 오류 13개)로 1을 내고 끝납니다. Green: packages/orm-python/src/polyspec/orm/database.py의 _driver_error가 SQLite extended result code, MySQL 오류 번호, PostgreSQL SQLSTATE를 driverError의 표로 바꾸고, 세 연결의 statement와 연결 열기가 그 OrmError를 던지며, SQLite의 NOWAIT row lock은 cause에서 SQLITE_BUSY를 읽습니다. 같은 명령이 test 12개 OK(MySQL 16개, PostgreSQL 12개 sub-case)로 0을 내고, make python-unit-check가 test 103개 OK, make python-interface-unit-check가 test 13개 OK, make conformance-result-check/python이 test 7개 OK로 0을 내며, symbol snapshot은 바뀌지 않습니다. docs/spec/config.md "Lost connections"가 닫힌 연결의 PyMySQL과 psycopg 오류를 적습니다. MySQL과 PostgreSQL 행은 CI에서만 실제 server로 실행되며, 그곳에서 Python client는 contracts/features.json의 planned이고 feature coverage case가 없습니다.——[o]
T49.3rollback fault test entry point polyspec.orm.testing.fail_next_rollback을 더합니다. 원인: Go, Rust, TypeScript, PHP는 docs/spec/protocol.md "Test faults"의 rollback fault를 test entry point로 켜고, Python client에는 그것이 없습니다. 완료 기준: fail_next_rollback(db)가 연결에 fault를 켭니다. commit과 savepoint rollback은 fault를 남깁니다. callback이 실패한 다음 transaction은 rollback되고, cause가 callback 오류이고 rollback이 FAULT 오류인 ROLLBACK 오류 하나를 던지며, 그 뒤의 transaction은 callback 오류만 던집니다. 배포의 wheel은 이 module을 담지 않습니다. Red: packages/orm-python/tests에서 python -m unittest test_rollback_fault가 오류 3개 ModuleNotFoundError: No module named 'polyspec.orm.testing'와 실패 1개 'polyspec.orm.testing' not found in [](배포의 exclude)로 1을 내고 끝납니다. SQLite trigger RAISE(ROLLBACK)가 rollback을 실패하게 하는 두 case는 T49.2부터 통과하며, T49.1의 tree에서는 두 오류를 가진 ROLLBACK 오류가 아니라 OperationalError('cannot rollback - no transaction is active') 그대로 실패했습니다. Green: polyspec/orm/testing/__init__.py가 _arm_rollback_fault로 Db의 fault를 설정하는 fail_next_rollback을 선언하고, Db._run이 실패한 callback의 rollback을 FAULT 오류 test fault: the rollback of the transaction ran and is reported as failed를 가진 ROLLBACK으로 보고하고 fault를 소비하며, pyproject.toml의 [tool.setuptools.packages.find]가 polyspec.orm.testing을 뺍니다. 같은 명령이 test 6개 OK로 0을 내고, package 복사본에서 uv build --wheel로 만든 wheel은 file 61개를 담고 testing file은 없습니다. contracts/symbols/python.json이 testing/__init__.py::fail_next_rollback을 더하고 contracts/interfaces.json이 그 hash를 적으며, go run ./tests/interfaces/check --language python이 symbol 1409개로 통과하고 go test ./contracts가 통과하며 make python-check가 0을 냅니다(test 109개, 13개, 7개 OK와 model check). docs/spec/protocol.md "Errors"와 "Test faults"가 Python의 field와 entry point를 적습니다.——[o]
T50Python client의 실패한 statement event subscriber를 다른 client처럼 보고합니다. 원인: packages/orm-python/src/polyspec/orm/events.py의 _subscriber_error가 OrmError에 없는 failure.message를 읽어서, SUBSCRIBER가 아닌 OrmError를 던진 subscriber는 statement를 AttributeError로 실패시켰고, 다른 subscriber 오류의 SUBSCRIBER 오류에는 cause가 없었습니다. packages/orm-npm/src/events.ts는 subscriber 오류의 text를 message에 쓰고 그 오류를 cause로 둡니다. 수용 기준: subscriber 오류는 statement를 SUBSCRIBER: statement event subscriber failed: <오류의 text>로 실패시키고, OrmError의 text는 <code>: <message>이며, SUBSCRIBER 오류는 subscriber 오류를 cause로 가집니다. packages/orm-python/tests/test_subscriber_error.py가 둘을 보입니다.—증거: Red: packages/orm-python/tests에서 python -m unittest test_subscriber_error가 error 1개 AttributeError: 'OrmError' object has no attribute 'message'와 failure 1개 None is not ValueError('the subscriber failed')를 냈습니다. Green: 같은 command가 test 2개를 OK로 실행하고, make python-check가 test 111, 13, 7개를 OK로 실행하며 model check가 통과합니다.[o]
T51Python rollback fault test entry point를 배포에 담습니다. 원인: T49.3이 package polyspec.orm.testing을 배포에서 빼서, 설치된 polyspec-orm의 소비자는 자기 test에서 이것을 import할 수 없습니다. 다른 모든 client는 entry point를 package에 담고 production process가 이것을 load하지 못하게 합니다: Go build tag, Rust feature, npm export condition, autoloader가 map하지 않는 PHP file. 완료 기준: 배포가 polyspec.orm.testing을 담고, polyspec.orm의 어느 module도 이것을 import하지 않으므로 process는 이것을 직접 import한 뒤에만 가지며, docs/spec/protocol.md "Test faults"(와 한국어 pair)가 PHP처럼 이것을 적습니다.—증거: Red: packages/orm-python/tests에서 python -m unittest test_rollback_fault가 test_the_distribution_holds_the_test_entry_point_that_no_import_loads를 'polyspec.orm.testing' unexpectedly found in ['polyspec.orm.testing', 'polyspec.orm.testing.*']로 실패했습니다. Green: exclude가 없으면 같은 command가 test 6개를 OK로 실행하고, polyspec.orm과 polyspec.orm.database를 import한 process의 sys.modules에 polyspec.orm.testing이 없습니다.[o]
T51.1T51의 한국어 기록을 writing-style 규칙이 금지한 표현 없이 씁니다. 원인: 02bc501의 CI가 test (static)를 docs.writing-style: docs/protocol.ko.md contains forbidden expression "이름으로"로 실패했습니다. T51이 Python test entry point의 한국어 row와 그 checklist row를 그 표현으로 썼고, push 전에 make docs-rules-check를 실행하지 않았기 때문입니다. 완료 기준: npm run docs:rules-check가 통과합니다.—증거: Red: npm run -s docs:rules-check가 FAIL docs-rules elapsed=355ms: the process ended inside the case: docs.writing-style: docs/protocol.ko.md contains forbidden expression "이름으로"를 출력했습니다. Green: row를 직접 import한 뒤에만으로 쓰면 같은 command의 출력은 PASS docs-rules입니다.[o]
T520.0.6을 release합니다. 원인: T47–T51의 작업이 main에 있습니다. 완료 기준: config/release.json의 모든 manifest, lock file, VERSION, version을 적는 문서가 0.0.6을 선언하고, changelog의 ## Unreleased가 새 빈 ## Unreleased 아래의 ## 0.0.6으로 바뀌며, config/dependency-review.json이 review를 다시 기록하고, tests/release-consumer가 0.0.6 archive의 lock을 담습니다.—증거: 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, make docs-rules-check가 나중에 지운 local tag로 exit 0이고, 2026-10-09의 dependency review가 예외 없는 새 stable release와 advisory를 찾지 않았습니다. make release-consumer TAG=v0.0.6은 npm archive를 설치하고 검사한 뒤 Composer project에서 멈췄습니다. release를 만든 machine에서 Composer가 GitHub API로부터 symfony/yaml, symfony/deprecation-contracts, symfony/polyfill-ctype의 dist를 받지 못했습니다(Could not authenticate against github.com). 같은 Composer project는 commit된 lock과 0.0.6 archive로 --prefer-install=source 설치에 성공했고, smoke command가 Polyspec\Orm\Db를 찾았습니다. Composer가 GitHub dist를 받을 수 있는 macOS machine에서 tag의 make install-node, make release-assets TAG=v0.0.6, make release-consumer TAG=v0.0.6이 exit 0이다. [release-consumer] v0.0.6: installed and checked 2 packages from var/release/assets로 npm package와 Composer package가 smoke command와 함께 통과했다.[o]
T53packages/php/tests/RollbackTest.php의 case first_statement_lost가 Db의 session만 확인한다. 원인: case가 Orm::connect 전후로 case database의 session을 읽고 새 session이 정확히 하나이길 요구하는데, run 37887199714(attempt 1)의 CI lane clients_pooler에서 PostgreSQL case가 새 session 두 개 {"before":[13243],"opened":[13246,13245]}를 보았다. 두 snapshot 사이에 다른 client가 같은 database에 연 session, 예를 들어 pooler의 server connection도 Db의 session으로 판정하기 때문이고, 같은 commit이 두 번째 attempt에서는 통과했다. 완료 기준: case가 Db의 session을 그 session만 가진 속성, 예를 들어 test가 direct DSN에 정하는 application_name으로 찾고, 두 snapshot 사이에 case database에 connection을 하나 더 여는 test에서도 MySQL과 PostgreSQL에서 정확히 Db의 session을 찾는다. 의존: 없음.—Evidence: commit 9fc1af74(fix 커밋은 rebase 전 58a53bfd): packages/php/tests/RollbackTest.php가 $db->pdo()에서 CONNECTION_ID() 또는 pg_backend_pid()로 session을 찾는다. session 수 차이로 찾지 않는다. RED: first_statement_lost가 MySQL과 PostgreSQL에서 스냅샷 사이에 추가 connection이 열려 실패했다(opened [352,353]과 [16081,16074]). GREEN: RollbackTest.php가 통과한다(test 11개, assertion 100개). Conformance: make run-databases TARGETS=conformance-check RECORD=1이 vector 차이 없이 exit 0이고, 다시 실행해도 exit 0이다. main에서 documents-check, docs:rules-check, feature-unit-check/owners, layout-check, go-fmt-check가 exit 0이다.[o]
T54Python client가 값 스타일 컬럼의 SQL NULL을 StyledValue.sql_null()로 읽는다. 원인: packages/orm-python/src/polyspec/orm/database.py의 _decode_assembly가 None cell을 건너뛰어서(if value is None or not column['styles']: continue), author.json_setting이나 consumer module core의 json 컬럼 service_route.parameters 같은 nullable 값 스타일 컬럼의 getter가 SQL NULL에 None을 돌려주었고, 그 값을 받은 setter는 CODEC_ENCODE: json_setting requires StyledValue로 실패했다. docs/spec/codec.md "Value model"은 모든 getter가 StyledValue를 돌려주고 SQL NULL이 SqlNull이기를 요구하며, packages/orm-npm/src/codec.ts의 decode는 null cell에 StyledValue.sqlNull()을 돌려준다. 완료 기준: client codec 단계가 있는 컬럼의 None cell은 codec.py의 decode를 거쳐 StyledValue.sql_null()이 되고, host 단계(aes, hex, ip)만 있는 컬럼은 TypeScript client의 decodeAssembly처럼 None을 유지한다. root 행, join 행, relation 행이 SQL NULL을 StyledValue.sql_null()로 읽고 to_array()는 이것을 {"kind":"sql-null"}로 쓴다. 행에서 읽은 값은 같은 저장 cell로 다시 쓰인다. docs/codec.md와 한국어 pair가 SQL NULL의 읽기와 Python의 두 변형을 적는다.—증거: Red: packages/orm-python/tests에서 python -m unittest test_styled_sql_null이 실패 3개 None is not an instance of <class 'polyspec.orm.styled_value.StyledValue'>(root, join, relation 행)와 오류 1개 CODEC_ENCODE: json_setting requires StyledValue(다시 쓰기)로 실패했다. Green: 같은 command가 test 4개를 OK로 실행하고, make python-check가 test 115, 13, 7개를 OK로 실행하며 model check가 통과한다.[o]
T55Python model과 row 집합에 JSON text 출력 to_json_text()를 준다. 원인: TypeScript model은 row와 Collection에 toJSONText()를 가지고(packages/orm-npm/src/model.ts) PHP는 toJson()을 가지며, docs/codec.md의 모델 JSON 출력 행이 client마다 그 method를 적는다. 그러나 Python model에는 JSON text 출력이 없었고 model의 json.dumps는 TypeError로 실패하므로, Python program은 저장된 ordered-json 텍스트를 담은 값 스타일 컬럼의 바깥 표현을 쓸 수 없었다. 완료 기준: Model.to_json_text()는 TypeScript client의 rowText처럼 row를 쓴다. 멤버는 pairs의 순서(선택한 컬럼, new 값, relation, flatten된 relation의 멤버이며 먼저 나온 이름을 쓴다)이고, 값 스타일 컬럼은 {"kind":"sql-null"}이나 {"kind":"value","value":<저장된 ordered-json 텍스트>}, 관련 row나 row 집합은 그 자신의 텍스트, 다른 값은 JSON.stringify가 쓰는 대로(실수는 JavaScript Number::toString, 문자열은 따옴표, 역슬래시, 제어 문자만 escape) 쓴다. Collection.to_json_text()는 [<row 텍스트>,...]를 쓴다. JSON으로 쓸 수 없는 값(bytes, 유한하지 않은 수, 문자열이 아닌 멤버 키, 다른 type)은 CODEC_ENCODE로 실패한다. to_array()는 TypeScript와 PHP의 pairs처럼 같은 행 순서를 쓴다. docs/codec.md와 한국어 pair가 Python method를 적고 표에 Python 열을 더한다. contracts/symbols/python.json과 contracts/interfaces.json의 hash가 두 method를 기록한다.—증거: Red: packages/orm-python/tests에서 python -m unittest test_model_json_text가 오류 5개(4개는 CONFIG: ToJsonText is not a column of author 또는 of user, 1개는 AttributeError: 'Collection' object has no attribute 'to_json_text')와 실패 1개 'CONFIG' != 'CODEC_ENCODE'로 실패했다. Green: 같은 command가 test 7개를 OK로 실행하고, make python-check가 test 122, 13, 7개를 OK로 실행하며 model check가 통과하고, go run ./tests/interfaces/check --language python이 symbol 1411개로 통과하며 go test ./contracts가 통과한다. 저장소에는 모델 JSON 출력의 client 간 test가 없으므로 test는 packages/orm-npm/tests/model.mjs가 요구하는 텍스트와 비교하고, 저장소 밖의 임시 실행에서 test의 텍스트 네 개(row, row 집합, relation이 있는 row, new 값과 순서가 바뀌는 key가 있는 row)를 같은 SQLite file에서 Python client와 build된 TypeScript client로 썼으며 두 출력은 byte 단위로 같다(1462 byte).[o]
T560.0.7을 release한다. 원인: T54와 T55가 Python client에 SQL NULL을 StyledValue.sql_null()로 읽는 동작과 model의 JSON text 출력을 주고, consumer unit의 Python package가 이것을 필요로 한다. 완료 기준: config/release.json의 모든 manifest, lock file, VERSION, version을 적는 문서가 0.0.7을 밝히고, changelog의 ## Unreleased가 새 빈 ## Unreleased 아래 ## 0.0.7이 되며, config/dependency-review.json이 review를 다시 기록하고, tests/release-consumer가 0.0.7 archive의 lock을 가진다. 의존: T54, T55.—증거: 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, make docs-rules-check가 나중에 지운 local tag로 exit 0이고, dependency review가 예외 없는 새 stable release와 advisory를 찾지 않았다. make release-consumer TAG=v0.0.7은 npm archive를 설치하고 검사한 뒤 T52와 같은 원인으로 Composer project에서 멈췄다. release를 만든 machine의 Composer가 GitHub API에서 symfony package의 dist를 받지 못했다. 같은 Composer project는 commit된 lock과 0.0.7 archive로 --prefer-install=source 설치에 성공했고, smoke command가 Polyspec\Orm\Db를 찾았다. Composer가 GitHub dist를 받을 수 있는 macOS machine에서 tag의 make install-node, make release-assets TAG=v0.0.7, make release-consumer TAG=v0.0.7이 exit 0이다. [release-consumer] v0.0.7: installed and checked 2 packages from var/release/assets로 npm package와 Composer package가 smoke command와 함께 통과했다.[o]
T57README가 Python client를 밝히고 제목이 version을 적지 않게 한다. 원인: T43이 Python client에 같은 query grammar를 주었는데도 README는 Python client를 적지 않았고, 제목은 manifest가 적는 version 0.0.7을 적었다. 제목은 version을 적지 않고, 소개와 예는 Go, PHP, Python, Rust, TypeScript를 적으며, Python chain과 조회가 나머지에 더해지고, models 항목이 Python package의 orm-gen bin을 적으며, quick start가 make python-check를 실행하고, 동일성과 도구 줄이 다섯 client를 적는다. 확인: make documents-check가 통과한다.——[o]
T58docs/dsl.md와 docs/dsl.ko.md가 Python client를 명시한다. 원인: DSL 문서가 Go, PHP, Rust, TypeScript를 비교한다. 언어 차이를 말하는 모든 문장을 Python client와 대조 검증하고 다섯 언어로 확장한다. model 표, tuple과 chain 규칙, 생성과 scan, null과 between 규칙, 실행 흐름이 대상이다. |.—Verification: make documents-check passes.[o]
T57-1version check가 root README의 version 선언을 읽지 않게 한다. 원인: T57이 README.md와 README.ko.md의 제목에서 version을 지웠는데 scripts/version/check.mjs의 version-check가 두 file의 제목을 version 선언으로 읽어서, push의 CI job test (static)이 README.ko.md: no version declaration found (VERSION is 0.0.7)로 실패했다. manifest와 VERSION, 본문에 version을 적는 문서는 여전히 그것을 선언한다. 확인: make version-check가 일치하는 선언 25개를 보고하고 node --test scripts/version/check.test.mjs가 case 5개를 통과한다.——[o]
T59dbspec에 state_machine setting을 추가한다. 원인: flowmark markdown executor가 checklist state machine을 자체 sub-grammar를 가진 manifest-level setting으로 관리하며(polyspec/flowmark의 docs/plans/design-plan.md, T5), dbspec에는 row 전환을 명명하는 setting이 없다. audit이 sub-grammar 선례다. 완료 기준: state_machine <column> <from> -> <to> [require (<column>, ...)]과 state_machine <column> terminal <state> [require (<column>, ...)]이 table마다 한 column에서 parse되고, validator가 상태 column(non-null varchar 또는 text), require column, table당 machine 하나, terminal 상태를 벗어나는 전환 없음을 검사하고, emitter가 canonical line을 써서 emit(parse(s)) == s가 성립하며, settings model이 machine을 운반한다. 확인: go test ./engine/....——[o]
T60dbspec에 markdown setting을 추가한다. 원인: flowmark markdown executor가 산문 column이 markdown인 corpus 문서를 읽으며(polyspec/flowmark의 docs/plans/design-plan.md, T5), schema가 그 column을 명명하는 manifest-level trait이 필요하다. navigation이 manifest-only 선례다. 완료 기준: markdown <column>이 text 또는 varchar column을 markdown 산문으로 표시하고 column마다 반복 가능하다. validator가 column 종류를 검사하고 nullability는 자유롭다(없는 산문은 NULL). emitter가 canonical line을 쓰고 settings model이 trait을 운반하며 database는 이를 위해 아무것도 render하지 않는다. 확인: go test ./engine/....——[o]
T61protocol에 IR executor 절을 추가하고 dialects에 markdown capability 표를 추가한다. 원인: flowmark 계획(polyspec/flowmark의 docs/plans/markdown-database-plan.md, "flowmark is the engine; orm reaches it as an IR executor" 결정과 T12.4 단계)은 SQL dialect 대신 IR executor를 고르는 DSN scheme으로 orm 클라이언트가 flowmark에 닿는다고 정하지만, protocol에는 그런 절, write-conflict 오류, 이 executor의 capability 표가 없다. 완료 기준: docs/spec/protocol.md에 DSN scheme이 IR executor를 고르는 절이 있다. 클라이언트는 ir.Validate로 요청을 검증하고 계획하지 않으며, 요청마다 {ir_version, manifest_hash, request, params, now}를 보내고, params는 runtime value model JSON이며, codec 단계는 클라이언트가 직접 적용한다. 읽기는 조립된 row를, 쓰기는 {affected, keys}를 돌려준다. 제어 메시지 begin, commit, rollback, savepoint, release, rollback_to가 적혀 있다. identity, key_prefix, order column은 executor가 값을 정하므로 insert에서 생략할 수 있다. docs/spec/errors.yaml에 WRITE_CONFLICT(읽은 뒤 대상이 바뀌었음, 클라이언트는 재시도하지 않음)가 있고 끝난 lock 대기는 CANCELED라고 적혀 있다. 생성된 오류 상수에 WRITE_CONFLICT가 있다. docs/spec/dialects.md에 flowmark docs/spec/60-dialect.md의 surface를 담은 markdown executor capability 표가 하나 있다. 한국어 쌍도 같은 내용을 적고, make documents-check와 make docs-rules-check가 0으로 끝난다.——[o]
T62dbspec에 markdown 저장 setting을 추가한다. 원인: flowmark 계획(flowmark docs/spec/30-schema.md의 저장 문법과 markdown setting, T12.2 단계는 이 orm setting을 요구한다)은 dbspec table을 manifest setting으로 markdown corpus에 옮기며, markdown setting(T60)과 state_machine(T59)의 선례를 따른다. SQL backend는 이를 무시한다. 완료 기준: store files, store document list|table, store block <fk> list|table이 parse되고, table에 store setting이 둘 이상이면 setting 오류다. 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>, state_machine <column> limit <state> <n>이 parse되고, emitter가 canonical line을 써서 emit(parse(s)) == s가 성립한다. validator는 다음을 setting 오류로 거부한다. checkbox, limit, initial이 machine의 state 집합 밖의 state를 이름 짓는 경우. checkbox가 state 집합을 서로 다른 glyph로 정확히 덮지 못하는 경우. state column의 default가 initial state가 아닌 경우. initial state가 terminal이기도 한 경우. history table의 column이 요구된 모양과 다른 경우(<fk column>의 foreign key와 선행 index, state column 타입의 from과 to, datetime(6)의 at, 요구된 column마다 nullable column 하나, 그 밖의 column 없음). key_prefix가 varchar primary key가 아닌 column에 있는 경우. order column이 non-null i32 또는 i64가 아니거나 default가 있거나 key, index, check에 들어 있는 경우. settings model이 각 setting을 운반하고 manifest hash가 setting을 포함한다. engine/dbspec에 red 후 green 규칙 test가 있고, dbspec 문서와 한국어 쌍이 규칙을 적는다. corpus 수준 거부(복합 key, aes, audit, immutable, cascade와 set_null, 외부 use 문서, store 없음)는 flowmark의 것이며 orm은 거부하지 않는다. T61이 필요하다.——[o]
T62-3dbspec의 canonical literal 함수를 export한다. 원인: flowmark(polyspec/flowmark, engine/dbspec을 library로 쓰는 markdown database engine)는 column 값을 dbspec의 canonical literal 형태로 읽으며 unexported canonicalDefault를 복사했고, 이 복사본은 orm과 어긋날 수 있다(T62의 후속). 완료 기준: dbspec이 CanonicalLiteral(t Type, text string) (string, bool)을 export하며, 이는 column default로 쓰인 literal의 canonical 형태와 그 literal이 type에 유효한지 여부를 돌려주고 canonicalDefault와 같은 코드로 구현된다. 그 test는 모든 type을 유효한 literal과 유효하지 않은 literal로 덮는다. flowmark의 복사본을 제거할 수 있다.——[o]
T62-1다른 dbspec 구현들이 state_machine, markdown과 markdown 저장 setting을 파싱한다. 원인: T59, T60, T62는 Go 구현만 바꾸었으므로, 이 setting이 있는 문서 집합은 TypeScript, PHP, Rust와 PHP 확장 구현에서 setting 오류다. 그러나 모든 구현은 같은 문서를 읽어야 한다(docs/dbspec.md의 "PHP extension"과 "Verification"). 완료 기준: 각 구현은 이 setting을 Go와 정확히 같이 파싱하고, 검증하며(같은 규칙, 같은 rule 이름과 위치), 방출한다. 이는 모든 구현이 실행하는 tests/dbspec/cases.json의 공유 case로 증명한다. 모든 구현의 dbspec 검사 target이 통과한다(dbspec-go-check, dbspec-ts-check, dbspec-php-check, dbspec-rust-check, PHP 확장의 검사, dbspec-compare-check와 manifest hash 일치 검사).——[o]
T62-2PHP, Rust, PHP 확장 dbspec lexer가 Go처럼 점이 있는 이름을 token 하나로 유지한다(T62-1의 후속). 원인: 이들은 a.b를 Go처럼 한 token으로 유지하지 않고 두 token으로 나눈다. 완료 기준: 공유 점 이름 case가 모든 구현에서 통과한다.——[o]
T62-4이름과 symbol의 non-ASCII 글자를 모든 구현이 Go처럼 lex한다. 원인: Go는 rune을 unicode.IsLetter와 unicode.IsDigit로 분류한다. Rust lexer(packages/orm-rust/orm-schema/src/dbspec/lexer.rs의 word_char)는 공백이 아닌 non-ASCII 문자를 모두 단어 문자로 보고, PHP 확장(packages/orm-php-extension/src/parse.c의 dotword_ascii와 code point 분기)은 C에 Unicode 글자 분류가 없어 같이 한다. 그래서 € 같은 symbol이 그 구현들에서는 이름의 일부이고 Go에서는 허용되지 않는 문자다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 동작을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다(수정 전 red 실행은 tests/dbspec/cases.json의 새 case에서 dbspec-compare/agreement로 실패했다). 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 python-check가 0으로 끝나며, make docs-rules-check와 make documents-check가 0으로 끝난다.[o]
T62-5header가 실패한 table의 본문을 모든 구현이 Go처럼 더 검사하지 않는다. 원인: Go는 그런 table을 failed로 표시하고(engine/dbspec/parse.go의 header와 중괄호 분기), engine/dbspec/validate.go는 이를 등록만 하고 본문 규칙을 건너뛴다(if t.failed). 그래서 본문은 문법만 parse된다. 공유 case 중 본문에 닿는 잘못된 table header는 없어 다른 구현은 고정되어 있지 않다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 동작을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 위 원인은 Go에 대해 틀리며 제목도 마찬가지다. Go는 header가 실패한 table의 본문을 모두 검증한다. validate.go에는 if t.failed 건너뛰기가 없다. header 실패(이름 없음, { 없음, { 뒤의 말, parse.go의 openTable)는 failed를 표시하고, 그 table을 자신으로 또는 target으로 읽는 검사 다섯 개만 바꾼다. 같은 이름 table의 중복 보고(건너뜀), 그 table을 가리키는 외래 key의 참조 열, 개수, key, type 검사(foreignKey, 자식의 색인 검사는 그대로 실행), 실패한 audit history table의 검사(audit, 반환), 실패한 audit 기록 table의 검사(auditRecord, 반환)다. state_machine의 history table은 전과 같이 검사된다. 공유 case failed-header-*(열세 개, tests/dbspec/cases.json)는 Go 출력으로 이를 고정하며, Go가 여전히 구문 오류로 보고하는 본문 구문 오류도 고정한다. Red: 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(key 줄 뒤의 column 줄에서 php 1 differs from go 1. header와 무관한 case여서 제거했고 T62-5-1이다). 그 뒤 다른 다섯 구현의 runner 출력은 target, 이름 중복, audit 검사의 다섯 case에서 Go와 달랐다. Green: make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다. go test ./engine/dbspec -run TestSharedVectors/invalid/failed-header가 통과한다. 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가 0으로 끝난다. Rust와 TypeScript의 symbol snapshot을 기록한 뒤(contracts/symbols/rust.json, contracts/symbols/typescript.json, hash는 contracts/interfaces.json) make interface-check가 0으로 끝난다. make documents-check가 0으로 끝난다. make docs-rules-check는 HEAD에도 있는 T62-4-1 줄의 금지 표현 하나 때문에 2로 끝난다.[o]
T62-4-5feature-unit 검사가 실행하는 모든 음성 owner sample을 테스트가 확인하고, 통과한 실행은 확인되지 않은 실패를 출력하지 않는다. 원인: T62-4-4는 세 sample 실패가 출력되지만 어떤 테스트도 확인하지 않음을 찾았다 — sample/owner/typescript/none의 두 번째 duplicate observed case first(scripts/features/coverage.test.mjs의 no executable command 호출이 실행)와 sqlite만 쓰는 두 번째 호출의 mysql, postgres 줄 — 그래서 그 sample이 바뀌어도 알아채지 못한다. 또 모든 음성 sample이 통과한 실행 아래에서 FAIL을 출력하고, go/none 메시지는 빈 목록에서 "observed cases differ"로 보인다. 완료 기준: 출력된 각 sample 실패를 그것을 실행하는 테스트가 확인하고, 통과한 실행의 출력은 확인된 음성 sample을 실패로 보여 주지 않으며, 빈 목록 메시지는 빈 집합을 밝히고, make feature-unit-check가 0으로 끝난다.—Evidence: Red: 메시지를 바꾸기 전 강화한 assertion이 make feature-unit-check/coverage에서 2로 끝났다. go/none의 차이는 observed cases differ from TestFirst였고, native와 database test는 sample이 만드는 오류와 FAIL 줄을 실제 값으로 보여 주었다. ab3e779a에서 같은 target은 0으로 끝나면서 FAIL 줄 여덟 개를 출력했다(sample/owner/typescript/none 세 개, typescript/mysql과 typescript/postgres 각 두 개, go/none 한 개). Green: make feature-unit-check/coverage가 0으로 끝나고 ℹ pass 17과 ℹ fail 0을 출력하며 FAIL 줄을 출력하지 않는다. make feature-unit-check가 0으로 끝나고(coverage 17, owners 19, select 7 통과, 실패 없음), make repo-check, make interface-check, make documents-check, make docs-rules-check가 0으로 끝난다.[o]
T62-4-6non-ASCII 문자열 literal을 가진 CHECK가 모든 client에서 MySQL을 거쳐 그대로 되돌아온다. 원인: tests/dbspec/plans.json의 plan case rename-column-check-non-ascii-literal(check ck_users_mail (mail <> 'café')에서 users.mail을 email로 바꾼다)이 물리 MySQL 실행에서 make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check, make dbspec-plan-rust-check를 실패시킨다. 적용된 schema는 check ck_users_mail (email <> 'café')로 읽히며, é의 UTF-8 바이트가 Latin-1로 풀린 뒤 다시 인코딩된 mojibake다. 테스트 서버(.runtime/servers/env의 mysql://root@127.0.0.1:33171/orm_test)는 character_set_server, character_set_client, character_set_connection이 모두 utf8mb4이고, SELECT 'café' = _utf8mb4'café'가 1을 반환하므로 서버 쪽 literal은 정상이다. 네 client가 모두 같은 잘못된 text를 만들며, 잘못된 값이 어디서 들어오는지(보내는 statement인지 각 client의 MySQL catalog가 읽는 CHECK_CLAUSE인지)는 아직 분리하지 않았다. plan step은 runner 사이에서 일치하므로(make dbspec-compare-check가 12 runs agree를 출력한다) 물리 왕복만 다르다. 완료 기준: 모든 client에서 non-ASCII CHECK literal을 가진 plan case가 MySQL을 거쳐 그대로 되돌아오고, tests/dbspec/plans.json에 그 case가 있는 상태에서 make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check, make dbspec-plan-rust-check가 0으로 끝난다.—Evidence: 주입 지점은 MySQL 서버의 information_schema.CHECK_CONSTRAINTS.CHECK_CLAUSE다. 모든 client의 introspection이 저장된 text를 그대로 읽는 곳이며, 보낸 DDL과 connection charset은 원인이 아니다. mysql CLI(MySQL 8.4.11, HEX())로 분리했다. CHECK (mail <> 'café')가 있는 table의 SHOW CREATE TABLE은 é의 UTF-8 bytes인 636166c3a9를 돌려주지만, 같은 제약의 CHECK_CLAUSE는 utf8mb4, utf8mb3, binary connection 모두에서 636166c383c2a9(café)를 돌려준다. latin1 connection은 DDL 시점에 자기 mojibake를 저장한다. character_set_server가 utf8mb3와 utf8mb4인 임시 서버 두 개도 같은 catalog text를 보였고, 'café'의 COLUMN_DEFAULT는 바르게 읽혔다. Red: 변경 전 case를 tests/dbspec/plans.json에 더한 상태에서 make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check, make dbspec-plan-rust-check가 mysql.plan.rename_column_check_non_ascii_literal에서 2로 끝났고, 적용된 schema에 check ck_users_mail (email <> 'café')가 나왔다. 변경: 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와 packages/orm-rust/orm-schema/src/dbspec/introspect/mysql.rs의 MySQL introspection은 CHECK_CLAUSE에 0x80 이상의 byte가 있는 CHECK의 본문을 SHOW CREATE TABLE에서(table마다 한 번) 읽어 CHECK_CLAUSE처럼 escape하므로 기존 decoder는 바뀌지 않는다. Rust driver(packages/orm-rust/orm/src/dbspec.rs, utils.rs와 packages/orm-rust/orm-schema/src/dbspec/introspect/mod.rs의 catalog_followups)는 catalog query 뒤에 그 follow-up query를 실행하며, trait CatalogQuerier는 &str을 받는다(test 구현도 바뀌었다). symbol snapshot contracts/symbols/{go,php,rust,typescript}.json은 go run ./tests/interfaces/check --record --self-test로 다시 기록했고, contracts/interfaces.json의 hash를 고쳤다. docs/dialects.md와 docs/dialects.ko.md가 그 동작을 적는다. 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, make repo-check이 모두 0으로 끝난다. go test ./engine/dbspec -count=1이 통과하고, 그 안의 TestPlanVectors/rename-column-check-non-ascii-literal이 vector를 통과한다. Go, PHP, TypeScript parser helper에는 자체 unit case가 없다. 이 case의 물리 왕복이 그 경로를 덮는다.[o]
T62-4-7식에 non-ASCII 문자열 literal을 가진 generated column이 모든 client에서 MySQL을 거쳐 그대로 되돌아오거나, 그럴 수 없음을 dbspec이 적는다. 원인: T62-4-6에서 mysql CLI(MySQL 8.4.11)로 관찰한 결과, information_schema.COLUMNS.GENERATION_EXPRESSION도 CHECK_CLAUSE처럼 non-ASCII literal의 UTF-8 bytes를 한 번 더 인코딩한다. 모든 client의 MySQL introspection이 그 column을 읽지만(engine/dbspec/introspect_mysql.go의 GENERATION_EXPRESSION), 이를 덮는 plan case가 없다. 이 항목을 등록할 때는 client로 실행하지 않았다. 완료 기준: 그런 generated column을 가진 plan case가 모든 client에서 MySQL을 거쳐 그대로 되돌아오고 make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check, make dbspec-plan-rust-check가 0으로 끝난다. 또는 dbspec 문법이 그런 식을 만들지 않고, docs/dbspec.md가 근거와 함께 그렇게 적는다.—Evidence: 분기는 두 번째다. dbspec은 generated column을 만들 수 없다. 열 줄은 이름, type, null, identity, default만 받고(engine/dbspec/parse.go의 841–842행 읽기), Go, PHP, PHP 확장, Rust, TypeScript, Python의 renderer는 generated column을 쓰지 않는다. 이 renderer들에서 STORED, VIRTUAL, AS (를 grep해도 열 DDL이 없고, 그들이 쓰는 GENERATED는 PostgreSQL의 GENERATED BY DEFAULT AS IDENTITY뿐이다. introspection이 GENERATION_EXPRESSION을 읽는 이유는, dbspec이 나타낼 수 없는 생성 식을 가진 MySQL 열을 미지원으로 보고하기 위해서다. 값이 비어 있지 않으면 모든 client는 type이나 text를 읽기 전에 그 열을 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, packages/orm-php-extension/src/introspect.c:1650. 관찰: .runtime/servers/env의 test server(MySQL 8.4.11)에서 scratch database orm_scratch_t62_4_7(뒤에 지움)의 table greet, label varchar(128) AS (concat(name, 'café')) STORED. GENERATION_EXPRESSION은 concat(`name`,_utf8mb4\'café\')(hex c383c2a9)이고, SHOW CREATE TABLE은 'café'를 보인다. 각 client가 public API로 그 database를 introspect한 결과는 같았다. table 하나와 미지원 열 greet.label, 이유 type varchar(128) utf8mb4 utf8mb4_0900_bin has no dbspec type이다. Go dbspec.Introspect(checkout 밖의 scratch module이 github.com/polyspec/orm을 이 checkout으로 replace한 go run), TypeScript introspectDbspec(빌드된 packages/orm-npm/dist, node), Python introspect_dbspec(packages/orm-python/src, PyMySQL), PHP Dbspec::introspect, PHP 확장 Native\Dbspec::introspect(packages/orm-php-extension/scripts/build.sh가 scratchpad에 build한 orm_dbspec.so), Rust examples/dbspec_introspect의 mysql(! column<TAB>greet<TAB>label을 출력하며, 이유는 출력하지 않는다. Rust 이유는 같은 형식 문자열이다). 모든 실행이 exit 0이다. Red: 없다. 어느 client도 다르게 판정하지 않았고 식 text를 보이지도 않았으므로 reader와 plan case는 바뀌지 않았다. Change: docs/dbspec.md와 docs/dbspec.ko.md는 dbspec이 generated column을 만들지 않는다고 문법과 introspection을 근거로 적는다. docs/dialects.md와 docs/dialects.ko.md는 어느 client도 식 text를 읽지 않는다고 적는다. CHANGELOG.md와 CHANGELOG.ko.md가 이 항목을 적고, 바뀐 twin의 ko source-sha256 표식을 다시 찍었다. 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, make repo-check 0. reader가 바뀌지 않아 dbspec introspect target은 실행하지 않았다.[o]
T62-4-8T62-4-6의 SHOW CREATE TABLE check reader가 모든 client에서 자체 unit case를 가진다. 원인: T62-4-6은 각 client에 reader를 더했다. 이 reader는 문자열 literal을 건너뛰고 괄호 짝을 맞춰 SHOW CREATE TABLE에서 CHECK 본문을 찾은 뒤 CHECK_CLAUSE처럼 escape한다(engine/dbspec/introspect_mysql.go의 mysqlShownCheck와 escapeMySQLClause, 그리고 PHP, PHP 확장, TypeScript, Python, Rust의 대응 코드). case는 'café'의 물리 왕복 하나뿐이다. ', \, )를 가진 literal, 한 table 안의 non-ASCII check 여럿, statement에 CHECK가 없는 이름은 실행되지 않는다. 완료 기준: 공유된 SHOW CREATE TABLE text와 기대하는 CHECK_CLAUSE 형식 본문의 집합을 각 client의 reader가 unit check에서 읽고, 모든 client가 같은 결과를 돌려준다.—Evidence: 공유 vector tests/dbspec/show-create.json에 case 아홉 개가 있다: non-ASCII literal, '를 가진 literal, \를 가진 literal, (와 )를 가진 literal, 한 table의 CHECK 둘(대상이 첫째이거나 둘째인 경우), \로 끝나는 literal, '를 가진 non-ASCII literal, statement에 없는 이름. 각 create text는 MySQL 8.4.11(.runtime/servers/env의 test 서버)의 SHOW CREATE TABLE 출력을 byte 그대로 옮긴 것이다. 관찰한 quoting: literal의 '는 \'로, \는 \\로 쓰이고, introducer _utf8mb4는 남으며, CHECK 본문은 CHECK ((...))로 감싸인다. 기대하는 본문은 SHOW CREATE 본문의 \와 '를 한 번 더 escape한 것이다. ASCII 본문에서는 MySQL이 저장한 CHECK_CLAUSE와 같고(HEX(CHECK_CLAUSE)로 확인), non-ASCII 본문에서는 두 번째 인코딩을 되돌린 그 clause와 같다. Reader: 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(검사를 위해 export, 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)와 Rust shown_checks(packages/orm-rust/orm-schema/src/dbspec/introspect/mysql.rs의 module show_create_tests, target dbspec-rust-check/show-create). Symbol snapshot contracts/symbols/typescript.json과 contracts/interfaces.json의 hash는 export 때문에 다시 기록했다. Red: 첫 실행에서 어느 reader도 기대한 본문과 다르지 않았으므로 reader는 바꾸지 않았다. vector가 실패할 수 있음을 보이려고 escapeMySQLClause가 \ escape를 건너뛰게 바꾸고 go test ./engine/dbspec -run TestMySQLShownCheckVectors를 실행하자 \를 가진 case 다섯 개가 실패했고 exit 1이었다. 그 뒤 escape를 되돌렸다. 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(re-record 뒤), make rust-fmt-check/clients가 각각 exit 0이다. 확장: packages/orm-php-extension/src/introspect.c의 shown_check는 PDO subclass 위의 Native\Dbspec::introspect로 도달하며, 그 PDO의 query, errorCode, errorInfo, fetchAll은 메서드 이름을 보고 호출된다(pdo_call, zend_call_method). 그래서 double이 catalog 답을 낼 수 있다. packages/orm-php-extension/tests/dbspec_show_create_test.php(target dbspec-php-extension-check/show-create)는 tests/dbspec/show-create.json의 catalog와 case마다 mysql_read의 catalog query 열 개에 답하며, 행은 메모리 SQLite statement가 돌려준다. 그래서 shown_check가 SHOW CREATE TABLE text를 읽는다. engine/dbspec/show_create_introspect_test.go(TestMySQLIntrospectionOfShownChecks, target dbspec-go-check/engine)는 같은 답을 fake Querier로 주고 Go의 Introspect를 실행한다. 두 쪽 모두 introspect된 table의 check 줄과 미지원 객체(kind, name, rule)를 case의 기대값과 비교하며, 없는 CHECK는 두 쪽 모두 check t.ck_missing has no CHECK in SHOW CREATE TABLE error를 기대한다. 확장의 첫 실행은 reason 본문이 다름을 보였다(C는 backtick, Go는 따옴표). 그래서 tests/dbspec/introspect.json처럼 reason은 rule로 비교한다. 첫 case의 non-ASCII check 이름은 두 쪽 모두 미지원(name.format)이므로, 본문은 읽히지만 문서에는 보이지 않는다. Red(확장): introspect.c의 escape_mysql_clause에서 \ escape를 뺐다. 그러자 make dbspec-php-extension-check/show-create가 literal-with-backslash, two-checks-target-non-ascii, literal-ends-with-backslash에서 실패하고 exit 2였다. 그 줄을 되돌렸고 git diff -- packages/orm-php-extension/src/introspect.c는 비어 있다. Green: make dbspec-php-extension-check가 case 아홉 개를 각각 두 번 실행하고 exit 0이다.[o]
T62-4-9모든 client의 introspection이 generated column을 generated column으로 보고한다. 원인: T62-4-7에서 생성식을 가진 MySQL column이 "type varchar(128) utf8mb4 utf8mb4_0900_bin has no dbspec type"이라는 사유와 함께 unsupported로 보고되는 것을 관찰했다(Go introspect_mysql.go, MysqlCatalog.php:66, introspect_mysql.ts:58, introspect_mysql.py:74, mysql.rs:79, introspect.c:1650). varchar(128)은 dbspec type이므로 이 사유는 원인을 잘못 적었다. column이 unsupported인 까닭은 생성식이다. 완료 기준: 모든 client가 그런 column을 생성식을 원인으로 적은 사유와 함께 보고하고, MySQL과 PostgreSQL introspection case가 그것을 고정하며, make dbspec-introspect-compare-check가 0으로 끝난다.—Evidence: 정정: 이 항목 전에는 공유 case가 사유를 비교하지 않았다. tests/dbspec/introspect.json은 [kind, table, name]만 비교했으므로, T62-4-8의 "reasons are compared by rule, as tests/dbspec/introspect.json does" 문장은 쓰인 시점에 사실이 아니었다. T62-4-8은 종결 상태이므로 고치지 않는다. 공유 변경: 미지원 항목은 네 번째 원소로 rule을 가질 수 있고, 사유는 그 rule을 포함해야 한다. Go, PHP, PHP 확장, TypeScript와 Rust harness가 이를 검사한다. case: mysql-generated-column(table gen, 열 label varchar(128) AS (concat(n, 'x')) STORED)과 postgres-generated-column(label varchar(128) GENERATED ALWAYS AS ("n"::text || 'x') STORED), 둘 다 ["column", "gen", "label", "a generated column"]을 기대한다. Red: reader 변경 전 make dbspec-introspect-check exit 2, make dbspec-introspect-php-check exit 2, make dbspec-introspect-ts-check exit 2, make dbspec-introspect-rust-check exit 2, make dbspec-introspect-php-extension-check exit 2. 처음 fixture의 name varchar(64)는 varchar가 renderer CHECK를 필요로 해서 모든 client에서 document differs로 실패했으므로 bigint 열로 바꾸었다. Change: MySQL과 PostgreSQL reader가 type보다 먼저 생성 여부를 보고 a generated column has no dbspec definition(SQLite reader의 문구)을 적는다. Go, PHP, TypeScript, Rust, PHP 확장의 reader를 모두 바꾸었다. docs/dialects.md와 ko 번역은 rule 원소가 있으면 비교한다고 적고, tests/dbspec/introspect.json의 note도 그렇게 적는다. Green: 위 여섯 introspect target(compare 포함) 모두 exit 0, make python-unit-check exit 0, make dbspec-compare-check exit 0(12 runs agree), make interface-check exit 0, make rust-fmt-check/clients exit 0. Python: packages/orm-python/tests/dbspec_introspect.py가 공유 case 13개(MySQL 5, PostgreSQL 3, SQLite 5)를 TEST_ENV의 서버에서 venv의 PyMySQL과 psycopg로 polyspec.orm.dbspec.introspect.introspect_dbspec을 통해 실행하며, case마다 database 또는 schema를 만들고 지운다. make dbspec-introspect-python-check가 이를 실행한다. packages/orm-python/tests/dbspec_introspect_runner.py는 make dbspec-introspect-compare-check의 Python runner이며, 이제 runner는 다섯 개다. Python 도구는 scripts/repo/scripts.mjs가 허용하는 packages/orm-python/ 아래에 둔다. 처음 tests/dbspec/introspect/ 아래에 둔 사본은 repo-check의 언어 규칙이 거부해서 옮겼다. Red: Python generated-column 변경을 되돌린 상태에서 make dbspec-introspect-python-check exit 2, 2 of 13 cases differ: mysql-generated-column은 got 'type varchar(128) utf8mb4 utf8mb4_0900_ai_ci has no dbspec type', postgres-generated-column은 got "type character varying(128) with collation 'default' has no dbspec type"로 실패했다. Green: 변경을 복구한 뒤 같은 target exit 0(introspect: 13 cases agree (5 mysql, 3 postgres, 5 sqlite))이고, Python runner를 넣은 make dbspec-introspect-compare-check도 exit 0이다(각 dialect 24849 bytes, Go 출력과 같음).[o]
T62-4-10T62-4-8이 더한 모든 test file과 fixture가 기능에 속하고, 그것을 실행하는 검사의 입력이다. 원인: T62-4-8은 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, tests/dbspec/show-create.json을 contracts/features.json에 적지 않고 더했고, T62-4-9는 그 tree 위에 commit되었다. 그 tree의 CI 실행은 job 여섯 개에서 실패했다. features/contracts(scripts/features/check.mjs)는 feature-stress-mysql-check, feature-stress-pg-sqlite-check, feature-verify-other, feature-helper-check, feature-verify-rust에서 Go file 둘, PHP file, TypeScript file에 대해 <file>: <language> test file belongs to no feature error 네 개를 보고했고, feature-unit-check의 test every tracked file selects a behaviour test or declares its scope는 어느 검사도 고르지 않는 file 여섯 개를 찾았다. 완료 기준: 여섯 file을 schema_definition의 형제 file처럼 선언하고, make feature-unit-check와 features/contracts 검사가 0으로 끝나며, 어떤 검사도 약화하지 않는다.—Evidence: contracts/features.json의 기능 schema_definition에서 tests/dbspec/show-create.json이 tests/dbspec/mermaid.json 옆의 fixtures에 들어가고, test file 다섯 개가 tests에 들어간다. 검증 입력: schema-go(make dbspec-go-check)에 Go file 둘과 fixture, schema-php(make dbspec-php-check)에 PHP test와 fixture, schema-typescript(make dbspec-ts-check)에 TypeScript test와 fixture, schema-php-extension(make dbspec-php-extension-check)에 확장 test와 fixture, schema-rust(make dbspec-rust-check, 그 dbspec-rust-check/show-create가 include_str!로 읽는다)에 fixture를 더했다. contracts/check-inputs.json: packages/orm-python/tests/test_dbspec_show_create.py를 실행하는 python-unit-check의 python-check에 fixture를 더했다. Python test 자체는 이미 packages/orm-python/**로 입력이었다. Red: 변경 전 make feature-unit-check exit 2(owners test의 실제 값이 여섯 file), make feature-stress-mysql-check는 features/contracts에서 CI와 같은 error 네 개로 exit 2. Green: make feature-unit-check exit 0(test 17, 19, 7개 통과), node scripts/features/check.mjs exit 0(18 feature contracts passed), make feature-stress-mysql-check는 features/contracts를 통과한다. 그 뒤 helper dbspec-apply-stress-mysql은 macOS에서 exit 2로 끝난다. 그 명령은 make feature-owner-check만 설정하는 TEST_ENV="$ORM_OWNER_TEST_ENV"를 넘기고, macOS의 sh는 file 이름 없는 .을 거부하기 때문이다. 이전 tree의 통과한 CI 실행은 Linux에서 같은 빈 값으로 helper를 실행했으므로, 이것은 이 회귀에 속하지 않는다. 선언한 명령도 통과한다: make dbspec-go-check(TestMySQLShownCheckVectors와 TestMySQLIntrospectionOfShownChecks 통과), make dbspec-php-check/show-create, make dbspec-php-extension-check/show-create, make dbspec-rust-check/show-create, make dbspec-ts-check(test 438개), make python-unit-check(test 123개)가 각각 exit 0. 또한 exit 0: make repo-check, make documents-check, make docs-rules-check.[o]
T62-5-1key, index, foreign key 또는 check 줄 뒤에서 구문 오류가 난 column 줄을 다른 구현들이 Go처럼 보고한다. 원인: Go는 column 줄의 order를 그 줄이 parse된 뒤(engine/dbspec/parse.go의 columnLine, c.done() 뒤)에만 보고한다. 그래서 구문 오류가 난 column 줄은 syntax만 받는다. primary key (id) 뒤의 name은 줄 끝에서 보고된다. PHP, TypeScript, Python, Rust와 PHP 확장은 줄을 parse하기 전에 첫 token(6 3)에 order를 보고하므로, 그런 줄은 order와 syntax를 함께 받는다. T62-5의 red 실행은 primary key 줄 뒤에서 이를 보였다(php 1 differs from go 1). 구문에 맞는 column 줄은 key 줄 뒤에서도 여섯 구현 모두 order만 받으므로, 결함은 구문 오류가 난 줄의 order 검사에만 있다. 공유 case는 정상 header를 쓰며, 잘못된 header는 실행하지 않았다. 이 case는 header에 관한 T62-5의 범위가 아니어서 뺐다. 완료 기준: tests/dbspec/cases.json의 공유 case가 그 출력을 Go 기준으로 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: Red: 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(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). 새 case에서 각 runner를 실행하니 php, typescript, python, rust와 php-extension은 구문 오류가 난 네 case(primary key, index, foreign key, check 줄)에서 syntax 앞에 첫 token의 order를 출력했고, column 줄이 정상인 네 case에서는 Go와 같았다(order만). go test ./engine/dbspec -run TestSharedVectors/invalid/column-after -count=1은 Go의 출력으로 여덟 case를 통과한다. 변경: 다섯 parser 각각은 column 줄이 parse된 뒤에 order를 보고하고, 줄에 구문 오류가 있거나 limit으로 parse가 멈춘 경우에는 보고하지 않는다. 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(새 column_line_in_order), packages/orm-php-extension/src/parse.c. Green: make dbspec-compare-check가 0으로 끝나고 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가 0으로 끝난다. Rust parser에 private method column_line_in_order가 생겨 make interface-check가 2로 끝났다. Rust symbol snapshot을 다시 기록하고(contracts/symbols/rust.json, 한 줄 추가) contracts/interfaces.json의 hash를 고쳤으며, 그 뒤 make interface-check가 0으로 끝난다. make feature-unit-check가 0으로 끝난다. rustfmt --edition 2021 --check가 parser.rs에서 0으로 끝난다. make documents-check와 make docs-rules-check가 0으로 끝난다.[o]
T62-5-2외래 key의 자식 색인 검사는 참조 열이 알려지지 않았거나 primary key 줄이 실패했을 때 Go처럼 실행된다. 원인: 코드를 읽어 찾았으며 실행하지는 않았다. Go의 선행 색인 검사는 자식의 열과 실패한 primary와 key 줄만 읽는데(validate.go의 foreignKey), TypeScript와 Python은 참조 열이 알려지지 않으면 그 검사 전에 반환하고, Python은 그 검사에서 실패한 primary key 줄을 무시한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 둘 다의 Go 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 원인은 TypeScript와 Python에 대해 맞다. Go는 참조 열 검사 뒤에 색인 검사를 실행하고 원천 위치 순으로 보고한다. 그래서 외래 key 이름의 색인 오류는 줄 뒤쪽의 알려지지 않은 참조 열보다 먼저 나오고, 같은 이름의 색인 오류는 set_null 오류보다 먼저 나온다. Go는 자식의 primary 줄이나 key 줄이 실패했을 때와 알려지지 않은 자식 열이 있을 때만 색인 검사를 건너뛴다. 알려지지 않은 참조 열은 색인 검사를 건너뛰게 하지 않는다. 공유 case foreign-key-unknown-reference-without-index, foreign-key-unknown-reference-with-index, foreign-key-unknown-reference-set-null, failed-primary-key-under-foreign-key와 failed-key-under-foreign-key(다섯 개, tests/dbspec/cases.json)가 Go의 출력을 고정하며, go test ./engine/dbspec -run TestSharedVectors가 통과한다. Red: 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(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). 검사는 runner마다 첫 차이에서 멈추므로 모든 runner의 출력을 저장해 비교했다. typescript와 python은 색인 오류 없는 case에서 색인 오류를, set_null case에서 set_null 오류를 빠뜨렸고, python은 primary key 실패 case에서 ! syntax 11 18 뒤에 ! foreign_key 12 15를 더 냈다. php, rust와 php-extension은 다섯 case 모두 Go와 같았다. Change: packages/orm-npm/src/dbspec/parse.ts(foreignKey)와 packages/orm-python/src/polyspec/orm/dbspec/parse.py(_foreign_key)는 알려지지 않은 자식 열에서 반환하는 것을 그대로 두고, 알려지지 않은 참조 열에서는 색인 검사와 set_null 검사(foreignKeyKeyChecks, _foreign_key_keys)를 실행한 뒤 반환한다. 짝과 type 검사만 건너뛴다. Python의 _foreign_key_keys는 실패한 primary key 줄 뒤의 색인 검사도 건너뛴다. Go와 TypeScript가 그렇게 한다. Green: make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다. go test ./engine/dbspec -run TestSharedVectors가 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와 make feature-unit-check가 0으로 끝난다. 선언이 바뀌지 않아 make interface-check는 snapshot을 다시 기록하지 않고 0으로 끝난다. make repo-check가 0으로 끝난다. Probe로 재현한 나머지 두 차이는 T62-5-4와 T62-5-5로 등록했다.[o]
T62-5-3make rust-fmt-check/clients가 Rust dbspec 원천에서 통과한다. 원인: T62-5가 rustfmt를 실행하지 않고 packages/orm-rust/orm-schema/src/dbspec/parser.rs(Ok(Parsed { … }) 식)와 dbspec/mod.rs를 commit해, format 검사가 T62-5가 쓴 줄에서 실패한다. 완료 기준: 원천을 repository 설정의 rustfmt로 정리하고 make rust-fmt-check/clients가 0으로 끝난다.—Evidence: Red: 수정 전 make rust-fmt-check/clients는 2로 끝났다(cargo fmt는 1). packages/orm-rust에서 cargo fmt --all --check -- -l은 orm-schema/src/dbspec/mod.rs(validate::validate(...) 호출)와 orm-schema/src/dbspec/parser.rs(Ok(Parsed { … }) 식)만 나열했다. 변경: orm-schema의 edition(2021)으로 rustfmt --edition 2021을 이 두 파일에만 write 모드로 실행했다. 설정은 packages/orm-rust/rustfmt.toml이며 rustfmt가 각 파일에서 찾는다. git diff --stat은 이 두 파일만 보여 주며 18 insertion, 2 deletion이다. Green: make rust-fmt-check/clients가 0으로 끝나고 PASS rust-fmt를 출력한다. make dbspec-rust-check가 0으로 끝난다. make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다. make interface-check는 snapshot을 다시 기록하지 않고 0으로 끝난다. 선언을 바꾸지 않아 Rust symbol snapshot이 그대로 맞는다(rust: 2830 native symbols inspected).[o]
T62-5-4참조 table이 정의되지 않았거나 참조 열 수가 외래 key 열 수와 다를 때, TypeScript와 Python의 외래 key 자식 색인 검사와 set_null 검사는 Go처럼 실행된다. 원인: 코드를 읽은 것이 아니라 probe를 실행해 찾았다. team_id에 색인이 없는 자식 table의 foreign key fk_users_team (team_id) references nowhere (id)에서 Go는 두 오류, 즉 table "nowhere" is not a table of this document or a used table과 외래 key 이름의 색인 누락을 보고하지만, TypeScript와 Python은 앞의 것만 보고한다. foreign key fk_users_team (team_id, other) references teams (id)에서 Go는 개수 오류와 색인 누락을 보고하고, TypeScript와 Python은 개수 오류만 보고한다. 정의되지 않은 target에서의 조기 반환(packages/orm-npm/src/dbspec/parse.ts의 foreignKey, packages/orm-python/src/polyspec/orm/dbspec/parse.py의 _foreign_key)과 개수 불일치의 조기 반환이 foreignKeyKeyChecks를 건너뛴다. PHP, Rust와 PHP 확장은 두 경우 모두 Go와 같다. 완료 기준: tests/dbspec/cases.json의 공유 case가 두 document의 Go 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: Red: tests/dbspec/cases.json에 공유 case 다섯 개를 더했다. foreign-key-undefined-table-without-index, foreign-key-undefined-table-with-index(색인이 있는 대조군), foreign-key-undefined-table-set-null, foreign-key-column-count-without-index, foreign-key-column-count-set-null이다. go test ./engine/dbspec -run TestSharedVectors는 Go의 diagnostic에서 옮긴 오류로 통과한다. 정의되지 않은 table은 target 위치에 이름을 보고하고, 색인 오류와 set_null 오류는 원천 순서대로 외래 key 이름 위치에 온다. 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(typescript 1 differs from go 1 in invalid/foreign-key-undefined-table-without-index at line 1052). 각 runner를 따로 실행해 Go의 출력과 비교했다. typescript와 python은 이른 반환이 건너뛰는 네 case에서 색인 오류와 set_null 오류를 빠뜨렸고, php, rust와 php-extension은 다섯 case 모두 Go와 같았다. Change: packages/orm-npm/src/dbspec/parse.ts(foreignKey)와 packages/orm-python/src/polyspec/orm/dbspec/parse.py(_foreign_key)는 target이 정의되지 않았거나 개수가 다를 때 foreignKeyKeyChecks와 _foreign_key_keys를 실행하고, 짝과 type 검사만 건너뛴다. Go의 target == nil과 len(f.columns) != len(f.references) 분기가 그렇게 한다. Go를 읽어 보니 이름이 잘못된 target도 v.ref에서 nil target이 되어, 같은 이른 반환이 그 경우의 key 검사를 건너뛰고 있었다. foreign-key-malformed-target-without-index가 이를 고정한다(Go: 이름 위치의 색인 오류와 target 위치의 name.format). 변경 전 TypeScript build는 이 case에서 달랐다. Python parser도 같은 이른 반환이 있었지만 변경 전에 probe하지는 않았다. Green: make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다. go test ./engine/dbspec -run TestSharedVectors가 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와 make repo-check가 0으로 끝난다. make interface-check는 선언이 바뀌지 않아 snapshot을 다시 기록하지 않고 0으로 끝난다. 변경 뒤 runner probe는 모든 runner에서 다른 case를 보이지 않는다. make documents-stamp, make documents-check와 make docs-rules-check가 0으로 끝나며, docs-rules가 영문 문서 39개와 한국어 번역 39개를 통과한다.[o]
T62-5-5알려지지 않은 자식 열이 있는 외래 key의 set_null 검사와 색인 검사는 Go처럼 실행된다. 대상은 Rust, TypeScript, Python, PHP와 PHP 확장이다. 원인: 코드를 읽은 것이 아니라 probe를 실행해 찾았다. team_id가 not null인 table의 foreign key fk_users_team (nope, team_id) references teams (id, code) on delete set_null에서 Go는 set_null 오류와 알려지지 않은 열 오류를 보고한다. TypeScript, Python, PHP와 PHP 확장은 자식 열이 알려지지 않으면 반환하므로 알려지지 않은 열 오류만 보고한다. Rust(packages/orm-rust/orm-schema/src/dbspec/validate.rs의 foreign_key)는 set_null 오류와 함께 외래 key 이름의 색인 오류도 보고하는데, 색인 검사가 알려지지 않은 자식 열을 건너뛰지 않기 때문이다. Go는 자식 열이 알려지지 않으면 이 검사를 건너뛴다(validate.go의 known). foreign key fk_users_team (nope) references teams (id)에서 자식에 key가 없을 때도 Go는 알려지지 않은 열 오류만 보고하지만, Rust는 색인 누락도 보고한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 두 document의 Go 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 원인은 맞다. Go의 foreignKey(validate.go)가 known으로 색인 검사를 건너뛰는 것을 읽어 확인했다. Red: 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(php 1 differs from go 1 in invalid/foreign-key-unknown-child-set-null at line 1071). 검사는 runner마다 첫 차이에서 멈추므로 모든 runner의 출력을 저장해 Go와 비교했다. php, typescript, python과 php-extension은 그 case에서 set_null 오류(! foreign_key 14 15)를 빠뜨렸다. rust는 그 case와 foreign-key-unknown-child-set-null-nullable에서 색인 오류(! foreign_key 14 15)를 더 냈고, foreign-key-unknown-child-without-index에서는 ! foreign_key 12 15를 더 냈다. tests/dbspec/cases.json의 공유 case foreign-key-unknown-child-set-null, foreign-key-unknown-child-set-null-nullable, foreign-key-unknown-child-without-index는 Go의 출력을 기대값으로 고정하며, go test ./engine/dbspec -run TestSharedVectors -count=1이 0으로 끝난다. 변경: packages/orm-rust/orm-schema/src/dbspec/validate.rs는 자식 열이 모두 알려지고 겹치지 않을 때만 외래 key의 색인 검사를 실행한다(Go처럼). packages/orm-npm/src/dbspec/parse.ts와 packages/orm-python/src/polyspec/orm/dbspec/parse.py는 알려지지 않은 자식 열에서도 key 검사를 실행하고 색인 검사만 건너뛴다(set_null 검사는 알려진 자식 열에 대해 실행한다). packages/orm-php/src/Dbspec/Parser.php와 packages/orm-php-extension/src/parse.c는 자식 열이 알려졌는지 나타내는 flag로 같은 일을 한다. Rust 규칙 test packages/orm-rust/orm-schema/tests/dbspec_rules.rs(foreign-key-rules)는 fk_e (x)의 색인 오류를 더 기대하지 않는다. 그 기대값은 옛 Rust 동작을 고정하고 있었다. TypeScript private 메서드 foreignKeyKeyChecks에 매개변수가 하나 늘어 typescript snapshot을 다시 기록했다. contracts/symbols/typescript.json과 contracts/interfaces.json의 그 hash만 바뀌었고 다른 snapshot은 바뀌지 않았다. Green: make dbspec-compare-check가 0으로 끝나고 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와 make repo-check가 0으로 끝난다. probe가 재현한 차이 하나는 T62-5-6으로 등록했다.[o]
T62-5-6알려지지 않은 자식 열이 있는 외래 key의 참조 열 짝과 type 검사를 TypeScript, Python, Rust, PHP와 PHP 확장도 Go처럼 실행한다. 원인: 코드를 읽은 것이 아니라 probe를 실행해 찾았다. teams가 primary key (id)일 때 foreign key fk_users_team (nope, team_id) references teams (id, code)에서 Go는 외래 key 이름에 the referenced columns are not the primary key or a unique key of 'teams'를 보고하고, 나머지 다섯 구현은 그 검사를 건너뛴다. 이들의 짝 검사는 자식 열이 알려지지 않으면 일찍 반환한다(TypeScript와 Python은 resolved, Rust는 complete, PHP와 PHP 확장은 $known과 known). Go는 자식 열과 관계없이 참조 열의 key 검사를 하고, 알려진 자식 열에 대해 type 검사를 한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 원인은 맞다. Go의 foreignKey(validate.go)는 참조 열이 모두 알려졌을 때 개수, 짝과 type 검사를 하고, type 검사는 알려진 자식 열에만 하며, 색인 검사는 자식 열이 모두 알려져야 한다. Red: tests/dbspec/cases.json에 공유 case 세 개를 더했다. foreign-key-unknown-child-referenced-key, foreign-key-unknown-child-referenced-type과 대조군 foreign-key-unknown-child-unknown-reference이며, 기대값은 Go의 출력이다. go test ./engine/dbspec -run TestSharedVectors -count=1이 통과한다. 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(php 1 differs from go 1 in invalid/foreign-key-unknown-child-referenced-key at line 1078). 각 runner의 전체 출력을 저장해 Go와 비교했다. php, typescript, python, rust와 php-extension은 첫 case에서 ! foreign_key 14 15(짝 오류)를 빠뜨렸고, referenced-type case에서는 그 오류와 같은 위치의 type 오류를 빠뜨렸다. 대조군은 모든 runner에서 Go와 같았다. Change: packages/orm-npm/src/dbspec/parse.ts(foreignKey, foreignKeyKeyChecks)와 packages/orm-python/src/polyspec/orm/dbspec/parse.py(_foreign_key, _foreign_key_keys)는 알려지지 않은 자식에는 placeholder를 두고, 개수 검사를 자식과 관계없이 하며, 참조 열이 모두 알려졌을 때 짝과 type 검사를 한다. 알려지지 않은 자식의 type 검사는 건너뛰고, type 검사는 첫 불일치에서 멈춘다. packages/orm-rust/orm-schema/src/dbspec/validate.rs(foreign_key)는 자식과 참조 열마다 Option을 두어 짝을 맞춘다. packages/orm-php/src/Dbspec/Parser.php(checkForeignKeyTargets)와 packages/orm-php-extension/src/parse.c(check_foreign_key_targets)는 짝 검사를 참조 열에만 맞추고, 알려지지 않았거나 type이 invalid인 자식은 type 검사에서 제외한다. C의 dfk_rec에서 쓰지 않던 known 필드를 없앴다. Interface: private TypeScript 메서드 foreignKeyKeyChecks의 매개변수 type이 바뀌어 contracts/symbols/typescript.json을 node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language typescript로 다시 기록하고, contracts/interfaces.json의 그 hash를 갱신했다. 다른 snapshot은 바뀌지 않았다. Green: make dbspec-compare-check가 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와 make repo-check가 0으로 끝난다. 추가 probe 여섯 형태를 모든 runner와 Go에서 실행했다. PHP, TypeScript, Python과 PHP 확장은 Go와 모두 같았고, Rust는 두 가지가 달라 T62-5-7과 T62-5-8로 등록했다.[o]
T62-5-7목표 table이 정의되지 않은 외래 key의 열 개수를 Go처럼 Rust에서도 검사한다. 원인: 코드를 읽은 것이 아니라 T62-5-6 뒤의 probe로 찾았다. foreign key fk_users_team (team_id, id) references nowhere (id)에서 자식 두 열을 앞세우는 색인이 없을 때 Go는 외래 key 이름에 색인 오류를, nowhere에 table 오류를 보고하고 개수 오류는 보고하지 않는다. Go의 foreignKey가 target이 nil이 아닐 때만(validate.go의 target != nil && !target.failed) 개수 검사를 하기 때문이다. Rust(validate.rs의 foreign_key)는 개수 검사를 !failed_target으로만 막아서, 같은 이름 위치에 the foreign key lists a different number of columns and referenced columns 오류를 추가로 보고한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 이 문서의 Go 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 원인은 맞다. Go의 foreignKey(validate.go)는 target != nil && !target.failed일 때만 개수 검사를 하고, Rust의 foreign_key는 표 줄이 실패한 target에서만 개수 검사를 건너뛰어 정의되지 않은 target에도 개수 오류를 보고했다. Red: tests/dbspec/cases.json에 공유 case foreign-key-unknown-table-count를 더했고, 기대값은 Go의 출력이다(외래 key 이름의 색인 오류 foreign_key 12 15와 nowhere의 table 오류 foreign_key 12 54). go test ./engine/dbspec -run TestSharedVectors -count=1이 통과한다. 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(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)는 target이 정의되어 있고 실패하지 않았을 때만 개수 검사를 한다. 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와 make repo-check가 모두 0으로 끝난다. 선언이 바뀌지 않아 snapshot은 다시 기록하지 않았다.[o]
T62-5-8자식 열의 type이 invalid인 외래 key의 색인 검사를 Go처럼 Rust에서도 실행한다. 원인: 코드를 읽은 것이 아니라 T62-5-6 뒤의 probe로 찾았다. team_id nosuch(알 수 없는 type이라 column 줄에 구문 오류가 있음)이고 team_id를 앞세우는 색인이 없는 table에서 foreign key fk_users_team (team_id) references teams (id)는 Go가 외래 key 이름에 색인 오류를 보고한다. Go의 known은 자식 열의 type이 아니라 열의 존재로 정하기 때문이다. Rust(validate.rs의 foreign_key)는 줄이 실패한 열을 Lookup::Unresolved로 읽어 자식이 알려지지 않은 것으로 보므로 아무것도 보고하지 않는다. PHP, PHP 확장, TypeScript와 Python은 Go와 같이 보고한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 이 문서의 Go 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 원인은 맞다. Go의 columnRef는 줄이 type 단어까지 구문상 맞고 type이 invalid인 열을 찾는다. Go는 resolveType이 실패한 뒤에도 그 열을 추가하므로 자식이 알려진 것으로 남는다. Rust의 column()은 그런 줄에 None을 돌려주어 열을 unresolved로 표시했다. Red: tests/dbspec/cases.json에 공유 case foreign-key-invalid-child-type-index를 더했고, 기대값은 Go의 출력이다(10:11의 type 오류와 foreign_key 12 15의 색인 오류). go test ./engine/dbspec -run TestSharedVectors -count=1이 통과한다. 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(rust 1 differs from go 1 in invalid/foreign-key-invalid-child-type-index at line 1089, Go ! foreign_key 12 15, 이 case에서 Rust는 오류가 없었다). Change: packages/orm-rust/orm-schema/src/dbspec/parser.rs는 구문은 맞고 type이 invalid인 열 줄의 이름을 기록하며(typeless와 table_typeless), packages/orm-rust/orm-schema/src/dbspec/validate.rs(foreign_key)는 그런 자식을 type 없이 알려진 것으로 두어 색인 검사를 실행하고 type 검사에서는 건너뛴다. 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와 make repo-check가 모두 0으로 끝난다. Interface: private Rust 구조체 Parsed, Parser, Scope와 TableScope에 필드가 생기고 TableScope에 메서드가 생겨, contracts/symbols/rust.json을 make 환경에서 다시 기록했다(node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language rust). contracts/interfaces.json의 그 hash도 갱신했다. 공개 선언은 바뀌지 않았다. 수정 뒤 probe가 추가 차이를 찾았다. type이 invalid인 자식을 가진 set_null 외래 key를 Rust가 null 검사하지 않는다(Go는 ! foreign_key 13 15, set_null requires every foreign key column to be null을 보고한다). 이는 T62-5-9로 등록했다.[o]
T62-5-9자식 열의 type이 invalid인 set_null 외래 key의 열 null 검사를 Go처럼 Rust에서도 실행한다. 원인: 코드를 읽은 것이 아니라 T62-5-8 뒤의 probe로 찾았다. team_id nosuch이고 index ix_users_team (team_id)가 있으며 foreign key fk_users_team (team_id) references teams (id) on delete set_null인 table에서 Go는 외래 key 이름에 set_null requires every foreign key column to be null을 보고한다. Go의 set_null 검사가 존재하는 자식 열을 모두 읽기 때문이다(c != nil && c.null == nil). Rust(validate.rs의 foreign_key)의 set_null 검사는 type이 맞아 열로 풀린 자식만 읽으므로 아무것도 보고하지 않는다. 완료 기준: tests/dbspec/cases.json의 공유 case가 이 문서의 Go 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 원인은 맞다. Go의 foreignKey(validate.go)는 존재하는 자식 열 모두에 set_null 검사를 하고(c != nil && c.null == nil), columnRef는 type이 invalid인 열을 돌려주므로 null이 아닌 그런 자식은 오류다. Rust의 set_null 검사는 type이 맞아 열로 풀린 자식만 읽었다. Red: tests/dbspec/cases.json에 공유 case foreign-key-invalid-child-set-null을 더했고, 기대값은 Go의 출력이다(10:11의 type 오류와 foreign_key 13 15, set_null requires every column to be null). go test ./engine/dbspec -run TestSharedVectors -count=1이 통과한다. 수정 전 make dbspec-compare-check는 dbspec-compare/agreement에서 2로 끝났다(rust 1 differs from go 1 in invalid/foreign-key-invalid-child-set-null at line 1092, Go ! foreign_key 13 15, 이 case에서 Rust는 오류가 없었다). 나머지 네 runner(PHP, TypeScript, Python과 PHP 확장)를 같은 입력으로 실행했고, 이 case의 블록이 모두 Go와 같았으므로 차이는 Rust에만 있었다. Change: packages/orm-rust/orm-schema/src/dbspec/parser.rs는 type이 invalid인 열의 이름과 null 여부를 함께 기록하고(typeless와 table_typeless), packages/orm-rust/orm-schema/src/dbspec/validate.rs(foreign_key)는 존재하는 자식 열 모두의 null 여부를 두고 그 값으로 set_null 검사를 한다. Green: make dbspec-compare-check가 0으로 끝나고 dbspec-compare/agreement 단계가 통과한다. Interface: private Rust 구조체의 typeless 필드가 이름과 null 여부의 짝으로 type이 바뀌어, contracts/symbols/rust.json을 make 환경에서 다시 기록했다(node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language rust). contracts/interfaces.json의 hash는 9501c34a...에서 2523fe80...으로 바꿨다. 공개 선언은 바뀌지 않았다.[o]
T62-6반복된 settings 블록을 모든 구현이 Go처럼 처리한다. 원인: Go는 표의 첫 settings 블록을 유지하고, 반복된 블록은 분리된 블록으로 읽는다(engine/dbspec/parse.go의 tableLine과 duplicate). 그 줄은 첫 블록의 줄과 같은 구문 검사(알 수 없는 setting, 이름 구문, lex 오류)를 받지만 검증되지 않는다. Go는 반복된 블록의 키워드에 order를 보고하되, header 줄이 정확히 settings {일 때만 보고하며, 형식이 틀린 반복 header는 자기 구문 오류만 보고한다. 다른 구현은 반복된 블록의 줄을 첫 블록에 덧붙여 검증했고(두 번째 entity 줄은 setting 오류, 그 안의 없는 열도 보고), 모든 반복 header에 order를 보고했다. Python은 {가 없는 settings 줄에서 IndexError로 실패했고, Rust는 {가 없는 settings를 열로 읽었다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 동작을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 위 원인은 수정 전에 바로잡았다. 처음 원인은 형식이 틀린 반복 header가 order 오류를 보고하지 않는다는 점과, 다른 구현이 반복된 줄을 첫 블록에 둔다는 점을 적지 않았다. Red: 수정 전 make dbspec-compare-check는 2로 끝났다(dbspec-compare/agreement, php 1 differs from go 1 in invalid/repeated-settings-block-invalid-lines). 나머지 다섯 runner는 반복 block case에서 Go와 달랐고, {가 없는 case에서 Python은 IndexError로 끝났으며 Rust는 그 줄을 열로 읽었다. Green: make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다. go test ./engine/dbspec -run TestSharedVectors가 통과한다. 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와 make docs-rules-check가 0으로 끝난다. make interface-check는 처음에 2로 끝났다. PHP, Rust, TypeScript의 symbol snapshot이 private field와 method를 적으므로 새 private 선언이 그것을 바꾸었다(공개 symbol은 바뀌지 않았다). node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language <php|rust|typescript>로 다시 기록하고 hash를 contracts/interfaces.json에 적었으며, 그 뒤 make interface-check가 0으로 끝난다.[o]
T62-6-1문서 끝에서도 닫히지 않은 settings 블록은 모든 구현이 가장 안쪽의 열린 중괄호에서 한 번만 보고하며, Go처럼 보고한다. 원인: T62-6의 probe(settings {와 entity 줄 하나, 그리고 문서 끝)에서 찾았다. T62-6을 실패시키므로 tests/dbspec/cases.json에는 넣지 않았다. Go는 settings의 {에 block opened at this {(syntax 6 12)만 보고하지만, PHP, PHP 확장, TypeScript, Rust와 Python은 표의 {에도 the table block is not closed(syntax 3 13)를 더 보고한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 문서 끝의 열린 settings 블록에 대한 Go 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: 원인은 engine/dbspec/parse.go의 parseStructure에서 확인했다. 끝에서 가장 안쪽의 열린 {에 block opened at this %s is not closed를 한 번 덧붙인다. Red: 수정 전 make dbspec-compare-check는 2로 끝났다(dbspec-compare/agreement, php 1 differs from go 1 in invalid/settings-block-open-at-end, 추가된 ! syntax 3 13). 나머지 다섯 runner를 같은 문서로 실행하자 typescript, python, rust, php-extension도 같은 추가 줄을 냈다. Green: 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의 닫는 분기와 packages/orm-rust/orm-schema/src/dbspec/parser.rs의 report_open_blocks가 가장 안쪽의 {만 보고하도록 고쳤다. 열린 settings block은 자기 {만 보고한다. 이중 보고를 고정하던 단위 case 두 개를 바꾸었다(packages/orm-rust/orm-schema/tests/dbspec_rules.rs의 unclosed-settings-and-table, packages/orm-npm/tests/dbspec-rules.mjs의 unclosed settings block). go test ./engine/dbspec -run TestSharedVectors는 ok를 출력한다. 같은 변경이 table과 diagram의 문서 끝 오류에도 닿으므로, 마지막 줄에 이미 구문 오류가 있는 열린 table과 diagram에 대해 Go의 출력을 고정하는 공유 case를 두 개 더 두었다. table-block-open-with-error-line-at-end(3번째 줄 13열의 table {와 6번째 줄 20열의 unexpected '(')와 diagram-block-open-with-error-line-at-end(8번째 줄 14열의 diagram {와 9번째 줄 16열의 unexpected '(')다. 이 case를 포함해 make dbspec-compare-check는 12 runs agree를 출력하므로, 모든 runner가 두 case에서도 이미 Go와 일치했고 수정이 필요한 runner는 없었다. 수정 뒤 종료 코드: 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(snapshot이 바뀌지 않아 다시 기록하지 않았다), make feature-unit-check 0, make repo-check 0.[o]
T62-6-2문서 끝에서 { 없이 열린 settings 줄이 마지막으로 연 block을 모든 구현이 Go처럼 보고한다. 원인: T62-6-1의 probe에서 찾았다(primary key 줄 뒤에 { 없는 settings를 두고 문서를 끝냈다). Go는 키워드에 block opened at this settings(syntax 6 3)를, 줄 끝에 syntax 6 11을 보고한다. PHP, TypeScript, Python과 PHP 확장은 이미 구문 오류가 있는 줄의 두 번째 구문 오류를 버리므로 syntax 6 11만 보고하고, Rust는 Go처럼 두 개를 보고한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: Go의 동작은 engine/dbspec/parse.go에서 확인했다(tableLine과 settingsLine은 {가 없으면 키워드에서 settings block을 열고, cursor.fail은 줄 끝에 line ends, expected '{'를 더하며, parseStructure는 키워드에 block opened at this settings is not closed를 더한다). 공유 case settings-without-brace-at-end를 Go의 출력으로 추가했고, go test ./engine/dbspec -run TestSharedVectors는 ok를 출력한다. Red: 수정 전 make dbspec-compare-check는 2로 끝났다(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). 같은 문서로 여섯 runner를 직접 실행하자 php, typescript, python, php-extension이 같은 차이를 냈고, rust는 Go와 일치했다. Green: 문서 끝의 block opened 오류는 줄당 구문 오류 하나로 거르는 처리를 거치지 않는다(packages/orm-php/src/Dbspec/Parser.php, packages/orm-php-extension/src/parse.c, packages/orm-npm/src/dbspec/parse.ts의 unclosed와 packages/orm-python/src/polyspec/orm/dbspec/parse.py의 _unclosed). 옛 동작을 고정한 단위 test는 없었다. make interface-check는 처음에 2로 끝났다. 새 private method가 PHP와 TypeScript symbol snapshot(contracts/symbols/php.json과 contracts/symbols/typescript.json, 각 한 줄)에 들어갔기 때문이다. 두 snapshot을 node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language <php|typescript>로 다시 기록하고 hash를 contracts/interfaces.json에 적었으며, 그 뒤 make interface-check는 0으로 끝난다. 수정 뒤 종료 코드: 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문서 끝에서 열려 있는 settings block 안의 오류 줄이나 표 줄은 Go처럼 settings 상태를 유지하고, 문서 끝의 보고는 settings의 { 하나뿐이다. 원인: 수정된 tree에서 T62-6-1의 probe 두 개로 찾았다. 하나는 8번째 줄에 알 수 없는 setting bogus x가 있는 문서이고(여섯 구현이 모두 일치한다), 다른 하나는 8번째 줄에 table other {가 있는 문서다. Go는 그 줄 자체의 오류와 settings {의 syntax 6 12 하나를 보고한다. 표 줄에서 Rust는 Go의 setting 8 1 대신 column 8 7, key 8 7, syntax 8 13을 보고하며, 나머지 다섯 구현은 Go와 일치한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: Go의 동작은 engine/dbspec/parse.go에서 확인했다. settingsLine은 settings 줄의 키워드를 setting 이름 토큰으로 읽고, 그 위치에 unknown setting(setting 8 1)을 보고하며, settings block은 열린 채로 남으므로 그 안의 table 줄은 block을 여는 줄이 아니다. Rust의 setting_line은 키워드를 읽기 전에 unclosed를 불러, settings block을 닫고 table other {를 표 줄로 읽었다(column 8 7, key 8 7, syntax 8 13). 공유 case settings-block-open-with-table-line(6번째 줄 12열의 settings {와 setting 8 1)을 Go의 출력으로 추가했고, go test ./engine/dbspec -run TestSharedVectors는 ok를 출력한다. Red: 수정 전 make dbspec-compare-check는 2로 끝났다(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). 같은 문서로 여섯 runner를 직접 실행하자 차이는 rust에만 있었다. Green: packages/orm-rust/orm-schema/src/dbspec/parser.rs의 setting_line은 더 이상 unclosed를 부르지 않으므로, settings block 안의 table이나 diagram 줄은 Go처럼 알 수 없는 setting이다. 다른 runner는 바꾸지 않았다. 옛 동작을 고정한 단위 test는 없었다. 수정 뒤 종료 코드: 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(snapshot이 바뀌지 않았다), make feature-unit-check 0, make repo-check 0.[o]
T62-6-4열린 table block 안의 table 줄을 모든 구현이 Go처럼 보고한다. 원인: table users {의 열 줄 뒤에 table other {를 둔 probe(닫는 }가 있는 문서와 없는 문서)에서 찾았다. Go는 표 줄의 {에 syntax 5 13을 보고한다. PHP, TypeScript, Python과 PHP 확장은 그 앞에 name.format 5 1과 type 5 7을 더 보고하고, Rust는 Go의 syntax 5 13 대신 key 5 7(닫지 않은 문서에서는 column 5 7도)을 보고하며, 닫힌 문서에서는 Go가 보고하지 않는 syntax 3 13을 보고한다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: Go의 동작은 engine/dbspec/parse.go에서 확인했다. tableLine은 닫는 }가 아닌 table 줄을 columnLine으로 보내므로, table other {는 type other의 열 table이고, 줄이 구문에 맞지 않아 parser는 {에 syntax 5 13만 보고한다. Go는 이 줄에 이름이나 type 오류를 보고하지 않는다. 고치기 전 현재 binary로 다시 확인했다(build directory를 남긴 compare 실행): php, typescript, python, php-extension은 syntax 5 13 앞에 name.format 5 1과 type 5 7을 더했고, rust는 닫힌 문서에서 syntax 3 13을 보고했으며 닫히지 않은 문서에서는 syntax 5 13 대신 key 5 7을(닫히지 않은 문서에서는 column 5 7도) 보고했다. 공유 case table-line-inside-open-table(닫힌 문서, name.duplicate 6 3과 column 6 10 포함)과 table-line-inside-open-table-at-end를 Go의 출력으로 추가했고, go test ./engine/dbspec -run TestSharedVectors는 ok를 출력한다. Red: 수정 전 make dbspec-compare-check는 2로 끝났다(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: 구문 오류가 난 열 줄은 Go처럼 구문 오류만 남긴다. packages/orm-php/src/Dbspec/Parser.php의 keepSyntaxSince, packages/orm-php-extension/src/parse.c의 keep_syntax_since, packages/orm-npm/src/dbspec/parse.ts의 keepSyntaxSince, packages/orm-python/src/polyspec/orm/dbspec/parse.py의 _column, packages/orm-rust/orm-schema/src/dbspec/parser.rs의 column_line_in_order를 고쳤다. Rust의 table_line은 table 줄에 unclosed를 부르지 않으므로, 그 줄은 Go처럼 열 줄이다. TypeScript 단위 case columns count code points는 구문 오류가 난 줄의 옛 열 오류를 고정하고 있어 syntax 4 32만 기대하도록 바꿨다. 공유 case column-unknown-type-with-syntax-error는 other other {에 대해 Go의 출력(key 3 7, column 4 10, syntax 5 15)을 고정한다. 다른 runner는 고치기 전에 그 앞에 type 5 9를 더 보고했었다. make interface-check는 처음에 2로 끝났다. 새 private method keepSyntaxSince(PHP와 TypeScript)가 symbol snapshot(contracts/symbols/php.json과 contracts/symbols/typescript.json, 각 한 줄)에 들어갔기 때문이다. 두 snapshot을 node tests/go-run.mjs interfaces-check ./tests/interfaces/check --record --language <php|typescript>로 다시 기록하고 hash를 contracts/interfaces.json에 적었다. 수정 뒤 종료 코드: 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열린 table block 안의 diagram 줄을 Rust dbspec parser가 Go처럼 보고한다. 원인: 현재 binary에서 T62-6-4의 probe로 찾았다(table users {의 열 줄 뒤에 diagram main {과 users at 0 0을 두고 }를 덧붙였다. 실패하므로 tests/dbspec/cases.json에 넣지 않았다). Go는 key 3 7, column 4 10, syntax 5 14, syntax 6 12를 보고한다. PHP, TypeScript, Python, PHP 확장은 같은 네 오류를 보고하고, Rust는 표의 {에 syntax 3 13과 column 4 10만 보고하며 diagram 줄의 구문 오류 두 개를 빠뜨린다. Rust table_line은 diagram 줄에 여전히 unclosed를 불러 표를 닫는다. T62-6-4의 table 줄만 예외다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: Go의 동작은 engine/dbspec/parse.go에서 확인했다. tableLine은 닫는 }가 아닌 diagram 줄을 columnLine으로 보내므로, diagram main {는 type main의 열 diagram이고, parser는 줄이 구문에 맞지 않아 {에 syntax 5 14를, 다음 줄(users at 0 0)의 0에 syntax 6 12를 보고한다. 공유 case diagram-line-inside-open-table(key 3 7, column 4 10, syntax 5 14, syntax 6 12)을 Go의 출력으로 추가했고, go test ./engine/dbspec -run TestSharedVectors는 ok를 출력한다. Red: 수정 전 make dbspec-compare-check는 2로 끝났다(dbspec-compare/agreement, rust 1 differs from go 1 in invalid/diagram-line-inside-open-table). 같은 문서로 여섯 runner를 직접 실행하자 차이는 rust에만 있었다. rust는 syntax 3 13(표의 {)을 보고했고 diagram 줄의 syntax 5 14와 syntax 6 12를 보고하지 않았다. Green: packages/orm-rust/orm-schema/src/dbspec/parser.rs의 table_line은 어떤 줄에도 unclosed를 부르지 않으므로, 열린 표 안의 table과 diagram 줄은 Go처럼 열 줄이다(한국어 주석이 이를 적는다). diagram context의 unclosed는 그대로 쓰인다. 다른 runner는 바꾸지 않았다. 옛 동작을 고정한 단위 test는 없었다. 수정 뒤 종료 코드: 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(snapshot이 바뀌지 않았다), make feature-unit-check 0, make repo-check 0.[o]
T62-6-6열 줄의 잘못된 type 인자 목록(varchar(64 x, varchar(64,), decimal(10 2))을 PHP, Rust와 PHP 확장 dbspec parser가 Go처럼 보고한다. 원인: 현재 binary에서 열 줄 조합을 probe하다 찾았다(닫힌 표에서 실행했고, 실패하므로 tests/dbspec/cases.json에 넣지 않았다). Go는 type 인자 목록이 받지 못하는 첫 token에만 syntax를 보고한다( name varchar(64 x의 x, syntax 5 19). TypeScript와 Python runner도 같은 syntax 5 19를 보고한다. PHP, Rust와 PHP 확장 runner는 type 이름에 type 5 8을 보고하고 구문 오류는 보고하지 않아 잘못된 token이 보고되지 않는다. 완료 기준: tests/dbspec/cases.json의 공유 case가 Go의 출력을 고정하고, 여섯 구현이 모두 dbspec-compare-check를 통과한다.—Evidence: Go의 동작은 engine/dbspec/parse.go에서 확인했다. columnLine은 type 인자를 cursor.value로 읽는데, 이는 단어나 수(그리고 수 앞의 -)를 받고, 이어서 ,나 )를 요구한다. 그 밖의 token은 그 token에 syntax(expected a type parameter 또는 ',' or ')')다. 단어나 소수점 수는 여기서 받아들여지고, 줄이 구문에 맞을 때만 resolveType이 type 이름에 type 오류로 거른다. 공유 case column-type-argument-unclosed(varchar(64 x), column-type-argument-trailing-comma(varchar(64,) null), column-type-argument-decimal-space(decimal(10 2))를 Go의 출력(각각 syntax 5 19)으로 추가했고, go test ./engine/dbspec -run TestSharedVectors는 ok를 출력한다. Red: 수정 전 make dbspec-compare-check는 2로 끝났다(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). 줄마다 직접 비교하자 rust와 php-extension도 type 5 8을, typescript와 python은 syntax 5 19를 보고했다. Green: packages/orm-php/src/Dbspec/Parser.php의 type() 인자 loop, packages/orm-php-extension/src/parse.c의 read_type, packages/orm-rust/orm-schema/src/dbspec/parser.rs의 column_type이 Go처럼 인자를 읽는다. 수, 단어, - 수는 값이고(단어나 - 수는 type 오류를 남기며 나머지 줄은 Go처럼 읽는다), 값 자리의 다른 token은 그 token에 syntax이며, 값 뒤의 ,와 )가 아닌 token도 그 token에 syntax다. 첫 구문 오류 뒤의 남은 token은 Go처럼 보고하지 않는다. 단어는 lexer 자신의 단어 판정으로 가린다. PHP는 Parser::isWord(생성된 UnicodeWord 표), C는 str_dotted(word_rune_width, 생성된 unicode_word.h 표), Rust는 lexer가 붙인 token 종류(Kind::Word, is_word_rune)를 읽는다. ASCII만 보는 판정은 쓰지 않는다. 공유 case column-type-argument-unicode-word(varchar(é))와 column-type-argument-unicode-decimal(decimal(10,é))은 Go의 출력(type 이름의 type 5 8)을 고정하며, go test ./engine/dbspec -run TestSharedVectors가 이를 확인하고, make dbspec-compare-check에서 여섯 runner가 모두 일치한다. 수정의 첫 판은 ASCII만 보는 단어 판정을 썼으므로, 이 두 case는 그 변경 뒤에 추가되었고 그 판정에서 실패하는 것을 직접 보지 못했다. 옛 동작을 고정한 단위 test는 없었다. 수정 뒤 종료 코드: 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(snapshot이 바뀌지 않았다), make feature-unit-check 0, make repo-check 0.[o]
T62-3-1engine/dbspec/literal_test.go가 contracts/features.json의 feature에 속한다. 원인: T62-3이 CanonicalLiteral의 테스트를 feature에 넣지 않고 추가해, feature-unit 검사가 추적되는 file이 어떤 feature에도 속하지 않는다고 보고한다. 완료 기준: 그 file이 engine/dbspec/literal.go를 소유한 schema feature의 입력이고, feature-unit 검사가 더 이상 그것을 보고하지 않는다.—Evidence: Red: 수정 전 make feature-unit-check는 2로 끝났다. 실패는 feature-unit-check/owners의 every tracked file selects a behaviour test or declares its scope(scripts/features/owners.test.mjs) 하나이며, 이 검사는 engine/dbspec/literal_test.go를 보고했다(untestedPaths). 같은 실행은 T62-4-4의 sample/owner/* 경우(typescript/none, typescript/mysql, typescript/postgres, go/none)를 FAIL 줄로 출력했지만 그 묶음은 fail 0이었고, 이 작업에서 고치지 않았다. 문구 belongs to no feature는 scripts/features/check.mjs의 문구로, 이 실행은 그 단계에 이르지 않았다. Green: contracts/features.json이 이 file을 schema_definition의 tests와 schema-go(make dbspec-go-check)의 inputs에 형제 test file처럼 적었다. tests 항목만으로는 보고가 남았다. owner 선택 검사는 test file을 검증 명령의 입력으로만 인정하기 때문이다. make feature-unit-check는 0으로 끝나고 그 보고가 사라졌다. make interface-check, make dbspec-go-check, make version-check/run은 0으로 끝났다. make repo-check/run은 2로 끝났다. 도구 버전 검사가 Go 1.27.0 설치를 .go-version의 1.27.2와 다르다고 보고하며, HEAD의 features.json에서도 같은 결과라 이 변경의 원인이 아니다. make documents-check는 0으로 끝났다.[o]
T62-4-1PHP와 TypeScript 구현이 이름 글자를 runtime의 Unicode data가 아니라 생성된 Go table로 분류한다. 원인: T62-4는 두 구현이 Unicode 17.0.0에서 Go와 같음을 임시 검사로만 확인했다. PCRE와 JavaScript 엔진의 Unicode 버전은 runtime에 따라 바뀌고, 이를 고정하는 commit된 테스트가 없다. 완료 기준: 둘 다 T62-4 생성기가 만든 table을 읽고, 그 최신성 테스트와 모든 code point 테스트가 각자의 검사에서 실행된다.—Evidence: Red: 표가 생기기 전에 go test ./tests/dbspec/unicode -count=1은 TestCommittedTablesAreGenerated에서 1로 끝났고 (packages/orm-php/src/Dbspec/UnicodeWord.php: open ... no such file), php packages/orm-php/tests/dbspec_unicode_test.php는 표를 읽을 때 잡히지 않은 RuntimeException으로 255로 끝났으며, node --test packages/orm-npm/tests/dbspec-unicode.mjs는 Cannot find module .../dist/dbspec/unicode_word.js로 1로 끝났다. 변경: go run ./tests/dbspec/unicode -write가 packages/orm-php/src/Dbspec/UnicodeWord.php(UnicodeWord::contains, UnicodeWord::RANGES)와 packages/orm-npm/src/dbspec/unicode_word.ts(containsWordRune, WORD_RANGES)를 썼고, 생성기의 최신성 test가 둘 다 덮는다. PHP Parser::wordRune과 Parser::isWord(public이며, \p{L}\p{Nd} 정규식 대신 Expression::operand가 읽는다), parse.ts의 TypeScript isWordRune이 이 표로 분류한다. Green: go test ./tests/dbspec/unicode -count=1이 0으로 끝나고, php packages/orm-php/tests/dbspec_unicode_test.php가 0으로 끝나며 1114112 code points agree를 출력한다. make dbspec-php-check와 make dbspec-ts-check가 0으로 끝난다(TypeScript는 ℹ pass 378, ℹ fail 0, dbspec-unicode.mjs 포함). make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다. contracts/symbols/php.json과 contracts/symbols/typescript.json을 다시 기록하고(PHP Parser::isWord는 private에서 public으로 바뀌었고, 새 UnicodeWord class와 contains, 새 TypeScript export containsWordRune이 있다) contracts/interfaces.json의 hash를 갱신한 뒤 make interface-check가 0으로 끝난다. make feature-unit-check가 0으로 끝나고, make documents-check와 make docs-rules-check가 0으로 끝난다.[o]
T62-4-2dbspec이나 database CHECK 텍스트를 읽는 모든 word 판정이 Go와 일치한다. 원인: T62-4는 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, packages/orm-php/src/Dbspec/PlanObjects.php의 CHECK_TOKEN pattern에 ASCII 전용 판정을 남겼고, 대응하는 Go 코드와 비교하지 않았다. 완료 기준: 각 판정을 대응하는 Go 코드와 비교해, 생성된 table을 읽거나, Go도 ASCII로 제한하는 글자만 읽는다는 것을 공유 case로 보인다.—Evidence: Go 대응 코드는 engine/dbspec/introspect_check.go:90-92(checkTokens, ASCII 글자, 숫자와 _만 읽고 나머지 바이트는 unexpected character), engine/dbspec/lex.go:104-108(isWordRune, Unicode로 읽지만 문자열 밖의 non-ASCII word는 ASCII 이름 규칙 validNameFormat(lex.go:252)에서 name.format으로 거부된다), engine/dbspec/parse.go:316(isHeaderNameRune, ASCII)다. 판정: CheckDecoder.php의 ctype_alpha/ctype_alnum은 여기 PHP 8.5 build에서 0x80 이상 바이트를 word 바이트로 읽었다(LC_ALL=C에서도 65개 바이트). 그래서 판정을 명시적 ASCII 도우미 isAsciiLetter로 바꿨고, Go와 같은 결정을 따른다. 바꾸기 전 ê > 0의 사유는 name.format, 바꾼 뒤는 Go와 같은 unexpected character다. introspect.c의 calpha/word_char, check.rs의 is_word_start, TypeScript의 isLetter(introspect_check.ts:53), PlanObjects.php의 CHECK_TOKEN과 isColumn, Renderer.php의 token 패턴과 operand 정규식, PHP의 header와 이름 정규식은 ASCII이며 바뀌지 않았다. 공유 case는 tests/dbspec/cases.json의 check-non-ascii-literal과 non-ascii-digit-after-document-name, tests/dbspec/introspect.json의 sqlite-check-non-ascii-word와 sqlite-check-non-ascii-literal이다. non-ASCII check literal의 plan case는 물리 MySQL 실행이 그 literal을 mojibake로 되돌려 제거했다(T62-4-6 참고). 명령과 종료 코드: make dbspec-compare-check 0(12 runs agree); make dbspec-php-check 0; make dbspec-php-extension-check 0(tests/dbspec/unicode/main.go의 UnicodeWord @internal docblock 수정 뒤); 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, make dbspec-introspect-rust-check 0(.runtime/servers/env의 로컬 MySQL과 PostgreSQL 서버 사용); 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 0; make dbspec-plan-check, make dbspec-plan-php-check, make dbspec-plan-ts-check, make dbspec-plan-rust-check 0; make interface-check 0(contracts/symbols/php.json을 다시 기록했다. 줄 하나, private CheckDecoder::isAsciiLetter이며 contracts/interfaces.json의 hash를 갱신했다); make feature-unit-check 0(FAIL 줄은 T62-4-4의 확인된 음성 sample이다); go test ./engine/dbspec와 go test ./tests/dbspec/unicode 통과; make documents-check와 make docs-rules-check 0.[o]
T62-4-3cargo clippy -p polyspec-orm-schema --all-targets -- -D warnings가 통과한다. 원인: packages/orm-rust/orm-schema/src/dbspec/parser.rs의 사용되지 않는 first와 lexer.rs is_number의 map_or를 보고한다. 완료 기준: make rust-check/clippy가 0으로 끝난다.—Evidence: Red: 수정 전 make rust-check/clippy는 library target polyspec-orm-schema (lib)에서 2로 끝났고, lint 세 개에서 멈췄다: 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)), clippy::too_many_arguments(orm-schema/src/dbspec/validate.rs:88, 인자 여덟 개로 일곱 개를 넘는다. 원인에는 두 lint만 적혀 있었고 셋째는 같은 실행이 보고했다). 원인: first 바인딩은 T62-1(72e2b5353)이 남긴 것이다. T62-1은 invalid 문자 분기의 check_line 가드를 없앴고, 그래서 그 분기가 check 줄도 받는다. 읽는 곳이 없으므로 바인딩은 쓰이지 않는 값이며 놓친 검사가 아니다. Go의 check 분기는 lex 오류 뒤에 반환하고(engine/dbspec/parse.go의 check case), Rust 분기는 같은 첫 잘못된 문자를 보고하며 check 줄의 이름에 영향을 주지 않는다. 그런 문자가 든 check 줄을 고정하는 공유 case가 없어, 이 판단은 코드 읽기에 근거한다. map_or는 clippy의 is_none_or이고, validate의 인자 여덟 개는 Parsed의 parse 필드 네 개와 출력 세 개다. 변경: first를 제거하고, fraction.is_none_or(digits)로 바꿨으며, validate(parsed: &Parsed, used, diags, literals)는 document, unresolved, failed_keys, header_failed, defaults를 parsed에서 읽고, dbspec/mod.rs는 parsed를 넘긴다. Green: make rust-check/clippy가 0으로 끝나고 PASS rust-check/clippy를 출력한다. make rust-fmt-check/clients가 0으로 끝난다. make dbspec-rust-check가 0으로 끝난다. make dbspec-compare-check가 0으로 끝나고 12 runs agree를 출력한다. validate의 선언이 바뀌어 make interface-check는 interfaces/rust에서 2로 끝났다. Rust symbol snapshot을 다시 기록했고(contracts/symbols/rust.json, validate 줄만 바뀌었다. Go, PHP, TypeScript, Python과 PHP 확장의 snapshot은 그대로다), contracts/interfaces.json의 hash를 고친 뒤 make interface-check가 0으로 끝난다(rust: 2830 native symbols inspected).[o]
T62-4-4feature-unit 검사가 sample/owner fixture에서 통과한다. 원인: make feature-unit-check가 sample/owner/*(typescript/none, typescript/mysql, typescript/postgres, go/none)에 대해 출력하는 FAIL 줄은 실패가 아니다. scripts/features/coverage.mjs의 executeCoverage는 실행이 끝날 때마다 그 실행의 FAIL 줄을 출력하고, scripts/features/coverage.test.mjs의 부정 case가 이 fixture를 일부러 실행해 돌려받은 오류를 검사한다. 중복되거나 예상과 다른 출력을 내는 owner, sqlite만 가리키는 DSN 때문에 mysql과 postgres 상태 reader가 실패하는 경우, 건너뛰는 Go test가 그렇다. 통과한 실행을 읽는 사람은 FAIL 줄을 보지만, 검사는 실패를 보고하지 않는다. 완료 기준: T62-4 이전 commit에서 실패를 재현하고, 원인을 밝혀 없애며, feature-unit 검사가 0으로 끝난다.—Evidence: T62-4 이전 commit인 72e2b535에서 make feature-unit-check가 같은 FAIL 줄을 출력하므로 이 줄들은 T62-4 이전부터 있었다. 검사하는 test는 native owner and dependent files execute twice from their own parts(scripts/features/coverage.test.mjs 166, 168, 170행: unexpected test output, duplicate observed case first, no executable command이며, 170행의 호출은 owner를 다시 실행해 중복 줄을 출력한다), checker reads physical database state around each native test run(312~313행과 317행), Go, PHP, and Rust native cases execute through their owning files(381행, observed cases .* differ from TestFirst)다. HEAD에서 make feature-unit-check는 0으로 끝나고, node --test scripts/features/coverage.test.mjs는 ℹ pass 17과 ℹ fail 0을 출력한다. 이 항목은 아무것도 고치지 않았다. 예상과 다르게 실패한 sample이 없기 때문이다. 72e2b535의 실행은 T62-3-1의 test file 때문에 feature-unit-check/owners에서 2로 끝났으며, 이는 HEAD에서 고쳐졌다.[o]
T63Go client에 markdown IR executor를 추가한다. 원인: flowmark 계획(Layering과 T12.9 server 단계, orm protocol 항목이 필요하다)은 Go client를 markdown://으로 corpus에 닿는 첫 orm client로 정하며, 다른 모든 client는 이 executor를 따른다. 완료 기준: DSN markdown://127.0.0.1:<port>/<corpus>?token=<token>이 flowmark server에 연결되어 모든 IR kind, transaction, savepoint를 그 server를 통해 실행한다. corpus 또는 token이 맞지 않으면 server의 CONFIG 오류로 실패한다. WRITE_CONFLICT로 실패한 쓰기는 호출자에게 돌아가며 재시도되지 않는다. Go client의 conformance는 tests/markdown/flowmark.version에 고정된 flowmark build에 대해 모든 IR kind를 정확한 case ID로 실행하고 모든 case가 통과한다. 고정된 build가 없으면 check가 실패하며 건너뛰지 않는다. T61, T62 및 flowmark server(flowmark checklist T12.9)가 필요하다. 상태: branch feat/markdown-go-T63(6b6079bb)에서 끝났고, local flowmark build에 대해 tests/markdown의 33개 case가 모두 green이다. CI 단계가 flowmark commit을 설치하는데, 그 commit은 flowmark의 T12 항목이 닫힐 때까지 GitHub에 없으므로 T114 뒤에 merge한다. (func(), error)를 돌려주는 Go Subscribe는 T64–T67이 각 client에서 따르고, export된 ir.ParamUses는 T89가 다른 구현에 갖춘다. 증거(local, macOS): 고정한 revision fd4eee986f7c의 checkout에서 go build ./cmd/flowmark로 만든 flowmark(module 버전이 tests/markdown/flowmark.version과 같다)에 대해 make markdown-conformance-check/go가 exit 0이고 tests/markdown/cases.json의 모든 case가 통과한다.——[o]
T64PHP client의 markdown IR executor. 원인: flowmark 계획(Layering과 T12.9 단계)은 각 orm client를 Go client가 먼저 구현한 IR executor를 거쳐 flowmark에 닿게 하므로, PHP client도 같은 protocol의 자체 executor가 필요하다. 완료 기준: PHP client가 DSN markdown://127.0.0.1:<port>/<corpus>?token=<token>으로 flowmark server에 연결하고, 모든 IR kind, transaction, savepoint를 그 server를 통해 실행한다. conformance는 Go client와 같은 case ID를 tests/markdown/flowmark.version에 고정된 flowmark build에 대해 실행하며 모든 case가 통과한다. 고정된 build가 없으면 check가 실패한다. T63이 필요하다. 증거(local, macOS): 고정한 revision fd4eee986f7c의 checkout에서 go build ./cmd/flowmark로 만든 flowmark(module 버전이 tests/markdown/flowmark.version과 같다)에 대해 make markdown-conformance-check/php가 exit 0이고 tests/markdown/cases.json의 모든 case가 통과한다.——[o]
T65Python client의 markdown IR executor. 원인: flowmark 계획(Layering과 T12.9 단계)은 각 orm client를 Go client가 먼저 구현한 IR executor를 거쳐 flowmark에 닿게 하므로, Python client도 같은 protocol의 자체 executor가 필요하다. 완료 기준: Python client가 DSN markdown://127.0.0.1:<port>/<corpus>?token=<token>으로 flowmark server에 연결하고, 모든 IR kind, transaction, savepoint를 그 server를 통해 실행한다. conformance는 Go client와 같은 case ID를 tests/markdown/flowmark.version에 고정된 flowmark build에 대해 실행하며 모든 case가 통과한다. 고정된 build가 없으면 check가 실패한다. T63이 필요하다.—증거: executor commit이 rebase에서 main 밖으로 빠졌으므로 tests/test_markdown.py와 tests/markdown_conformance.py는 main에 있는데 polyspec/orm/markdown.py와 engine/params.py는 추적되지 않았고, unit discovery가 ModuleNotFoundError: No module named 'polyspec.orm.markdown'으로 실패했다. executor를 지금의 client(등록한 set마다의 Engine, markdown dispatch 전의 request hook) 위에 되돌렸다. FLOWMARK=.runtime/bin/flowmark python packages/orm-python/tests/markdown_conformance.py가 고정한 build(tests/markdown/flowmark.version, module cache에서 build)에 대해 case 34개를 통과하고, unittest discover -p 'test_*.py'가 test_markdown을 포함한 test 138개를 실행한다.[o]
T66Rust client의 markdown IR executor. 원인: flowmark 계획(Layering과 T12.9 단계)은 각 orm client를 Go client가 먼저 구현한 IR executor를 거쳐 flowmark에 닿게 하므로, Rust client도 같은 protocol의 자체 executor가 필요하다. 완료 기준: Rust client가 DSN markdown://127.0.0.1:<port>/<corpus>?token=<token>으로 flowmark server에 연결하고, 모든 IR kind, transaction, savepoint를 그 server를 통해 실행한다. conformance는 Go client와 같은 case ID를 tests/markdown/flowmark.version에 고정된 flowmark build에 대해 실행하며 모든 case가 통과한다. 고정된 build가 없으면 check가 실패한다. T63이 필요하다. 증거(local, macOS): 고정한 revision fd4eee986f7c의 checkout에서 go build ./cmd/flowmark로 만든 flowmark(module 버전이 tests/markdown/flowmark.version과 같다)에 대해 make markdown-conformance-check/rust가 exit 0이고 tests/markdown/cases.json의 모든 case가 통과한다.——[o]
T67TypeScript client의 markdown IR executor. 원인: flowmark 계획(Layering과 T12.9 단계)은 각 orm client를 Go client가 먼저 구현한 IR executor를 거쳐 flowmark에 닿게 하므로, TypeScript client도 같은 protocol의 자체 executor가 필요하다. 완료 기준: TypeScript client가 DSN markdown://127.0.0.1:<port>/<corpus>?token=<token>으로 flowmark server에 연결하고, 모든 IR kind, transaction, savepoint를 그 server를 통해 실행한다. conformance는 Go client와 같은 case ID를 tests/markdown/flowmark.version에 고정된 flowmark build에 대해 실행하며 모든 case가 통과한다. 고정된 build가 없으면 check가 실패한다. T63이 필요하다. 증거(local, macOS): 고정한 revision fd4eee986f7c의 checkout에서 go build ./cmd/flowmark로 만든 flowmark(module 버전이 tests/markdown/flowmark.version과 같다)에 대해 make markdown-conformance-check/typescript가 exit 0이고 tests/markdown/cases.json의 모든 case가 통과한다.——[o]
T68한국어 checklist에서 writing-style 금지 표현을 없앤다. 원인: writing-style 규칙(contracts/rules.json의 docs.writing-style)이 한국어 checklist의 T58 줄에 있는 정한다를 금지하므로 make docs-rules-check가 main에서 실패한다. 완료 기준: make docs-rules-check가 0으로 끝나고, 한국어 checklist가 영문 checklist와 같은 사실을 적는다(T58: docs/dsl.md와 docs/dsl.ko.md가 Python client를 명시한다).—Evidence: make docs-rules-check가 PASS docs-rules를 출력하고 make documents-check가 통과하며, 두 명령 모두 이 tree에서 0으로 끝난다.[o]
T70repository 검사는 실행 중인 Go나 Composer의 major와 minor가 선언된 release와 같으면 patch와 상관없이 받아들인다. 원인: scripts/repo/toolchains.mjs가 .go-version과 .composer-version이 선언한 정확한 release를 요구해, 선언이 1.27.2일 때 로컬 Go 1.27.0이 make repo-check에서 실패하고 모든 owner 검사를 막는다. patch release는 언어도 module 동작도 바꾸지 않으며, PHP 검사는 이미 major와 minor만 비교한다. 완료 기준: goVersionErrors와 composerVersionErrors가 실행 중인 major와 minor를 선언된 release와 비교하고, 다른 patch는 받아들이는 사례와 다른 minor는 거절하는 사례가 있으며, 선언과 CI workflow는 여전히 정확한 release 하나를 고정하고, Go 1.27.0에서 make repo-check가 통과한다.—Evidence: 등록은 d5f6f788이고, 수정은 f28b4908이다. f28b4908은 scripts/repo/toolchains.mjs의 goVersionErrors와 composerVersionErrors가 실행 중인 major와 minor를 선언된 release와 비교하게 바꾸고, scripts/repo/check.test.mjs에 "a running Go with the declared major and minor passes whatever its patch"와 "a running Composer with the declared major and minor passes whatever its patch" case, minor가 다른 Go 1.28.0과 Composer 2.11.0을 거절하는 case를 더했다. 수정 전의 Red 실행은 이 기록에서 다시 하지 않았다. Green: make repo-check/unit가 0으로 끝나고 tests 90, pass 90, fail 0을 출력한다. make repo-check가 0으로 끝나며, go1.27.0은 .go-version의 1.27.2와 patch만 다르고 Composer 2.10.3은 .composer-version과 같다. PASS repo를 출력한다. make documents-check와 make docs-rules-check가 0으로 끝난다.[o]
T73ir.Validate가 저장된 filter의 모든 규칙을 지킨다. 원인: flowmark의 view 검증(그 T12.3-1)에서 ir.Validate가 column 대 column 술어의 ref 경로와 column을 해석하지 않으며(planner의 renderPred만 한다), 최상위 group과 group의 첫 항목의 conn을 받아들이고, parameter가 묶는 type을 알 길이 없다는 것이 드러났다. 그 type은 planner의 functionType과 valueFunctionArgType이 계산한다. 완료 기준: ir.Validate가 Ref.Path(root, join된 relation, subquery 안에서만 ^)를 해석하고 대상 column을 요구하며 아니면 COLUMN_UNKNOWN이다. 최상위 group과 group 첫 항목의 conn은 IR_INVALID로 거절한다. 같은 규칙을 모든 구현의 validator가 지키며, 공유 vector로 고정한다. red 다음 green test. export된 parameter 사용 목록은 이 항목에 들지 않는다. 그것은 모든 client가 지키는 CODEC_ENCODE 표준과 함께, 별도 항목으로만 orm에 들어온다.——[o]
T74codec decode는 모든 구현에서 엄격하고 공개 표면이 하나다. 원인: Go의 host stage hex와 ip는 export되지 않은 hostDecode에 있지만 다른 client는 그에 해당하는 것을 공개한다. 그리고 client마다 공백 처리가 다르다. Go는 hex와 base64 주위를 잘라 내고(strings.TrimSpace), 다른 구현도 저마다 다르다. 저장된 값은 encoder의 정확한 출력이므로, 공백을 잘라 내면 어떤 encoder도 쓰지 않은 값을 받아들이게 된다. 완료 기준: 공유 vector가 먼저 여섯 구현 모두에서 그 불일치를 red로 재현한다. 그다음 모든 구현이 hex와 base64 text 주위와 안의 공백을 거절하고, Go는 다른 구현에 대응하는 HostDecode를 export한다. 문서(EN과 ko)가 그 규칙을 적는다. 한 소비자를 위한, 모든 stage의 key 없는 decode는 orm에 들지 않는다.——[o]
T75make dbspec-apply-stress-mysql가 macOS에서 실행된다. 원인: 그 명령은 make feature-owner-check만 설정하는 TEST_ENV="$ORM_OWNER_TEST_ENV"를 넘기므로, macOS의 sh가 file 이름 없는 .을 거절한다(exit 2). Linux CI는 7ea3932b에서 통과했다(T62-4-10에서 발견). 완료 기준: helper가 모든 플랫폼에서 선언된 출처에서 환경을 읽고, macOS와 Linux에서 실행된다.——[o]
T76AGENTS가 설계 원칙을 적는다. 원인: 모든 저장소에 대한 사용자 규칙(하위 호환 없음, 현재 요구사항을 완전히 충족하는 가장 단순한 구현, 관심사를 분리한 module 구성, 장기적 architecture와 임시방편 금지)이 flowmark의 AGENTS에는 있었지만 이 저장소의 AGENTS에는 없었다. 완료 기준: AGENTS.md와 AGENTS.ko.md가 이를 적고, make documents-check가 통과한다.——[o]
T77-13생성 파일은 추적하지 않는다. 원인: 저장소에 생성 파일 규칙이 없었고 .gitignore에 vendor/, venv/, 일반 *.egg-info/ 패턴이 없어서 의존성이나 빌드 산출물이 눈에 띄지 않고 커밋될 수 있었다(node_modules history purge가 이어졌다). 완료 기준: AGENTS.md(와 ko)가 규칙과 명명된 예외를 적고, .gitignore가 규칙이 말하는 모든 패턴을 덮으며, make staged-check가 명명된 예외가 아닌 덮인 staged 경로를 거절하고, .githooks/pre-commit이 커밋마다 이를 실행한다.—Evidence: node --test scripts/check/staged-generated.test.mjs가 23개 사례를 통과한다(덮인 패턴마다 거절, 예외와 일반 경로는 허용, 섞인 목록은 위반만 보고). make staged-check는 staged 생성 파일이 없다고 보고하고, config/checklist.json이 pre-commit 훅을 나열한다.[o]
T77-14interface와 conformance check의 Python test가 통과하고 기능에 속한다. 원인: test_python_extractor.py와 test_python_natives.py는 심볼 스냅샷을 문자열로 읽는데, T77-8-1 이후 스냅샷과 추출기는 심볼마다 {exported, signature}를 담으므로 8개 test(extractor 2, natives 6)가 실패했다. conformance_result.py는 testcase.main() 대신 unittest.main()으로 끝나서 harness 검사가 거절했고, 세 file은 어느 기능에도 속하지 않았다. 완료 기준: test가 심볼마다 signature를 읽고, 모듈이 testcase.main()으로 끝나며, 세 file이 기능의 test로 등재되고, make python-unit-check가 통과한다.—Evidence: make python-unit-check가 test 1352개를 실행해 통과한다. node scripts/features/check.mts는 기능에 속하지 않는 python test file을 보고하지 않는다.[o]
T77-15Go gate unit을 뺀 뒤 performance gate가 정적 검사를 통과한다. 원인: Go unit(T77-8-1-1)을 뺄 때 performance_gate 기능에 go-internal parity id가 남고 Go owner도 남아, main에서 node scripts/features/check.mts가 finding 두 건으로 실패했다. 완료 기준: 기능은 존재하는 parity unit만 명명하고, owner는 unit을 가진 구현만이며, 정적 검사가 통과한다.—Evidence: node scripts/features/check.mts가 feature contract 19개 통과를 보고하고, make feature-docs는 생성 문서를 바꾸지 않는다.[o]
T77-16생성 파일 검사는 kit의 pre-commit hook이다. 원인: 저장소가 kit의 K210 hook 옆에 자체 .githooks/pre-commit과 staged 생성 파일 검사를 따로 두어, 규칙의 소유자가 둘이었다. 완료 기준: config/generated.json이 저장소의 경로와 예외를 선언하고, 로컬 hook과 그 검사와 test를 지우며, make hooks가 kit의 .githooks/pre-commit을 설치하고, staged node_modules 경로는 거절되고 fixture 경로는 통과한다.—Evidence: kit의 pre-commit hook은 staged node_modules/x/index.js에서 종료 코드 2, staged tests/probe/fixtures/a.json에서 종료 코드 0으로 끝난다. 둘 다 commit 없이 직접 실행했다. make hooks-check가 통과한다.[o]
T77-17conformance result check를 module mode로 실행한다. 원인: Makefile target이 파일을 직접 실행해 packages가 sys.path에 없었고, main에서 ModuleNotFoundError로 실패했다. 완료 기준: venv로 module mode 실행, PYTHONPATH 없음, 검사 exit 0.—증거: main에서 packages/python/.venv/bin/python -m packages.python.tests.conformance_result가 OK로 exit 0.[o]
T77-18저장소 convention test가 main에서 통과한다. 원인: tests/testcase.mts와 scripts/repo/testcases.mts의 위반 네 가지. 네 개 언어 밖의 Python thin-slice tool(Python 클라이언트 예제로 packages/python/examples/thin_slice로 옮김), 공유 testcase package로 case를 시작하지 않는 Rust test(request.rs, collection_identity.rs), node:test로 case를 선언하는 JavaScript test(packages/ts/tests/plan-cache-in.mjs, feature 명령이 실행). 완료 기준: make repo-check/unit이 exit 0.—증거: 네 수정 뒤 이 worktree에서 make repo-check/unit이 exit 0.[o]
T77-19interface self-test가 실행 전에 빠진 TypeScript checker 의존성을 명칭으로 알린다. 원인: self-test가 의존성 없이 TypeScript 선언 추출기를 실행해서, install-interfaces-typescript를 돌리지 않은 checkout이 parity-check에서 ERR_MODULE_NOT_FOUND로 실패했다. 완료 기준: 의존성이 없으면 interface-check가 설치 단계를 명명하며 실패하고, 있으면 self-test가 통과한다.—증거: 의존성이 있을 때 interface-typescript-check가 exit 0, 옮겨 두었을 때 exit 1이며, 이후 parity-check가 TypeScript interface 단계를 실행한다.[o]
T77-20DSN 없이 parity-check를 실행하면 빠진 DSN의 이름을 알린다. 원인: BENCH_*_DSN 없이 parity-check가 돌아서, conformance와 coverage unit이 모두 같은 "BENCH_SQLITE_DSN is required" 메시지로 실패했다(한 실행에서 111개 unit이 0이 아닌 값으로 종료). bench DSN은 bench database를 만드는 run-databases가 설정한다. 완료 기준: DSN 하나가 없으면 parity-check가 그 변수를 명명하는 guard에서 멈추고, 셋 다 있으면 guard가 통과한다. 빨강: BENCH_SQLITE_DSN을 비우자 Go conformance unit이 그 메시지로 실패. 초록: 하나가 없으면 parity-bench-env-check가 변수를 명명하며 exit 2, 셋 다 있으면 exit 0.——[o]
T77-21TypeScript engine unit의 기대 entity 순서가 bench.dbs의 선언과 맞는다. 원인: 기대 목록에 day_cell이 없었다. bench.dbs는 day_cell을 clock_cell과 upsert_cell 사이에 선언하므로, 나머지 117개 case가 통과해도 "entities in document order" FAIL이 나오고 exit 1이 되었다. 완료 기준: unit이 모든 case 통과로 exit 0. 빨강: 단독 실행의 그 FAIL 줄과 exit 1. 초록: node --conditions=orm-test --test packages/ts/tests/engine.mjs가 117 pass, 0 fail로 exit 0.——[o]
T77-22parity unit이 실행의 bench DSN을 받는다. 원인: run-databases는 bench DSN을 TEST_ENV 파일에 쓰지만 make가 이를 export하지 않아, kit이 make에서 띄우는 unit이 BENCH_*_DSN 없이 실행되어 bench unit이 같은 메시지로 실패했다(파일을 읽히면 Go SQLite unit은 통과). 완료 기준: make가 parse 시점에 TEST_ENV의 bench DSN을 읽어 export하고, 자식 레시피가 BENCH_SQLITE_DSN을 보며, guard는 빠진 DSN을 여전히 명명한다. 빨강: parity 실행에서 Go SQLite unit이 BENCH_SQLITE_DSN is required로 실패. 초록: 실행 TEST_ENV에서 자식이 DSN을 보고 Go unit 통과, 파일이 없으면 자식은 값이 없고 guard는 exit 2.——[o]
T77-23dbspec harness test가 harness가 출력하는 case 이름을 기대한다. 원인: T77-9-4-2가 case 라벨을 평탄하게 바꿔, 실패 픽스처는 shop, export-shop 등 평탄한 id를 출력하는데 TestCaseHarnessReportsOnlyFailure는 여전히 mermaid/export/shop 등을 찾았다. 그래서 dbspec 패키지가 exit 1로 끝났고(1 of 1 ids passed), parity reporter는 실패한 테스트를 명명하지 않았다. 완료 기준: 기대 이름이 평탄한 id이고, dbspec 패키지가 exit 0. 빨강: TestCaseHarnessReportsOnlyFailure가 mermaid/export/shop 등에 대한 FAIL 줄 없음으로 실패. 초록: 단독 통과, packages/go/engine/dbspec의 go test가 exit 0.——[o]
T77-25markdown conformance unit이 읽는 flowmark binary를 찾는다. 원인: FLOWMARK가 .runtime/bin/flowmark를 가리켰지만 parity 선행 조건이 이를 빌드하지 않아, Go 등 markdown unit이 FLOWMARK is not set 또는 Go build info 없음으로 실패했다(언어별 0 of 35). 완료 기준: parity-check가 unit 실행 전에 빠진 binary와 설치 단계를 명명하고, binary가 설치되면 Go markdown conformance unit이 exit 0이다. 빨강: FLOWMARK 없이 Go markdown unit이 FLOWMARK is not set으로 실패. 초록: binary가 없으면 flowmark-check가 install-flowmark를 명명하며 exit 2, make install-flowmark 후 exit 0이고, FLOWMARK를 설정하면 TestMarkdownConformance가 exit 0.——[o]
T77-24physical query-form test가 환경이 설정하는 DSN 이름을 읽는다. 원인: TestPhysicalQueryForms가 아무 make target이나 script도 설정하지 않는 ORM_MIGRATION_MYSQL_DSN과 ORM_MIGRATION_POSTGRES_DSN을 읽어 ORM_MIGRATION_MYSQL_DSN is required로 실패했다. 완료 기준: 테스트가 같은 패키지의 다른 physical 테스트가 읽는 ORM_TEST_MYSQL_DSN과 ORM_TEST_POSTGRES_DSN을 읽는다. 빨강: ORM_MIGRATION_MYSQL_DSN is required로 실패. 초록: 실패가 다음 원인(MySQL DSN 형식과 place.location 픽스처, 행 T77-26과 T77-27)으로 옮겨 간다.——[o]
T77-28models script scan test가 models를 호출하는 source의 모든 디렉터리를 다룬다. 원인: models script는 models를 호출하는 packages/ts/testing/conformance/runner_typescript.mts를 스캔하는데, 테스트는 tests/, tests/conformance, examples/thin-slice/ts만 나열해 호출 집합에서 runner가 빠졌다(parity 리포터는 이를 2 of 1로 집계). 완료 기준: 테스트가 클라이언트 testing 디렉터리를 나열하고 통과한다. 빨강: packages/ts/testing/conformance/runner_typescript.mts를 명명한 deep-equal 실패. 초록: packages/ts/tests/models-scan.mjs가 3 pass, 0 fail로 exit 0.——[o]
T77-29examples test가 옮겨진 위치의 Python thin-slice를 찾는다. 원인: T77-18이 examples/thin-slice/python/main.py를 packages/python/examples/thin_slice/main.py로 옮겼지만, examples의 DSN과 출력 test가 옛 경로를 계속 실행해 can not open file로 실패했다. 완료 기준: 두 test가 옮긴 경로를 쓰고 examples 패키지가 exit 0. 빨강: examples 패키지가 can not open file examples/thin-slice/python/main.py로 실패. 초록: ./examples의 go test가 exit 0.——[o]
T77-26physical query-form test가 MySQL을 DSN의 드라이버 형식으로 연다. 원인: TestPhysicalQueryForms가 환경의 mysql:// URL을 sql.Open에 그대로 넘겨, Go 드라이버가 default addr for network 127.0.0.1:53294 unknown 오류를 보고한다. 변환은 internal/dialects/servers.go(mysqlConfig)에 한 곳 있다. 완료 기준: 테스트가 그 한 곳의 변환으로 MySQL DSN을 읽고, test마다 따로 변환하지 않는다. 빨강: default addr 줄. 초록: MySQL 하위 test가 열려 case를 실행한다.—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-27physical query-form 함수가 PostgreSQL과 SQLite에서 실행된다. 원인: TestPhysicalQueryForms가 postgres와 sqlite에서 COLUMN_UNKNOWN place.location으로 실패한다. test가 compile하는 place table에 location 컬럼이 없는데 query는 그것을 명칭으로 쓴다. 완료 기준: test가 쓰는 픽스처가 place.location을 선언하거나, query가 선언된 컬럼만 쓰도록 이유를 적어 고치고, 하위 test가 compile되어 실행된다. 빨강: COLUMN_UNKNOWN place.location. 초록: postgres와 sqlite 하위 test가 compile되어 통과한다.—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-30TypeScript generate 단위는 setDay를 생성된 signature로 선언한다. 원인: 단위가 FAIL: declaration setDay(value: string | Date | null): this로 실패하여, 생성된 선언과 단위가 기대하는 선언이 어긋난다. 완료 기준: 생성된 선언이 기대와 맞고 단위가 exit 0. 빨강: 단독 실행의 그 FAIL 줄과 exit 1.—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-31TypeScript SQLite concurrency 단위는 database 파일을 연다. 원인: 단위가 Error: unable to open database file(ERR_SQLITE_ERROR)로 실패하여, 여는 파일 경로가 없거나 실행에서 쓸 수 없다. 완료 기준: 단위가 SQLite 파일을 열고 exit 0. 빨강: 그 error 줄, 단독 실행 exit 1.—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-32TypeScript coverage case 단위는 node:test run을 재귀 호출하지 않는다. 원인: 단위가 test 파일 안에서 node:test run()을 호출하여 Node가 재귀 경고와 함께 건너뛰고, 리포터는 1 of 1 passed로 집계하지만 프로세스는 exit 1로 끝난다. 완료 기준: 단위가 run을 중첩하지 않고, case가 통과하여 exit 0. 빨강: node:test run() is being called recursively 경고와 exit 1. 초록: case 통과와 exit 0.—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-33physical fixture가 고정 timestamp를 쓰고, day_of_week와 days_ago 기대값이 그 날짜에서 계산된다.—증거: 묶음 실행 scratchpad/g3-parity.log: 바인딩 46개 두 번 실행, 케이스 47개 통과, 모든 unit이 모든 구현에서 성립, 각 바인딩 exit 0(T77-35: go-orm unit 45개, T77-33: go-internal-packages-go-engine-functions-physical). parity-check 단계는 남은 실행 디렉터리를 보고한다(kit 결함, T77-58로 추적).[o]
T77-33-1MySQL days_ago bind도 실행 시계를 쓴다.—증거: 묶음 실행 scratchpad/g3-parity.log: 바인딩 46개 두 번 실행, 케이스 47개 통과, 모든 unit이 모든 구현에서 성립, 각 바인딩 exit 0(T77-35: go-orm unit 45개, T77-33: go-internal-packages-go-engine-functions-physical). parity-check 단계는 남은 실행 디렉터리를 보고한다(kit 결함, T77-58로 추적).[o]
T77-34TestConnectionsUseUTC가 SQLite의 datetime column에 time.Time을 저장한다. 원인: sqlite 하위 test가 timezone_test.go:87에서 CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value로 실패한다(고정 2026-01-02 start_dt 생성 시). 그래서 parity 실행의 orm 패키지 unit이 다른 test 한 개 실패로 exit 1이 된다. 실행 시각 픽스처인 T77-33과 다르다. 값은 고정 날짜다. 완료 기준: sqlite encode가 명세대로 datetime column의 time.Time을 받고, test가 sqlite, mysql, postgres에서 exit 0. 빨강: CODEC_ENCODE 줄. 초록: TestConnectionsUseUTC 통과.—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각 go-orm unit은 자기 test만 실행한다.—증거: 묶음 실행 scratchpad/g3-parity.log: 바인딩 46개 두 번 실행, 케이스 47개 통과, 모든 unit이 모든 구현에서 성립, 각 바인딩 exit 0(T77-35: go-orm unit 45개, T77-33: go-internal-packages-go-engine-functions-physical). parity-check 단계는 남은 실행 디렉터리를 보고한다(kit 결함, T77-58로 추적).[o]
T77-36vendor된 kit 복사본이 @polyspec/kit 0.0.53과 일치한다. 원인: scripts/kit 복사본이 K208 이전의 trust wrapper와 install 설명을 담고 있어 make kit-check가 설치된 kit과 어긋났다. 완료 기준: make kit-sync가 unchanged이고 make kit-check가 0.0.53에서 exit 0.—증거: make kit-sync가 @polyspec/kit 0.0.53에 대해 unchanged를 출력하고, 복사본을 쓴 뒤 make kit-check가 exit 0.[o]
T77-37PHP 확장 테스트 helper requiredDsn은 한 번만 선언한다. 원인: packages/php-ext/tests/bootstrap.php와 packages/php/tests/coverage_cases.php가 requiredDsn을 둘 다 선언해, 두 bootstrap을 함께 로드하면 redeclare fatal이 나고 schema-definition php-extension이 exit 255로 0/7 결과로 끝난다. 인수 조건: 두 bootstrap이 로드하는 공유 helper 하나에만 선언하고, schema-definition php-extension unit은 7/7 통과한다.—근거: main에서 RED: php -d extension=orm_dbspec.so phpunit CoverageSchemaDefinitionTest.php가 Cannot redeclare function requiredDsn (packages/php/tests/coverage_cases.php:57)로 실패; 변경 후 GREEN: 같은 명령이 OK (7 tests, 27 assertions), exit 0; 변경한 세 파일은 php -l 통과.[o]
T77-38Rust savepoint 테스트가 parity 실행에서 SQLite DSN을 받는다. 원인: tx_send_tests.rs:123 sqlite_send_savepoint는 ORM_SEND_SQLITE_DSN을 읽고 없으면 패닉하는데, 이 값은 rust-send-savepoint-check 대상만 내보내므로, 전체 crate lib를 도는 parity의 Rust unit마다 exit 101과 함께 외부 실패로 같은 테스트가 찍힌다. 인수 조건: parity-check가 savepoint 대상과 같은 SQLite DSN을 내보내고, 그 값이 설정되면 sqlite savepoint 테스트가 통과한다.—근거: RED(변수 없음): cargo test --lib -- --exact tx::send_tests::sqlite_send_savepoint가 declared test DSN is required로 exit 101 (orm/src/tx_send_tests.rs:9); GREEN(parity와 같은 형식의 DSN): 같은 테스트가 ok이고 lib 실행의 sqlite savepoint 케이스가 통과한다(mysql과 postgres 케이스는 parity가 주는 서버 DSN이 필요하다).[o]
T77-39Go DSN 테스트가 T104가 설정하는 MySQL session sql_mode를 기대하지 않는다. 원인: T104(1b575695)가 모든 클라이언트에 session sql_mode를 설정하고 dsn.go에서 MySQL DSN에 넣었으며 tests/dsn/mysql-session.json이 그 값을 기록했지만, packages/go/orm/dsn_test.go는 sql_mode 없는 DSN을 기대해 TestParseDSN이 mysql tcp와 mysql socket and UTC에서 실패했다. 인수 조건: MySQL 기대값 두 곳이 T104 규약의 sql_mode 값을 담고 TestParseDSN이 통과한다.—근거: main에서 RED: go test -run '^TestParseDSN$' ./orm이 exit 1이며, mysql_tcp와 mysql_socket_and_UTC에서만 기대 문자열에 없는 sql_mode=%27STRICT_ALL_TABLES...%27가 parseDSN() 출력에 있다; 변경 후 GREEN: 같은 명령이 TestParseDSN PASS, exit 0.[o]
T77-40Go 테스트는 date와 datetime 컬럼에 native 시간값이 아니라 텍스트를 bind한다. 원인: 엔진은 date나 datetime bind에 native time.Time을 CODEC_ENCODE로 거부한다(T89-3, T189). 그런데 cancel_test.go(start_dt, 4곳), value_types_test.go(day), model/model_test.go(GtStartDt)가 여전히 시간값을 넘겨서 TestWithContextCancels, TestRuntimeValueTypes, TestConditions/postgres가 실패했다. 인수 조건: 해당 bind가 setter가 받는 텍스트 형식을 넘기고, 세 테스트가 repo 테스트 환경에서 통과한다.—근거: make test-servers 환경의 main에서 RED: TestWithContextCancels는 CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value(cancel_test.go:47), TestRuntimeValueTypes는 CODEC_ENCODE: parameter 3 (time.Time) is not a date value(value_types_test.go:62), TestConditions/postgres는 같은 CODEC_ENCODE로 panic. 변경 후 GREEN: 세 명령이 각각 exit 0, PASS.[o]
T77-41PHP restore는 hook이 남긴 요청에서 키를 읽는다. 원인: restore()가 writeOf 전에 자기 파라미터 맵을 만들었는데, hook 뒤 planner의 canonicalize(Db.php plan, Request::canonicalize)가 파라미터 번호를 다시 매기므로 restoreKeyValue가 재번호된 set 인덱스를 옛 맵과 비교해, hook의 y 대신 모델의 label x를 돌려주고 restore가 행을 찾지 못했다(Model.php get의 NO_ROWS). Go는 같은 요청에서 번호를 다시 매기지 않고 키를 읽는다. 인수 조건: restore가 hook 직후, planner가 번호를 다시 매기기 전에 키와 인자를 읽고, restore_rewritten_key가 통과한다.—근거: 변경 전 main에서 RED: php phpunit RequestHookTest.php의 restore_rewritten_key가 NO_ROWS로 실패(Model.php get, 키 인자 ['a','x']); 변경 후 GREEN: 같은 suite가 restore_rewritten_key를 통과(착지 커밋 참조).[o]
T77-43모든 런타임이 키를 set 값으로 받는 restore 행을 되돌린다. 원인: TypeScript, Go, Python에서 T77-41과 같은 결함이다. restore()가 write 전에 자기 파라미터 맵을 만들었는데, hook 뒤 planner의 canonicalize가 파라미터 번호를 다시 매겨서 키 인자가 옛 번호로 읽혔다. 공유 케이스 restore_rewritten_key_set_scope(contracts/features.json, 다섯 owner)가 where 파라미터 없이 label set 항목에서 이를 보여준다(canonical 순서에서 label이 옛 인덱스를 얻는다). 인수 조건: 모든 런타임이 hook 직후, canonicalize 이전에 키와 인자를 읽고, 공유 케이스가 세 DB에서 통과한다.—근거: 변경 전 main에서 RED: TypeScript coverage_request_hook.mjs가 mysql, postgres, sqlite에서 restore_rewritten_key_set_scope 실패; Go TestRequestHook/restore_rewritten_key_set_scope가 NO_ROWS; Python test_restore_rewritten_key_set_scope가 NO_ROWS. 변경 후 GREEN: TS coverage 3개와 request_hook exit 0; Go TestRequestHook exit 0, PASS 12; Rust restore_rewritten_key_set_scope ok; PHP RequestHookTest OK(11); Python test_request_hook OK(15), coverage_request_hook OK(23)와 PASS CASE 3줄.[o]
T77-44CHECK_TARGETS의 모든 input은 추적 파일을 가리키고, Python 인터페이스 unit 검사는 자기 test를 실행한다. 원인: check-inputs.json의 input 세 개가 추적 파일이 없는 경로를 가리켰고(tests/dbspec/introspect/, tests/dbspec/apply/, tests/interfaces/test_.py), python-interface-unit-check는 test_.py가 없는 tests/interfaces를 탐색해 0개를 실행하고 통과했다. interfaces-python 기능은 test_python_extractor.py와 test_python_natives.py를 주장하지만 그 검사는 실행하지 않았다. 인수 조건: 각 input이 대상이 실행하는 추적 파일을 가리키고, 인터페이스 unit 검사가 13개 test를 실행한다.—근거: main에서 RED: make python-interface-unit-check가 Ran 0 tests와 exit 2를 냈고, make feature-unit-check는 추적 파일이 없는 input 세 개를 보고했다; 변경 후 GREEN: 대상이 Ran 13 tests, OK, exit 0을 내고 input 검사가 통과한다.[o]
T77-45추적하는 모든 파일은 행동 시험을 고르거나 헬퍼 scope를 선언하고, 선언된 owner target은 자기 파일을 실행한다. 원인: 추적 파일 16개가 어떤 owner target에도 골라지지 않았다. 11개는 자기를 실행하는 harness의 헬퍼다(PHP, PHP 확장, TypeScript 클라이언트의 dbspec runner와 PHP 공유 DSN helper). 이 헬퍼들은 harness를 명시한 suite scope를 받는다. 5개는 실제 시험 파일이며 실행 주체가 parity, feature, harness 선언이었다: TestSubtestNamesAreFlat(packages/go/engine/dbspec/names_test.go)는 dbspec-go-check/engine에서 실행된다. DbspecPlanReaderTest.php는 dbspec-plan-php-check에서, conformance_vector_count.test.mts는 conformance-ts-suite-check에서 실행된다. TestCoveragePerformanceGate(packages/go/bench/coverage_performance_gate_test.go, 빌드 태그 featurecoverage)는 perf-check의 perf-test 단계로 실행된다. 시험 파일의 scope 선언을 금하는 규칙은 AGENTS.md에 기록했다. 인수 조건: make feature-unit-check가 exit 0이고, 연결된 각 target이 repo 환경에서 exit 0이다.—근거: main에서 RED: make feature-unit-check가 exit 2이며 16개 파일을 보고한다; GREEN: make feature-unit-check exit 0; dbspec-plan-php-check exit 0(39, 1 tests OK); dbspec-go-check/engine exit 0(PASS); conformance-ts-suite-check exit 0; bench 환경의 perf-check exit 0이며 TestCoveragePerformanceGate PASS(1.37s).[o]
T77-46Go feature unit들은 feature-check가 주는 환경을 가진 wrapper 하나로 실행한다. 원인: Go feature unit이 go test를 직접 실행해 ORM_FEATURE_DATABASE와 ORM_FEATURE_DSN이 없었고, 네이티브 테스트가 시작되지 못해 이름 없이 exit 1로 실패했다. 그 환경은 scripts/features/coverage.mts의 runLane 안에서만 만들어졌다. 인수 조건: coverage.mts가 featureEnvironment와 databaseDsn을 export하고 runLane과 항목 목록이 그것을 호출하며, wrapper(packages/go/testing/feature-json.mts) 하나가 DB 항목과 테스트 이름을 받아 그 함수로 환경을 얻고 go test -json을 출력한다.—근거: red: ORM_BENCH_MYSQL_DSN 없이 wrapper가 mysql database DSN is not set in ORM_BENCH_MYSQL_DSN으로 exit 1(대체 값 없음); green: 다른 작업 디렉터리에서 postgres의 TestCoverageCompositeKeyRows가 pass 이벤트 두 개와 exit 0이고, kit goEvents가 출력에서 케이스 두 개를 읽음; node --test scripts/features/coverage.test.mts exit 0.[o]
T77-47config/parity.json의 언어 집합 선언은 집합을 가진 코드와 맞는다. 원인: languageLists가 tests/interfaces/contracts/validate.go에 ^var languages = []string{...}$ 패턴을 선언했으나, T77-10 이후 그 파일은 clientLanguages()로 config/parity.json에서 집합을 읽어서 패턴이 아무것도 맞추지 않고 R9가 낡은 선언이라고 보고한다. 리터럴 목록과 맞출 것이 없는 선언은 비교할 대상이 없으므로, 호출에 맞게 고쳐 쓰지 않고 제거한다. 인수 조건: make parity-declarations-check가 R9 finding 없이 exit 0이다.—근거: red: make parity-declarations-check가 R9 language-set-drift tests/interfaces/contracts/validate.go를 보고(패턴이 아무것도 맞추지 않음, exit 2); green: 제거 후 every declaration holds를 출력하고 exit 0.[o]
T77-48DB 항목이 있는 Go feature unit 열두 개는 DB마다 wrapper에 바인딩된다. 원인: model과 orm 패키지의 Go feature unit들이 원시 go test에 바인딩되어 환경 없이 이름 없이 exit 1로 실패했다. 인수 조건: DB 항목이 있는 unit은 자기 DB와 DSN 변수로 wrapper를 바인딩한다. 기존 unit id는 mysql을, -postgres와 -sqlite unit은 나머지 두 항목을 같은 테스트로 실행하며, make parity-declarations-check가 통과한다.—근거: make parity-declarations-check가 명령 바인딩 36개와 함께 every declaration holds를 출력하고 exit 0; wrapper 동작 자체는 T77-46에 있다. 이 unit들을 세는 전체 parity 실행은 main의 다음 전체 실행이다.[o]
T77-48-1wrapper는 go test -run에 넘기는 테스트 이름을 이스케이프한다. 원인: wrapper가 테스트 이름을 이스케이프하지 않고 -run 패턴에 넣어, 정규식 문법이 든 이름이 실행 대상을 바꿀 수 있었다. kit은 최상위 이름을 문법 문자로 이스케이프한다(parity-runners.mts). 인수 조건: wrapper가 이름을 Go 식별자로 검증하고, 같은 문자로 이스케이프하며, 그 밖의 이름은 거부한다.—근거: green: 다른 디렉터리에서 postgres의 TestCoverageCompositeKeyRows가 pass 이벤트와 exit 0; red: 정규식 문법 이름 Test.*가 exit 1로 --test must be a Go identifier 오류.[o]
T77-49PHP 확장의 dbspec 시험 복사본은 PHP client의 것과 맞고, 공유 DSN helper는 testing 디렉터리에 있다. 원인: php-ext의 시험 복사본 세 개(DbspecPlanTest, DbspecApplyTest, DbspecApplyCleanupTest)가 client와 공유하는 plans.json 읽기를 중복 정의하거나 공유 case의 require가 없어 test_mirrors가 실패했고, packages/php/tests/required_dsn.php는 시험 디렉터리의 헬퍼라서 R5가 선언되지 않은 시험으로 보고했다. 인수 조건: 복사본들이 plan_vectors와 공유 case를 packages/php/tests/dbspec_plan_cases.php와 coverage_cases.php에서 읽고, helper는 packages/php/testing/required_dsn.php로 옮겨져 두 loader가 그 경로를 쓴다.—근거: red: make dbspec-php-extension-check가 test_mirrors 실패(DbspecApplyCleanupTest differs), make parity-declarations-check가 required_dsn.php에 R5를 보고; green: dbspec-php-extension-check exit 0이며 선언과 미러 시험이 OK(437개 + 5개, 1748 + 27 assertion), 선언 검사가 더 이상 R5를 보고하지 않음.[o]
T77-50goexit 시험은 datetime 컬럼에 native 시간값이 아니라 텍스트를 bind한다. 원인: goexit_test.go가 start_dt에 time.Now()를 두 번 넣었고, 엔진은 datetime bind의 native time.Time을 CODEC_ENCODE로 거부하므로(T89-3, T189) TestTransactionRollsBackWhenTheCallbackLeaves가 실패했다. T77-40과 같은 결함이 시험 파일 하나 더에 있었다. 인수 조건: 두 bind가 텍스트 형식을 넘기고, 시험이 repo 환경에서 통과한다.—근거: red: go test -run '^TestTransactionRollsBackWhenTheCallbackLeaves$' ./orm가 CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value(goexit_test.go:60)로 실패; green: 같은 명령이 PASS로 exit 0.[o]
T77-51interface 기록 실행은 manifest의 symbol hash를 쓴다. 원인: -record가 symbol snapshot은 썼지만 contracts/interfaces.json의 symbol_hashes와 확장 symbol_hash는 쓰지 않았고 검사만 읽었다. 그래서 기록된 변경마다 manifest 해시가 낡았고, 해시는 손으로 관리되어 왔다. 인수 조건: 기록 실행이 기록한 해시를 contracts/interfaces.json의 해당 항목 값만 바꿔 쓰고, 다음 검사에 해시 차이가 없다.—근거: red: 시그니처 변경 뒤 기록 실행이 go, php, rust, typescript, python의 symbol snapshot hash differs를 보고해 manifest는 손으로만 고칠 수 있었다; green: 변경 후 기록 실행이 해당 항목만 다시 쓰고, 이어진 검사에 해시 차이가 없다.[o]
T77-52PHP CollectionKey 규약은 PHP가 구현하는 키 집합이다. 원인: 규약 항목 CollectionKey가 PHP 키를 string|int로 선언했으나, PHP Collection::get과 keyOf는 int, string, bool, Bytes, DateTimeInterface를 받고 CollectionIdentityTest가 다섯 가지를 모두 시험하며, TypeScript Key는 number, bigint, string, boolean, Uint8Array, Date다. 그래서 규약이 낡은 쪽이었다. 인수 조건: PHP 항목이 구현 집합을 나열하고 generate 검사가 통과하며, 다른 런타임은 바뀌지 않는다.—근거: red: TestLogicalContractRejectsNativeDrift가 Collection.get/php arguments Polyspec\Orm\Bytes|DateTimeInterface|string|int|bool$key do not implement [CollectionKey]를 보고; green: 변경 후 시험이 ok이고, 심볼 snapshot과 manifest 해시는 기록 실행(T77-51)으로 기록되어 interface-check가 exit 0.[o]
T77-53CHECK_TARGETS의 모든 검사 대상은 CI 그룹에서 실행된다. 원인: T77-45가 dbspec-go-check/engine과 perf-check를 CHECK_TARGETS에 넣었지만 어떤 CI 그룹도 실행하지 않아 생성 검사 CI 시험이 실패했다. 인수 조건: dbspec-go-check/engine은 CI_TARGETS_dbspec에서, perf-check는 CI_TARGETS_clients_db에서 실행되고 CI 요구 시험이 통과한다.—근거: red: make interface-check가 CI_GROUPS의 어떤 그룹도 dbspec-go-check/engine과 perf-check를 실행하지 않는다고 보고; green: main에서 make interface-check가 exit 0.[o]
T77-54케이스를 하나도 실행하지 않은 시험 실행은 실패한다. 원인: 단위 검사가 실행한 케이스가 없어도 통과했다. 예를 들어 어떤 이름과도 맞지 않는 go test -run은 no tests to run을 내고 exit 0이어서, 증명하려던 것이 증명되지 않았다. 인수 조건: scripts/check/count-cases.mts가 실행 출력을 kit의 이벤트 리더(@polyspec/kit/test-output의 goEvents, nodeEvents, cargoEvents, pythonEvents, phpunitEvents)로 읽어 케이스를 세고(id가 name과 다른 참조. 패키지나 suite 결과는 케이스가 아니다), 0이면 exit 1로 실패한다.—근거: red: go test -run '^NoSuchTestXYZ$' ./orm가 [no tests to run]과 exit 0을 냈다; green: 그 출력에 count-cases가 zero go cases reported와 exit 1을 내고, TestCoverageCompositeKeyRows의 실제 go test -json 출력에는 케이스 1개와 exit 0을 내며, node --test scripts/check/count-cases.test.mts가 시험 3개를 통과한다.[o]
T77-56두 physical Go unit은 make 대상이 내는 go test -json 스트림에서 케이스를 읽는다. 원인: go-internal-dialects-apply-clients-physical과 go-internal-dialects-introspect-compare-physical은 make 대상 안에서 돌아 go test -v 텍스트를 냈고, kit은 그것을 케이스로 읽지 못해 읽을 수 있는 이름 없이 exit 1로 실패했다. 인수 조건: GO_TEST_FLAGS(기본값 빈 값)가 모든 repo Go 시험에 전달되고, tests/go-test.mts는 -json이 주어지면 원시 test2json 스트림을 쓰며, 각 unit은 command make -s 자기 대상 GO_TEST_FLAGS=-json을 바인딩한다. 케이스 집계는 kit과 같다(pass와 fail 결과).—근거: red: unit이 읽을 수 있는 이름이 없고 exit 1; green: make -s dbspec-apply-pairs-check GO_TEST_FLAGS=-json이 stdout에 go test -json 스트림을 쓰고, kit goEvents가 그것에서 pass 271, fail 0 케이스를 읽음(나머지 stdout 22줄은 준비 단계 텍스트이며 리더가 무시); make parity-declarations-check가 통과.[o]
T77-57Python connection UTC 시험은 instant를 native datetime이 아니라 offset 텍스트로 bind한다. 원인: connection_utc.py가 timezone을 가진 datetime.datetime을 set_start_dt와 start_dt에 넘겼고, 엔진은 datetime bind의 native datetime을 거부한다(CODEC_ENCODE; docs/protocol.md의 datetime 파라미터는 텍스트이고 offset 텍스트는 UTC로 바뀐다). 인수 조건: 같은 instant를 offset 텍스트로 쓰고 필터하며, 시험이 세 DB에서 통과한다.—근거: red: python -m unittest packages/python/tests/connection_utc.py가 CODEC_ENCODE: parameter 5 (datetime) is not a datetime value로 실패; green: 변경 후 같은 명령이 OK (2 tests)와 exit 0.[o]
T77-550개 케이스 규칙은 kit의 run-tests를 쓰고, orm의 카운터는 제거한다. 원인: T77-54가 kit의 리더로 케이스를 세는 scripts/check/count-cases.mts를 썼으나, kit은 이미 @polyspec/kit/run-tests를 export한다. 이 도구는 go, cargo, phpunit, python의 리더로 시험 도구를 실행하고, 통과도 실패도 시간 초과도 없으면 no test ran으로 실패한다. 둘 다 두면 같은 규칙에 러너가 둘이다. 인수 조건: orm 카운터와 그 시험을 제거하고, unit 레시피가 kit의 run-tests를 쓴다.—근거: red: 같은 저장소의 go test에서 zero-match 필터는 [no tests to run]과 exit 0; green: 같은 zero-match 필터를 kit run-tests가 실행하면 exit 1과 no test ran.[o]
T77-42Python request-hook coverage 모듈은 항목별 case만 실행한다. 원인: CoverageRequestHook이 RequestHookTest를 상속한 모듈 수준 TestCase여서 모듈 전체를 돌릴 때 unittest가 찾았고, 상속된 case가 빈 driver로 실행되어 모든 case가 KeyError ''로 실패했다(11 ERROR). 그래서 py-cov-request-hook은 coverage id 10개가 모두 통과해도 exit 1이었다. 인수 조건: 클래스를 항목의 driver로 항목마다 만들고, 모듈이 오류 없이 실행된다.—근거: make databases-create의 bench database로 main에서 RED: python -m unittest packages/python/tests/coverage_request_hook.py가 32개 중 11 ERROR(case_database의 KeyError ''), CASE 줄은 sqlite, postgres, mysql 모두 PASS; 변경 후 GREEN: 같은 명령이 21개 OK, exit 0.[o]
T77언어 parity를 선언하고 도구가 검사한다(kit K33 release 필요). 원인: 여섯 구현의 parity를 손으로 유지하고 있다. scripts/features/check.mjs:20과 coverage.mjs:15는 네 언어만, tests/conformance/check/main.go:101은 다섯 언어를 다룬다. contracts/features.json은 18개 기능 모두 Python을 planned로 표시하므로 Python test는 하나도 요구되지 않는다. vector_contract는 test 없이도 모든 언어를 backed로 판정한다. interface-check는 snapshot을 비교할 뿐 symbol에 대응 symbol이 있는지 묻지 않는다. input glob이 고른 test는 아무것도 실행하지 않아도 통과한다. 불일치는 검사가 아니라 사람의 검색으로 발견된다. 완료 기준: kit 원천은 링크된 kit main 2fc6e2bd 체크아웃이다(2026-10-18 결정, 로컬 개발: 릴리스 없음, 소비자는 링크로 읽는다. kit-sync를 태그로 하는 조항은 이 결정으로 대체되며, 소유자가 되돌릴 수 있다).—Commands: make feature-unit-check/coverage, make feature-unit-check/owners[!] 원인: 이 sandbox가 제공할 수 없는 CI의 database와 Rust 실행이 필요하다; 재시도: CI에서 parity bindings 실행이 통과한 뒤
T77-1Python이 다른 구현과 같은 test harness를 가진다. 원인: Python만 case deadline이 있는 RUN/STEP/PASS/FAIL 보고가 없어서, 실행한 case를 관측할 수 없다. 완료 기준: 모든 Python test가 Go, JS, PHP, Rust와 같은 형식의 harness로 보고한다. red 다음 green.—Evidence: packages/python/tests/testcase.py가 unittest에 공유 harness를 준다(deadline과 함께 RUN, STEP, PASS, FAIL, SKIP. COMPUTE, DATABASE, PROCESS. deadline을 넘긴 case는 실패하고 process는 1로 끝난다). 모든 test module이 그것으로 실행되고 test_testcase.py가 그것을 검사한다. RED: import가 실패했고, 그다음 test_modules_run_through_the_harness가 module 61개를 적었다. python-unit-check 710건 중 710건, python-interface-unit-check 13건이 통과한다.[o]
T77-2Python vector runner가 실행된다. 원인: codec_vector.py와 dbspec_* runner 다섯은 test_*.py discovery에 걸리지 않아(Makefile:455) 아무도 실행하지 않는데, check-inputs.json:67은 이들을 고른다. codec-check(Makefile:985-999)와 apply_clients_physical_test.go:32는 Python을 뺀다. 완료 기준: runner가 Python package test에서 실행되고, codec-check와 apply 쌍 비교가 Python을 포함한다. codec-check/python이 codec-check/compare를 위해 ORM_CODEC_OUT을 쓰고, PHP 확장 parity 실행이 함께 들어가며, docs/spec/codec.md(EN과 .ko.md)가 runner 목록을 적는다. 먼저 red: 첫 실행이 Python의 모든 차이를 보인다(공백 차이는 T74의 것이다). parity 검사가 그 case의 실행을 보여 준다.—Evidence: dbspec_apply.py가 dbspec-python-check의 harness test로 실행되고, codec runner가 PHP CodecVectorsTest가 decode하는 python.json을 쓰며, dbspec_apply_runner.py가 client 여섯의 apply chain(실행 270번)에 들어간다. RED: codec-check가 91건(python 출력 없음), pair가 PostgreSQL 20건 실패했다. 후자는 실제 결함으로, PostgreSQL apply가 lock_timeout을 %s marker로 복원했고 지금은 다른 client처럼 $1이다. pair 270건 중 270건, codec-check, dbspec-python-check가 통과한다.[o]
T77-3Python이 모든 공유 suite를 소비한다. 원인: Python은 contracts/fixtures 일곱, planner.json, sqlite-paths.json, event vector를 하나도 실행하지 않는다. 완료 기준: 각 suite가 먼저 parity 검사에서 red(실행된 case 0)를 보이고, 그다음 Python이 모든 case를 실행해 일치한다. packages/orm-python/tests/coverage_planner.py가 planner.json의 모든 case를 실행하고 그 검사는 다른 언어처럼 contracts/features.json에 선언되며, packages/orm-php-extension/tests/coverage_planner_test.php가 native Dbspec을 load한 채 PHP case를 실행한다. 첫 실행이 보이는 불일치는 고치거나 항목으로 등록한다. 이 항목은 validator 항목 T83부터 T99보다 먼저 들어간다.—Evidence: Python이 건너뛰던 공유 suite 8개(aggregate_numeric, authenticated_encryption, decimal_model, planner, styled_column_states, install/changed_database, sqlite-paths, events)마다 config/parity.json에 bind된 Python test가 있고 모든 case를 fixture id로 보고한다. 첫 실행이 결함 셋을 찾아 고쳤다: get_sum/get_avg는 NaN, inf, raw 오류 대신 CODEC_DECODE를 답하고, percent escape가 깨진 SQLite DSN 경로는 CONFIG이며, MySQL install이 statement event를 발행한다. parity가 실행하는 방식으로 모든 suite가 모든 case를 통과하고, python R2 finding이 8에서 0이 됐다.[o]
T77-4모든 구현이 생성된 오류 catalog를 가진다. 원인: docs/spec/errors.yaml은 Go, PHP, Rust 상수만 생성한다. TypeScript와 Python에는 RELATION_UNKNOWN과 WRITE_CONFLICT가, Python에는 DIALECT_UNKNOWN도 없다. 완료 기준: generator가 TypeScript와 Python catalog를 내고, parity 검사가 다섯 client를 errors.yaml과 비교한다. PHP 확장은 dbspec 표면만 구현하고 orm 오류 code가 없으며, 선언의 profile이 이를 적는다.—근거: red: make parity-declarations-check가 TypeScript와 Python에 R7 error-catalog ... declares no error codes를 보고했고(rust의 R5 finding과 합쳐 3건), node packages/ts/tests/code-catalog.mjs는 packages/ts/src/codes.ts가 없어 ENOENT로 실패했다. green: orm-gen errors --lang typescript와 --lang python이 errors.yaml의 40개 code를 packages/ts/src/codes.ts와 packages/python/src/polyspec/orm/codes.py에 쓴다. make parity-declarations-check는 R7 finding이 없다(남은 R5 finding은 T148-3-7-1과 main의 T169-3 test의 것이다). node packages/ts/tests/code-catalog.mjs 1건 통과; make python-unit-check는 1332건 중 1331건 통과(실패 1건 test_plan_cache_bound.py는 main의 것이다); TypeScript unit dsn 16, markdown 95, styled-value 16, codec-vector 135, dbspec 2, dbspec-rules 48, generate-scan 3, request_hook 11, package-declarations 5, group_rows 1건이 통과하고, engine.mjs(1 check)와 generate.mjs(1 check)는 main에서도 같은 방식으로 실패한다. codes.ts나 codes.py의 note를 바꾸면 drift test가 실패한다. PHP 확장은 client profile 밖에 남는다.[o]
T77-5AES나 blind index key가 없으면 어디서나 CONFIG다. 원인: Go, PHP, Rust는 CONFIG(errors.yaml:62)를, TypeScript와 Python은 CODEC_* code를 돌려준다. 완료 기준: aes와 blind_index의 빈 key, 그리고 key 없는 aes decode에 대한 tests/codec/vectors.json 공유 vector case가 먼저 TypeScript와 Python에서 red(codec.ts:57, codec.py:149는 CODEC_DECODE), 그다음 여섯 구현 모두 green. 잘못된 암호문은 CODEC_DECODE로 남는다. "CONFIG or CODEC_DECODE"라고 적은 docs/spec/dialects.md:57과 docs/spec/codec.md(EN과 .ko.md)가 key가 없으면 CONFIG, 잘못된 암호문이면 CODEC_DECODE라고 적는다.—근거: commit 32c6fdef(main; 8e1f7762를 1d1176fb 위로 rebase, codec 오류는 main이 쓰는 생성된 codes.CONFIG 상수를 쓴다): 누락된 AES key와 빈 blind-index key는 TypeScript와 Python에서 CONFIG를 CONFIG로 판정한다. Go, PHP, Rust는 이미 그렇게 CONFIG로 판정한다. 잘못된 ciphertext는 CODEC_DECODE로 남는다. red 후 green: TypeScript codec-aes-vector.mjs(encode, decode, blind_index에서 key가 비었을 때 CODEC_ENCODE/CODEC_DECODE, 기대 CONFIG) 후 5 of 5; Python test_codec_aes_vectors 후 OK(5 of 5). Go, PHP, Rust는 이미 CONFIG를 답하고 codec vector test가 통과한다. docs/codec.md와 docs/dialects.md에 이 규칙이 EN과 KO로 적혀 있다.[o]
T77-6collection key의 identity가 모든 구현에서 하나다. 원인: PHP는 NULL key를 ''로 바꾸고(Collection.php:24), Go는 거절하며(collection.go:87), TypeScript는 '\u0000' sentinel을 쓴다(database.ts:306). 완료 기준: tests/collection/keys.json이 NULL, "", false, 0, "0", true, bytes, datetime key를 고정한다. 먼저 red, 그다음 여섯 구현이 일치하며, PHP는 Go처럼 NULL key에 CODEC_DECODE "collection row identity contains SQL NULL"을 던진다. DB case collection_null_identity는 root collection API(KeyNamePrice().Gets())로 SQL NULL price가 있는 row를 읽으며, 각 client/DB에서 정확히 SELECT 한 번 뒤 CODEC_DECODE를 확인한다. 이는 nullable key_by를 SQL 실행 전 IR_INVALID로 거부하는 relation_null_key와 별개이며, 그 vector는 반환된 NULL row의 collection identity를 검증하지 않는다. 문서(EN과 .ko.md)가 규칙을 적고, protocol.md §2 assemble.key("NULL key 성분은 CODEC_DECODE다")도 포함한다. 판정(2026-10-18): 구조 항목 T77-6-2(runner마다 chain interpreter 하나)와 그 하위 항목 6개를 철회하고, 각각 cause를 적은 불가능 행으로 남긴다. 각 runner는 자기 언어로 생성된 API를 호출하며, 이것이 conformance case가 검증하는 표면이다. JSON step 형식의 interpreter는 interpreter 다섯 개를 가진 새 작은 언어가 되고, 타입이 있는 API 대신 이름 dispatcher를 검증하게 되며, 실패하는 case를 하나도 고치지 않는다. 모든 vector가 통과하고, runner 사이에 본문이 달라지면 기록된 statement가 바뀌므로 기대값이 이미 비교한다.—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-1NULL collection key의 conformance case를 다섯 runner에서 실행한다. 범위: 다섯 runner만이며, PHP extension 경로는 T77-6-1-1(보류)이므로 여기에 포함하지 않는다. 원인: case는 database 실행에서 기록한 기대값이 필요하며, 그 실행은 push 뒤 CI에서 실행된다. 이 변경은 unit test만 실행한다. 완료 기준: nullable group의 group_count나 raw assembly로 nullable column을 key로 삼은 relation처럼 identity가 NULL인 collection을 읽는 conformance case가 다섯 runner에서 실행되고, 모든 출력이 일치할 때만 기대값을 기록한다.—증거: make run-databases TARGETS=conformance-check VECTOR=relation_null_key(record orm_check_81025_ca04457a, result passed, exit 0)가 SQLite, MySQL, PostgreSQL에서 다섯 runner를 실행하며, 각 database가 1 vectors × 5 languages identical과 go:ok php:ok typescript:ok python:ok rust:ok를 보고한다. 기대값은 T77-6-1-7이 이미 기록한 IR_INVALID 거부 "key_by price is nullable"이다. 참고: vector는 nullable key를 collection을 읽기 전에 거부하므로 그 자체가 NULL identity를 읽지는 않는다; 이는 T77-6-1-7의 기록된 설계다.[o]
T77-6-1-1PHP 확장이 NULL collection key conformance case를 실행한다(T77-6-1에 이어). 원인: packages/php-ext는 PHP client의 dbspec 표면만 구현하고(T77-4) conformance runner가 없어서 T77-6-1의 case를 실행할 수 없다. 수용: 확장에 case용 conformance runner가 있고(다른 runner와 같은 형태), MySQL, PostgreSQL, SQLite에서 출력이 다섯 runner와 일치하며 case가 통과한다. red: 확장에 relation_null_key용 runner가 없다. green: 확장에서 case가 통과한다. 제거 조건: 확장의 conformance runner가 생기면 이 하위 항목을 닫는다. 보류 사유: 확장에 query client가 아직 없다(C로 된 query, collection 및 나머지 ORM 표면이 없다; T199 참조). Retry: 다른 체크리스트 행이 모두 끝난 뒤. Retry(2026-10-18): 이 표면을 담은 T182(T182-6)와 함께, T182가 다시 시작할 때 다시 시작한다.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-2Go runner가 NULL collection key case relation_null_key의 handler를 가진다 (T77-6-1에 이어). 원인: Go runner는 등록되지 않은 vector 이름을 거절하므로 case가 어떤 database도 읽기 전에 실패한다. 완료 기준: handler가 nullable key relation(user 100에서 Author를 user seq로 맞추고 price로 key)을 만들고, package가 build와 vet을 통과한다.—증거: packages/go에서 go build와 go vet ./internal/conformance/가 통과한다. 기대값은 마지막 배치의 database 실행에서 기록한다.[o]
T77-6-1-3PHP runner가 relation_null_key의 handler를 가진다 (T77-6-1에 이어). 완료 기준: handler가 PHP client로 같은 relation을 만들고, runner가 등록한다.—Evidence: the handler is written and its file checks (see the commit).[o]
T77-6-1-4Rust runner가 relation_null_key의 handler를 가진다 (T77-6-1에 이어). 완료 기준: handler가 Rust client로 같은 relation을 만들고, runner가 등록한다.—Evidence: the handler is written and its file checks (see the commit).[o]
T77-6-1-5TypeScript runner가 relation_null_key의 handler를 가진다 (T77-6-1에 이어). 완료 기준: handler가 TypeScript client로 같은 relation을 만들고, runner가 등록한다.—Evidence: the handler is written and its file checks (see the commit).[o]
T77-6-1-6Python runner가 relation_null_key의 handler를 가진다 (T77-6-1에 이어). 완료 기준: handler가 Python client로 같은 relation을 만들고, runner가 등록한다.—Evidence: the handler is written and its file checks (see the commit).[o]
T77-6-1-7NULL collection key vector relation_null_key의 기대값을 SQLite, MySQL, PostgreSQL에 기록한다 (T77-6-1에 이어). 원인: nullable key_by는 IR이 IR_INVALID로 거절하므로, 벡터는 CODEC_DECODE가 아니라 그 거절을 기대하고 runner는 거절을 결과로 돌려준다. 완료 기준: 다섯 runner가 세 database에서 일치하고 기대값이 기록된다.—증거: make run-databases TARGETS=conformance-check VECTOR=relation_null_key로 다섯 runner를 SQLite, MySQL, PostgreSQL에서 실행하고, 15개 runner·database 조합이 모두 IR_INVALID "key_by price is nullable" 거절에 일치하며 세 compare 단계가 통과한다(dialects.md key_by 행).[o]
T77-6-1-8relation_null_key를 recorder가 쓰는 형식으로 기록한다 (T77-6-1-7에 이어). 원인: 이 vector의 expect는 statements를 result보다 앞에 두고 쓰였지만 recorder는 key를 정렬해 쓴다. 그래서 바뀌지 않은 출력을 기록해도 vector 파일 세 개가 다시 쓰이고 TestRecordKeepsTheRecordedFiles가 실패한다. 완료 기준: 세 파일이 recorder의 형식을 갖고 test가 통과한다.—증거: RED: main에서 TestRecordKeepsTheRecordedFiles가 mysql: the record changed the recorded file로 실패했고, 달라진 다섯 줄은 relation_null_key expect의 key 순서였다. GREEN: 세 파일에서 key를 정렬한 뒤(6줄 이동, 값 변경 없음) test가 통과한다. 결정(owner가 2026-10-18에 전달): PHP 확장은 모든 기능을 C로 구현한다. 다른 개발 행이 모두 끝난 뒤 재개하며, 그때까지 보류한다.[o]
T77-6-2conformance runner가 vector의 chain을 vector 이름마다가 아니라 한 번 해석한다(T77-6의 구조 개선). 원인: 다섯 runner(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)가 vector 이름마다 손으로 쓴 본문을 가져서, vector가 늘 때마다 같은 로직을 다섯 번 반복한다. 계획: (1) tests/conformance/vectors*.json의 chain 스키마를 단일 형식으로 고정한다(step 종류는 기존 본문이 쓰는 begin, commit, create, gets, relations 등); (2) runner마다 vector의 step을 실행하고 vector가 기대하는 결과 형태를 돌려주는 chain 해석기를 하나씩 둔다; (3) 이름별 본문을 해석기 호출로 한 vector씩 바꾸며, 각 vector는 바뀌지 않은 기대값에 대해 red 후 green이다; (4) 해석기를 step 종류별로 단위 test한다; (5) 기존 vector 전부가 바뀐 뒤에도 같은 결과를 보고한다. 수용: 어느 runner에도 이름별 본문이 남지 않고, 모든 vector가 바뀌지 않은 기대값으로 통과한다. 보류 사유: 다섯 runner의 파괴적 구조 변경은 사용자가 지금 요구하지 않았다. Retry: 사용자가 이 구조 변경의 보류를 해제한다. 결정(사용자, 2026-10-18): 보류를 해제했다. Cause: T77-6 아래 판정으로 철회했다(2026-10-18).——[-] cause: 다섯 runner(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)가 vector 이름마다 손으로 쓴 본문을 가져서, vector가 늘 때마다 같은 로직을 다섯 번 반복한다. 계획: (1) tests/conformance/vectors*.json의 chain 스키마를 단일 형식으로 고정한다(step 종류는 기존 본문이 쓰는 begin, commit, create, gets, relations 등); (2) runner마다 vector의 step을 실행하고 vector가 기대하는 결과 형태를 돌려주는 chain 해석기를 하나씩 둔다; (3) 이름별 본문을 해석기 호출로 한 vector씩 바꾸며, 각 vector는 바뀌지 않은 기대값에 대해 red 후 green이다; (4) 해석기를 step 종류별로 단위 test한다; (5) 기존 vector 전부가 바뀐 뒤에도 같은 결과를 보고한다. 수용: 어느 runner에도 이름별 본문이 남지 않고, 모든 vector가 바뀌지 않은 기대값으로 통과한다. 보류 사유: 다섯 runner의 파괴적 구조 변경은 사용자가 지금 요구하지 않았다
T77-6-2-1conformance vector의 step 형식을 명세한다 (T77-6-2에 이어). 원인: vector는 chain을 표시용 text(chain)로만 담고, program은 다섯 runner가 갖고 있다: lambda subquery, 중첩 relation, 여러 operation, 고정 clock, mask된 key, 파생 결과. 완료 기준: tests/conformance/schema와 README(+ko)가 steps를 정의한다. steps는 이름이 지정된 model에 대한 operation의 JSON 목록이며, 51개 본문이 쓰는 모든 kind를 담고, kind마다 받아들이는 schema case와 거부하는 schema case가 있다. vectors*.json의 모든 vector가 steps를 갖고, chain은 그 rendering이 된다. Cause: T77-6 아래 판정으로 철회했다(2026-10-18).——[-] cause: vector는 chain을 표시용 text(chain)로만 담고, program은 다섯 runner가 갖고 있다: lambda subquery, 중첩 relation, 여러 operation, 고정 clock, mask된 key, 파생 결과. 완료 기준: tests/conformance/schema와 README(+ko)가 steps를 정의한다. steps는 이름이 지정된 model에 대한 operation의 JSON 목록이며, 51개 본문이 쓰는 모든 kind를 담고, kind마다 받아들이는 schema case와 거부하는 schema case가 있다. vectors*.json의 모든 vector가 steps를 갖고, chain은 그 rendering이 된다
T77-6-2-2TypeScript runner가 steps를 해석한다 (T77-6-2-1에 이어). 완료 기준: interpreter 하나와 step kind마다 unit test가 있다. packages/ts/testing/conformance/runner_typescript.mts에 이름별 본문이 남지 않는다. 모든 vector가 MySQL, PostgreSQL, SQLite에서 바뀌지 않은 기대값을 반환한다. Cause: T77-6 아래 판정으로 철회했다(2026-10-18).——[-] cause: T77-6 아래 판정으로 철회했다(2026-10-18)
T77-6-2-3Python runner가 steps를 해석한다 (T77-6-2-1에 이어). 완료 기준: packages/python/testing/conformance/runner_python.py에 대해 T77-6-2-2와 같다. Cause: T77-6 아래 판정으로 철회했다(2026-10-18).——[-] cause: T77-6 아래 판정으로 철회했다(2026-10-18)
T77-6-2-4PHP runner가 steps를 해석한다 (T77-6-2-1에 이어). 완료 기준: packages/php/testing/conformance/runner.php에 대해 T77-6-2-2와 같다. Cause: T77-6 아래 판정으로 철회했다(2026-10-18).——[-] cause: T77-6 아래 판정으로 철회했다(2026-10-18)
T77-6-2-5Go runner가 steps를 해석한다 (T77-6-2-1에 이어). 완료 기준: packages/go/internal/conformance/main.go에 대해 T77-6-2-2와 같고, model 호출은 이름에 대한 reflection이 아니라 생성된 model API로 dispatch한다. Cause: T77-6 아래 판정으로 철회했다(2026-10-18).——[-] cause: T77-6 아래 판정으로 철회했다(2026-10-18)
T77-6-2-6Rust runner가 steps를 해석한다 (T77-6-2-1에 이어). 완료 기준: packages/rust/tests/src/conformance.rs에 대해 T77-6-2-5와 같다. Cause: T77-6 아래 판정으로 철회했다(2026-10-18).——[-] cause: T77-6 아래 판정으로 철회했다(2026-10-18)
T77-6-3각 runner가 자기 vector가 이름 붙인 공개 method를 호출한다 (T77-6에 이어). 원인: conformance vector는 statement, bind, 결과를 고정하지만 그것을 만드는 공개 API는 고정하지 않는다. 그래서 다른 method로 같은 SQL에 도달한 runner도 통과하고, vector가 이름 붙인 method는 그 언어에서 검증되지 않은 채 남는다(예: terminal_by는 getBySeq를 이름 붙인다). T77-6 아래 판정은 타입 API를 우회하는 chain interpreter를 철회했다. 완료 기준: unit test가 각 vector의 chain을 읽어 그 안의 method 이름을 얻고, 그 vector의 본문이 runner의 명명 규칙(Go와 Rust는 camelCase를 자기 규칙으로 바꿈)으로 그중 하나도 호출하지 않는 runner마다 실패한다. 본문이 어긋난 현재 runner에서 red가 나오고, 그다음 각 runner가 이름이 지정된 method를 호출하거나, 어떤 runner도 호출할 수 없는 method를 이름 붙인 chain text를 고친다. test는 database 없이 실행된다.—증거: 각 vector가 이제 자기가 검증하는 공개 method 목록 calls를 선언한다(tests/conformance/schema/vectors.schema.json이 요구하고, checker가 읽고 기록한다). RED: tests/conformance/check/chain-methods.test.mts가 runner 본문에서 빠진 method를 찾았다. 본문을 정확히 읽게 한 뒤(Go의 여러 파일, Rust의 나뉜 run!(, Python의 등록과 PEP 8의 and_/or_/not_) 남은 것은 contains_ascii_fold와 contains_binary_code_point(모든 runner가 준비를 helper 하나로 공유하고, 본문은 검증 대상 method andLkName, andLbName를 호출한다), 그리고 contains_null이다. contains_null은 Go, Rust, TypeScript의 타입 있는 andLkName이 NULL을 받지 않아 core로 실행되며, vector가 그 경로를 밝히고 모든 runner가 하는 호출을 나열한다. GREEN: 다섯 runner에서 test가 통과한다. checker unit test 25개가 TestRecordKeepsTheRecordedFiles를 포함해 통과한다. make -s parity-declarations-check가 성립한다. make -s feature-unit-check는 main에 이미 있는 실패 두 개만 보고한다.[o]
T77-6-4복합 collection identity는 각 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.jsonmake collection-composite-key-check[o]
T77-7Python이 key_by: ""를 다른 구현처럼 읽는다. 원인: validate.py:151,155는 Go, PHP, TypeScript가 없는 것으로 읽는 빈 key_by를 거절한다. 완료 기준: T73이 추가하는 IR validate suite의 case와 planner.json case(key_by: ""인 root query는 유효하다)가 먼저 Python에서 red(실행에 T77-3이 필요하다), 그다음 PHP와 Rust도 같은 case에서 일치한다. protocol.md §1.2(EN과 .ko.md)가 빈 key_by는 지정하지 않은 것과 같다고 적는다.—Evidence: tests/ir/validate.json에 key-by-empty-is-absent와 key-by-empty-on-one-relation, planner.json에 planner_key_by_empty_is_absent가 생겼고 Go, TypeScript, PHP, Rust, Python이 실행한다. protocol.md 1.2절이 두 언어로 규칙을 적는다. RED: Python이 둘 다 COLUMN_UNKNOWN을 답했다. Rust fixture reader는 root key_by를 거부했는데 지금은 빈 값을 없는 것으로 읽는다. validate.json과 planner coverage가 다섯 구현에서 통과한다.[o]
T77-8모든 공개 symbol은 선언된 단위이거나 비공개다. 원인: TypeScript는 codec decode/encode를 decodeCodec/encodeCodec로 다시 export해 "한 개념은 한 이름" 규칙을 어긴다. Go와 Rust만 원시 AES 함수를 공개하고, Rust hex_decode, hex_upper, split_host, executor_stages, styled_cols, Python php_serialize, PHP decodeRows와 isHostStyle, TypeScript orderedJsonOutput도 한쪽에만 공개돼 있다. 완료 기준: interface extractor가 symbol의 export 여부를 기록한다. 각 symbol은 모든 구현의 API 단위가 되거나, 비공개가 되거나, 이유와 함께 native_only가 된다. codec, collection, AES를 포함한 API 단위의 원천은 config/parity.json 하나다. contracts/interfaces.json의 이름 규칙은 그리로 옮기며, interface-check가 단위를 거기서 읽으므로 두 번째 목록은 남지 않는다. interface-check는 parity-check보다 먼저 실행해, parity-check가 읽는 snapshot을 최신으로 둔다.—증거: commit 4f1077ea가 tree에서 다섯 client와 extension의 snapshot을 다시 기록하고, PHP Collection::get rule을 도입된 signature로 고치며, contracts/interfaces.json의 symbol hash를 갱신한다. commit 72f62afa가 TypeScript와 Python의 오류 catalog를 codes로 export하고, 다섯 client의 symbol extractor와 함께 오류 catalog를 config/parity.json의 kind api 단위 40개로 선언하며, interface-check가 그 단위를 읽게 한다(tests/interfaces/check/api.mts; interfaces/api-units ok). interface-check는 parity-check의 선행 조건이다. commit e3924345가 이 상태를 기록한다. 검사: make interface-check 종료 코드 0, self-test 포함(go, php, rust, typescript, python, php-extension ok); node --test tests/interfaces/check/*.test.mts 27 통과; node packages/ts/tests/code-catalog.mjs 2 통과; test_code_catalog.py 2 통과; make documents-check와 make commits-check 통과. make parity-declarations-check는 종료 코드 2이며 R5 finding 1개(rust packages/rust/testcase/src/lib.rs)를 보고한다. 이 항목의 것이 아니다. 남은 부분은 T77-8-1이다.[o]
T77-8-1다섯 client의 공개 symbol을 분류하고, codec, collection, AES 단위와 contracts/interfaces.json의 이름 규칙을 config/parity.json으로 옮긴다. 원인: T77-8은 오류 catalog 단위만 선언한다. extractor가 symbol의 export 여부를 기록하지 않고, client의 공개 symbol을 분류하지 않았으며, codec, collection, AES 단위를 선언하지 않았고, 이름 규칙은 아직 contracts/interfaces.json에 있다. 완료 기준: interface extractor가 symbol의 export 여부를 기록한다. 다섯 client의 각 공개 symbol은 모든 client에 대응이 있으면 API 단위가 되고, 없으면 이유와 함께 native_only가 된다. 대응이 없는 symbol에 단위를 만들지 않는다. codec, collection, AES 단위가 config/parity.json에 선언된다. contracts/interfaces.json의 이름 규칙이 그리로 옮겨져 interface-check가 목록 하나만 읽는다. make parity-check가 interface-check 다음에 실행되어 통과하며, 종료 코드와 finding을 기록한다.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-1step 2가 선언하지 않은 codec, collection, AES 공개 symbol을 분류한다: 다섯 client에 대응이 있으면 unit, 없으면 이유와 함께 native_only다. 원인: step 2는 unit 16개를 선언했다. codec decode와 encode(TypeScript가 이름을 바꾼다), collection key type, 모든 client가 공유하지 않는 member가 남아 있다. 완료 기준: 그 module의 선언되지 않은 공개 symbol마다 api unit 또는 이유를 적은 native_only 항목이 있고, 선언과 interface 단위 test가 통과한다. 진행: codec, collection, AES module의 unit 15개(kit id 75개, client마다 하나씩, 선언되지 않은 kit id에 대해 각각 확인)와 native_only 항목 31개가 config/parity.json에 선언됐다. 재계산: 새 추출(make interface-check, 스냅샷은 바뀌지 않음)과 make parity-declarations-check가 선언되지 않은 공개 symbol을 찾지 않는다(R11 finding 없음). 앞의 95개는 rename 전 추출에서 나왔다. docs/features.ko.md의 R10 docs-count finding은 model_generation의 한국어 문장 하나에서 나왔고, 원본인 contracts/features.json에서 고쳤다.—Evidence: make parity-declarations-check가 "every declaration holds"를 출력하고 make interface-check가 통과한다.[o]
T77-8-1-2dbspec 공개 symbol(reader, set, plan, schema)을 같은 방식으로 분류한다. 원인: client의 선언되지 않은 공개 symbol 대부분이 dbspec module에 있다. 완료 기준: T77-8-1-1과 같다. 진행: PHP, TypeScript, Python, Rust의 dbspec 공개 symbol(kit id 965개)은 config/parity.json의 native_only 항목이며, 각각 이유와 contracts/interfaces.json의 anchor를 가진다. Go는 이 묶음에 kit id가 없다(dbspec engine을 package orm이 다시 export하지 않으므로 unit이 불가능하다). unresolved: 없음. make parity-declarations-check는 이 항목들에서 finding을 내지 않으며, main에서 온 R10 docs-count finding 두 개(docs/features.md, docs/features.ko.md)는 이 항목의 것이 아니다.—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-3client 표면(Db, Model, Utils, Schema, request, engine, markdown)을 같은 방식으로 분류한다. 원인: client module의 나머지 공개 symbol이 선언되지 않았다. 완료 기준: T77-8-1-1과 같다. 진행: client 표면의 unit 37개(kit id 185개, client마다 하나씩, 선언되지 않은 kit id에 대해 각각 확인)와 native_only 항목 732개가 config/parity.json에 선언됐다. unresolved: 선언되지 않은 kit id 192개는 결정될 때까지 남는다. make parity-declarations-check는 이 항목들에서 finding을 내지 않으며, docs/features.md와 docs/features.ko.md의 R10 docs-count finding 두 개는 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-4bench set의 생성된 model을 같은 방식으로 분류한다. 원인: client마다 생성된 model class가 entity별로 반복되며 선언되지 않았다. 완료 기준: T77-8-1-1과 같다. 진행: TypeScript와 Python의 생성된 model symbol(kit id 2277개; Go, PHP, Rust는 bench set의 entity class를 생성하지 않아 unit이 될 수 없다)은 config/parity.json의 native_only 항목이며, 각각 이유와 contracts/interfaces.json의 anchor를 가진다. make parity-declarations-check 종료 코드 0. 여섯 id는 unresolved로 선언되지 않은 채 남는다: TypeScript connect wrapper, Python orm.models.models.connect, 그리고 entity별이 아닌 Python module 상수 MANIFEST_TEXT, MANIFEST_HASH, SCHEMA, model. 이 항목은 그 여섯 개가 결정되면 완료된다.—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-5step 4 (명세대로는 불가능: kit K227이 소비자 manifest용 config/parity.json 키를 추가하지 않고, contracts/interfaces.json의 rules(78개)와 contracts/rules.json의 rules(9개)는 id를 공유하지 않아 제거할 중복 목록이 없다): contracts/interfaces.json의 rule, record, owner를 config/parity.json으로 옮기고 이전 사본을 지워 interface-check가 목록 하나만 읽게 한다. 호환 계층을 만들지 않는다. 원인: contracts/interfaces.json에 아직 이름 규칙이 있다. 완료 기준: 두 번째 목록이 남지 않고, interface 단위 test가 통과한다. 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에 아직 이름 규칙이 있다. 완료 기준: 두 번째 목록이 남지 않고, interface 단위 test가 통과한다. 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
T77-9각 언어의 test는 자기 package에 있다. 원인: tests/codec/check.php, tests/conformance/runner*, tests/dbspec/{compare,apply,introspect}/<lang>, tests/dialects의 Go introspect.json 읽기, tests/interfaces/<lang>는 package 밖에 있는데, Rust conformance runner만 package 안에 있다. 완료 기준: 모든 언어 adapter가 자기 package로 옮겨 가고, tests/ 아래에는 공유 vector와 언어 간 comparator만 남는다. parity 검사는 package 밖의 기능 test를 거절한다. suite마다 구현별 binding이 하나 있고, 그 binding이 모든 case를 실행하고 case마다 보고한다(T77의 선언에서 발견). 지금은 Go와 Rust coverage test가 case마다 함수 하나이고, Go dbspec subtest는 canonical/<id>로 중첩되며, Go, PHP, TypeScript, Rust conformance binding은 vector 일부만 실행한다. Python conformance는 comparator가 구동하는 program(tests/conformance/runner_python.py)이고, PHP는 plans.json을 여러 file에서 나눠 읽는다. 증거(종료, 2026-10-18): 하위 항목 T77-9-1부터 T77-9-4(그리고 T77-9-4-1부터 -7)가 모두 완료됐다. config/parity.json의 어떤 구현 root도 tests/를 가리키지 않는다(packages 밖 root는 go의 examples와 benchmarks/rust뿐이며, 이것은 language adapter가 아니라 example과 benchmark program이다). main 98df3c23에서 make -s parity-declarations-check가 "every declaration holds"를 출력한다.—Evidence: T118은 이 adapter 경로를 하나도 다루지 않았다. T118-8(commit 592f2f74)은 harness test 셋만 tests/harness에서 scripts/harness로 옮겼고, R6 수정은 adapter 디렉터리를 root로 선언했다. 그래서 adapter가 지금 검사를 통과한다. 닫힌 부분: tests/codec/check.php는 더 이상 없고, commit 229dc8aa(#T148-1)가 그 case를 packages/php/tests/CodecVectorsTest.php로 옮겼다. Red: 이 tree에서 make -s parity-declarations-check는 exit 0이다("every declaration holds"). go, python, rust의 root와 comparator를 package로만 좁히면(config/parity.json의 임시 수정, 되돌림) exit 2이고 declaration finding 4개가 나온다. 잘못 놓인 support file마다 하나씩이다: tests/conformance/runner_python.py, tests/dbspec/compare/python.py, tests/interfaces/python.py, tests/interfaces/rust/src/main.rs. 분할: 남은 adapter는 아래 하위 항목 네 개이며, 언어 묶음마다 하나씩 자기 placement 검사와 함께 들어간다.[o]
T77-9-1Go adapter는 packages/go에 있다. 원인: tests/conformance/runner_go(4 files), tests/dbspec/{compare,apply,introspect}/go(5 files), tests/interfaces/go/main.go, tests/dialects의 dialect physical test(18 files, 그중 introspect.json을 읽는 Go test 포함)가 packages/go 밖에 있고, config/parity.json의 go root가 tests/를 포함한다. 완료 기준: program은 packages/go/cmd/<name>으로, test는 build tag와 함께 packages/go/internal/dialects로 옮긴다. Makefile, comparator, 문서의 Go 경로도 새 경로로 바꾼다. 공유 생성기 tests/dbspec/unicode와 명세 검증기 tests/interfaces/contracts는 tool program으로 tests/에 남고, config/parity.json의 go root와 support 목록은 packages/go만 지목한다. Red: go root를 packages/go로 좁힌 make -s parity-declarations-check가 이 file마다 실패한다. Green: 옮긴 뒤 통과한다. Closed: coordinator 판정으로 tests/dbspec/unicode와 tests/interfaces/contracts는 cross-language 도구이므로 tests/ 아래에 남는다. 두 경로는 config/parity.json의 go root에서 빠졌고, 빠진 뒤 make -s parity-declarations-check는 exit 0이다. Evidence(dialect physical test, 이 commit으로 land): tests/dialects(18 files, 그중 introspect.json을 읽는 Go test 포함)를 packages/go/internal/dialects로 옮겼다. 상대 경로는 이제 repoRoot(paths.go) 하나에서 풀린다. config/parity.json의 go root, go-internal 단위(go-internal-dialects-*로 이름 변경), dbspec binding과 언어 목록, layout exclusion, contracts/features.json, contracts/check-inputs.json, Makefile, harness 단위 test, comparator 주석, docs/dialects.md와 docs/spec/parity.md가 새 경로를 지목한다. CHANGELOG 기록은 그대로 둔다. Red: go root에서 tests/dialects를 뺀 채 make -s parity-declarations-check는 exit 2이며 R6 misplaced-test 11줄(dbspec-introspect와 go-internal-tests-dialects 단위)을 보고한다. Green: make -s parity-declarations-check는 exit 0이고, make -s layout-check는 dialects file을 들지 않으며, go vet -tags physical ./packages/go/internal/dialects는 exit 0, physical test binary는 compile된다(go test -tags physical -run '^$'), go test -run '^TestProbeIDs$'는 통과하고, node --test scripts/harness/go-test.test.mts는 4/4 통과하며, make documents-check는 통과한다. 여기서 돌리지 않은 것: physical test는 database server가 필요하다(TestDialectFacts는 DSN 메시지로 멈춘다). Evidence(Go symbol extractor, 이 commit으로 land): tests/interfaces/go/main.go를 packages/go/internal/symbols/main.go로 옮겼다. tests/interfaces/check/extract.mts, contracts/features.json의 Go 입력 네 개, interfaces README 두 개, config/layout.yml이 새 경로를 지목한다. Red: !tests/interfaces/go/** exclusion을 빼면 make -s layout-check가 exit 2이며 tests/interfaces/go/main.go(layout-infrastructure-typescript, S9)를 보고한다. Green: layout-check는 interfaces file을 하나도 들지 않고(exit 2는 main에도 있는 tests/collection 오류다), go vet ./packages/go/internal/symbols는 exit 0이며, go run ./packages/go/internal/symbols . packages/go/orm packages/go/model은 main의 go run ./tests/interfaces/go와 같은 byte를 출력한다. Evidence(dbspec Go program, 이 commit으로 land): tests/dbspec/{apply,compare,introspect}/go를 packages/go/internal/dbspec/{apply,compare,introspect}로 옮겼다. tests/dbspec/inputs.test.mts, tests/dbspec/compare/runners.mts, tests/dialects(go build와 go run 경로 두 곳), 사용법 문자열, config/layout.yml(exclusion 세 개 제거), contracts/check-inputs.json과 contracts/features.json의 schema-compare 입력이 새 경로를 지목한다. Red: exclusion 세 개를 빼면 make -s layout-check가 exit 2이며 오류 다섯 개를 보고한다, tests/dbspec/{apply,compare,introspect}/go/*.go, layout-infrastructure-typescript(S9). Green: layout-check는 dbspec file을 하나도 들지 않고(exit 2는 main에도 있는 tests/collection 오류다), make -s parity-declarations-check는 exit 0, go vet ./packages/go/internal/dbspec/...는 exit 0, gofmt -l은 깨끗하며, 각 program은 인자 없이 사용법을 출력하고 exit 2로 끝난다. compare program은 tests/dbspec vector와 contracts/fixtures/bench/bench.dbs에 대해 main의 go run ./tests/dbspec/compare/go와 같은 27604행을 출력한다(둘 다 exit 0). 여기서 돌리지 않은 것: apply와 introspect program은 database가 필요하고, tests/dbspec/inputs.test.mts는 make 환경(CARGO_TARGET_DIR)이 필요하며, owner-check는 test server가 필요하다(main에도 없다). Evidence(conformance runner, 이 commit으로 land): tests/conformance/runner_go를 packages/go/internal/conformance로 옮겼다. Makefile, contracts/features.json, config/parity.json(go root와 unit go-internal-conformance-runner-main), config/layout.yml, tests/conformance/check/main.mts, scripts/repo/check.test.mts, packages/go/model과 conformance README의 Go 경로를 새 경로로 바꿨다. Red: conformance comparator를 tests/conformance/check로 좁힌 placement 검사가 exit 2이며 R6 misplaced-test go-internal-tests-conformance-runner-go-main(tests/conformance/runner_go/main_test.go)을 보고한다. Green: 같은 좁힌 실행에는 Go finding이 없고(남은 R6 3개는 T77-9-2의 PHP와 TypeScript file), make -s parity-declarations-check는 exit 0이다. go vet -tags ormtest ./packages/go/internal/conformance는 exit 0이고, go test -tags ormtest ./packages/go/internal/conformance -run TestPickRejectsUnselectedField가 통과하며, go test -tags physical,ormtest,markdown,featurecoverage -run ^$ ./packages/go/model은 compile된다. 이 변경과 무관하게 main에서도 실패한다: make layout-check(tests/collection), make feature-unit-check/owners(추적 file 하나), scripts/repo/check.test.mts의 실패 3개.——[o]
T77-9-2PHP와 TypeScript adapter는 packages/php와 packages/ts에 있다. 네 항목으로 나누며, 항목마다 따로 land한다.—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-1PHP conformance runner는 packages/php에 있다. 원인: tests/conformance/runner.php와 tests/conformance/result_php_helpers.php가 packages/php 밖에 있다. 완료 기준: 두 file을 packages/php의 layout대로 옮기고, comparator와 Makefile이 새 경로를 가리키며, config/parity.json의 php root가 tests/conformance를 가리키지 않는다. Red: comparator를 tests/conformance/check로 좁힌 placement 검사가 exit 2이며 이 file들에 R6을 보고한다. Green: PHP finding이 없고 make -s parity-declarations-check가 exit 0이다.—Evidence: Red: conformance comparator를 tests/conformance/check로 좁힌 placement 검사가 exit 2이며 tests/conformance/runner.php와 tests/conformance/result_php_helpers.php에 R6을 보고한다. Green: 같은 좁힌 실행에 PHP finding이 없고(남은 것은 T77-9-2-2의 TypeScript R6뿐), make -s parity-declarations-check는 exit 0이다. 두 file의 php -l이 통과하고, vendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/ConformanceResultTest.php가 통과하며(test 1개, assertion 7개), --dsn 없이 실행한 runner는 자기 --dsn 검사에 도달하므로 require가 풀린다. 돌리지 않은 것: database conformance 실행과 CoverageConformanceVerificationTest는 database server가 필요하다.[o]
T77-9-2-2TypeScript conformance adapter는 packages/ts에 있다. 원인: tests/conformance/runner_typescript.mts, result_typescript.mts, result_typescript.test.mts가 packages/ts 밖에 있다. 완료 기준: packages/ts의 layout대로 옮기고, config/parity.json의 node root와 comparator가 새 경로를 지목한다. Red와 green: T77-9-2-1과 같다.—Evidence: Red: conformance comparator를 tests/conformance/check로 좁히면 make -s parity-declarations-check가 exit 2이며 tests/conformance/result_typescript.test.mts에 R6 misplaced-test를 보고한다(var/red-placement.log 23행). Green: 세 file은 packages/ts에 있다(runner_typescript.mts와 result_typescript.mts는 packages/ts/testing/conformance, result_typescript.test.mts는 packages/ts/tests). make -s parity-declarations-check는 exit 0이다(var/final-placement.log). 단위 test node --test packages/ts/tests/result_typescript.test.mts는 5개 중 5개 통과한다. 인자 없이 실행한 node packages/ts/testing/conformance/runner_typescript.mts는 자기 --dsn 검사에 도달한다(exit 2). make documents-check는 exit 0이다. config/parity.json에 선언한 ts-internal 단위 5개가 R5를 해소하며, docs/features.md와 docs/features.ko.md의 R10 문장은 Rust 생성기를 포함한다. 실행하지 않음: 데이터베이스 conformance 실행과 typescript-build 이외의 TypeScript build 검사.[o]
T77-9-2-3PHP와 TypeScript interface extractor는 자기 package에 있다. 원인: tests/interfaces/php.php, tests/interfaces/typescript.mts, tests/interfaces/typescript/가 packages/php와 packages/ts 밖에 있고, interface checker(tests/interfaces/check/extract.mts)가 이것으로 build한다. 완료 기준: 자기 package.json과 lock이 있으면 함께, package로 옮기고, checker, layout exclusion, check input이 새 경로를 지목한다. Red와 green: T77-9-2-1과 같다.—Evidence: Red: config/layout.yml에서 !tests/interfaces/php.php 제외를 지우면 make -s layout-check가 exit 2이며 tests/interfaces/php.php: error: layout-infrastructure-typescript: S9를 보고한다(기준선의 exit 2는 main도 보고하는 tests/collection 오류이다. tests/interfaces/typescript.mts는 TypeScript file이어서 finding이 없다). Green: PHP extractor는 packages/php/testing/interfaces/php.php이고, TypeScript extractor는 packages/ts/testing/interfaces/typescript.mts이며 그 project(package.json과 lock)는 packages/ts/testing/interfaces/typescript다. make -s layout-check는 interfaces file을 말하지 않는다(exit 2는 tests/collection 오류뿐이다). php -l이 통과한다. make install-php 뒤 node --test tests/interfaces/check/contract.test.mts가 7개 중 7개 통과한다. 두 extractor가 packages/ts/src와 packages/php/src에서 exit 0으로 실행된다. make -s parity-declarations-check는 exit 0이고, make documents-check는 exit 0이다. 검사기, layout, check input, 의존성 기록, toolchain과 release 기록이 새 경로를 지목한다. 실행하지 않음: 데이터베이스 서버가 필요한 interface 검사.[o]
T77-9-2-4PHP와 TypeScript dbspec adapter는 자기 package에 있다. 원인: tests/dbspec/{compare,apply,introspect}의 php, php-extension, typescript adapter로, comparator가 실행한다. 완료 기준: 각각을 packages/php, packages/php-ext, packages/ts의 layout대로 옮기고, comparator와 reader가 새 경로를 가리키며, config/parity.json의 php, php-extension, typescript root는 package만 지목한다. Red와 green: T77-9-2-1과 같다.—Evidence: Red: config/layout.yml에서 tests/dbspec/{apply,compare,introspect}의 php adapter 제외를 지우면 make -s layout-check가 exit 2이며 tests/dbspec/apply/php.php, tests/dbspec/compare/php.php, tests/dbspec/introspect/php.php에 각각 layout-infrastructure-typescript: S9를 보고한다(기준선의 exit 2는 main도 보고하는 tests/collection 오류이다. red 실행에서 php-extension과 TypeScript adapter는 되살리지 않았다). Green: PHP adapter는 packages/php/testing/dbspec, php-extension adapter는 packages/php-ext/testing/dbspec, TypeScript adapter는 packages/ts/testing/dbspec에 있다. make -s layout-check는 dbspec adapter를 말하지 않는다(exit 2는 tests/collection 오류뿐이다). 일곱 PHP file 모두 php -l이 통과하고, 세 TypeScript file은 node --check가 통과한다. 각 adapter를 인자 없이 실행하면 usage 메시지에 도달한다(PHP와 TypeScript, exit 2). node --test tests/dbspec/compare/check.test.mts가 6개 중 6개 통과한다. make -s parity-declarations-check는 exit 0이다. 공유 입력과 vector(tests/dbspec/input., cases., vectors.*)는 comparator가 읽는 곳에 남는다. 예외로 php-interface.php는 PHP compare adapter와 함께 옮겼다. 실행하지 않음: 데이터베이스 apply, introspection, 비교 실행과 데이터베이스 서버가 필요한 Go physical test.[o]
T77-9-3Python과 Rust adapter는 packages/python과 packages/rust에 있다. 원인: tests/conformance/runner_python.py, tests/conformance/result_python.py, tests/dbspec/compare/python.py, tests/interfaces/python.py, tests/interfaces/test_python_*.py, tests/interfaces/rust/. 완료 기준: 각 file은 자기 package로 옮기고(Rust conformance runner의 layout이 선례, packages/rust/tests/src/conformance.rs), config/parity.json의 python과 rust support 목록은 package만 가리키며, comparator는 새 경로를 쓴다. Red와 green: T77-9-1과 같은 placement 검사.—Evidence: Red: config/layout.yml에서 tests/conformance, tests/dbspec/compare, tests/interfaces의 Python adapter 제외 여섯 개를 지우면 make -s layout-check가 exit 2이며, 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, tests/interfaces/test_python_natives.py에 layout-infrastructure-typescript: S9를, tests/interfaces/python.py:1:1에 layout-infrastructure-shebang을 보고한다(기준선의 exit 2는 main도 보고하는 tests/collection 오류이다). Green: runner와 comparator adapter는 packages/python/testing(conformance와 dbspec/compare)에, extractor는 packages/python/testing/interfaces에, Python test는 packages/python/tests(conformance_result.py, test_python_extractor.py, test_python_natives.py)에, Rust interface crate는 packages/rust/testing/interfaces에 있다(자기 Cargo.lock을 가지며 cargo check --locked가 통과한다). make -s layout-check는 Python이나 Rust interface file을 말하지 않는다(exit 2는 tests/collection 오류뿐이다). make -s parity-declarations-check는 exit 0이다. python root는 packages/python만 가리키고, python과 rust support 목록은 packages만 지목한다. python3 -m unittest packages.python.tests.conformance_result가 통과한다. 통과하지 않지만 이 move의 원인이 아닌 것: test_python_extractor(실패 2)와 test_python_natives(오류 6)는 HEAD tree(HEAD의 archive)에서도 같게 실패한다. extractor 코드는 usage 줄을 빼고 바뀌지 않았다. 실행하지 않음: venv가 필요한 make 환경의 Python suite와 데이터베이스 실행.[o]
T77-9-4suite마다 구현별 binding이 하나 있고, 그 binding이 모든 case를 실행해 이름 붙여 보고한다. 원인: 지금은 Go와 Rust coverage test가 case마다 함수 하나이고, Go dbspec subtest는 canonical/<id>로 중첩되며, Go, PHP, TypeScript, Rust conformance binding은 vector 일부만 실행한다. Python conformance는 comparator가 구동하는 program(tests/conformance/runner_python.py, T77-9-3에서 옮김)이고, PHP는 plans.json을 여러 file에서 나눠 읽는다. 완료 기준: suite마다 profile의 구현당 binding이 정확히 하나 있고, 그 binding이 suite의 모든 case를 실행해 각각을 이름 붙여 보고하며, parity 검사가 이를 확인한다. Red: 현재 binding에 대한 parity 검사. Green: binding을 다시 쓴 뒤. Verified (2026-10-18): 모든 하위 항목이 완료다(T77-9-4-1은 f7453dda, 나머지는 이전 landing). 완료 기준은 cf01f139에서 충족되었으므로 T77-9-4-1과 T77-9-4-6의 evidence로 닫는다.——[o]
T77-9-4-1Go coverage test가 case 집합마다 binding 하나로 실행한다(T77-9-4에 이어; kit 규칙 "구현마다 binding 하나"에 의존, link-owner의 kit 항목). 원인: Go coverage test(packages/go/**/coverage_*_test.go)는 case마다 test 함수를 하나씩 두어, suite의 parity binding이 함수를 여럿 나열한다. 완료 기준: Go suite마다 binding이 하나 있고, 그 suite의 모든 case를 실행해 case마다 명칭으로 보고한다. Red: binding 검사가 kit 규칙을 가진 뒤에는 config/parity.json에서 같은 suite에 binding을 둘 이상 선언한 Go coverage file을 집계한다. 그때까지는 현재 file에서 suite별 Go binding 수를 세는 node 단위 test가 실패한다. Green: suite마다 binding이 하나이고 단위 test가 통과한다. 보류 원인: Go 파일 packages/go/model/coverage_conformance_verification_test.go는 conformance-mysql, conformance-postgres, conformance-sqlite 세 unit이 묶고, 그 unit들의 PHP와 Rust 파일도 같다. 그래서 red 검사가 실패한다. suite당 binding 하나는 다섯 client에 걸친 unit 하나를 요구하며, 모든 client의 parity unit을 바꾸는 구조 변경이다. 그래서 다음 계획과 함께 보류로 기록한다: (1) config/parity.json에 세 database를 덮는 suite unit conformance-verification을 선언하고, 각 client가 기존 database별 함수를 subcase로 묶는다. (2) 같은 commit에서 세 database별 unit과 그 선언을 지운다. (3) make feature-docs로 docs/features.md와 ko 쌍을 갱신한다. 재시도: owner가 이 unit 병합을 승인할 때. Verified (2026-10-18): cf01f139에서 완료 기준이 이미 충족되어 코드를 바꾸지 않았고 red도 없었다. Go suite는 case마다 한 함수의 t.Run subtest로 실행한다(TestCoverageAuthenticatedEncryption, TestCoveragePlanner, TestCoverageSchemaDefinition은 runFixtureCases, TestCoverageConformanceMySQL/Postgres/SQLite는 runConformanceVectors). 보류 원인(Go 파일 하나를 unit 세 개가 묶음)은 kit 위반이 아니다: R12는 unit마다 binding을 집계한다.—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-2Go dbspec subtest가 평평한 case 명칭으로 보고한다(T77-9-4에 이어; kit 규칙에 의존). 원인: Go dbspec subtest는 canonical/<id>로 중첩되어, case가 자기 이름이 아니라 그룹 아래에 보고된다. 완료 기준: case마다 id 하나의 명칭으로 보고하고, binding이 suite의 모든 case를 실행한다. Red: dbspec suite의 go test -run 보고 이름에 /가 들어 있으면 단위 test가 실패한다. Green: 이름이 평평하고 test가 통과한다.—Evidence: TestSubtestNamesAreFlat fails with 24 names containing "/" before the renames and passes after; TestVectorLoadersRejectMalformedVectors passes.[o]
T77-9-4-3PHP conformance binding이 모든 vector를 실행한다(T77-9-4에 이어; kit 규칙에 의존). 원인: PHP conformance binding(packages/php/tests/ConformanceResultTest.php)과 runner는 vector 일부만 실행한다. 완료 기준: binding이 tests/conformance/vectors*.json의 모든 vector를 실행하고 각각을 명칭으로 보고한다. Red: PHP binding이 보고하는 vector 수가 vector 총수보다 적으면 단위 test가 실패한다. Green: 두 수가 같다.—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-4TypeScript conformance binding이 모든 vector를 실행한다(T77-9-4에 이어; kit 규칙에 의존). 원인: TypeScript conformance binding(packages/ts/tests/result_typescript.test.mts와 conformance_case.mjs)은 vector 일부만 실행한다. 완료 기준: binding이 모든 vector를 실행하고 각각을 명칭으로 보고한다. Red: 보고된 vector 수가 총수보다 적으면 단위 test가 실패한다. Green: 두 수가 같다.—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-6Python conformance program이 test binding이 된다(T77-9-4에 이어; kit 규칙에 의존). 원인: packages/python/testing/conformance/runner_python.py는 comparator가 구동하는 program이지 test binding이 아니다. 완료 기준: packages/python/tests의 Python test binding이 모든 vector를 실행하고 각각을 명칭으로 보고하며, comparator는 같은 program을 계속 구동한다. Red: binding 검사가 이 suite의 Python conformance binding을 보고하지 않는다. Green: binding이 있고 검사가 그것을 보고한다. 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): cf01f139에서 완료 기준이 이미 충족되었다. Python binding conformance_mysql.py, conformance_postgres.py, conformance_sqlite.py가 변경되지 않은 runner_python.py를 통해 모든 vector를 명칭으로 실행하므로 코드를 바꾸지 않았고 red도 없었다.—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-5Rust conformance binding이 모든 vector를 실행한다(T77-9-4에 이어; kit 규칙에 의존). 원인: Rust conformance binding(packages/rust/tests/src/conformance.rs)은 vector 일부만 실행한다. 완료 기준: binding이 모든 vector를 실행하고 각각을 명칭으로 보고한다. Red: 보고된 vector 수가 총수보다 적으면 단위 test가 실패한다. Green: 두 수가 같다.—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-7PHP plan reader가 plans.json을 한 file에서 읽는다(T77-9-4에 이어). 원인: PHP binding이 tests/dbspec/plans.json과 그 부분들을 여러 file에서 읽는다. 완료 기준: reader 하나가 file을 읽고, 그 plan case마다 binding 하나가 있다. Red: PHP plan reader가 여는 file 수를 세는 단위 test가 1보다 많으면 실패한다. Green: 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언어 집합이 하나다. 원인: feature 검사, conformance, apply 비교, runner가 각자 집합을 정한다. 완료 기준: 모두 config/parity.json을 읽는다. 손으로 쓴 clients 표시와 check.mjs:170-180을 지우고, feature는 parity 단위를 참조하며, contracts/rules.json의 tests.language-parity 규칙은 parity 검사를 쓴다.—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-1feature check, coverage, build, decimal runner, conformance, apply reader가 언어 집합을 config/parity.json에서 읽는다(T77-10에 이어). 원인: scripts/features/build.mts:14와 check.mts:24(clients), scripts/features/coverage.mts:53(languages), scripts/decimal-physical-check.mts:7, tests/conformance/check/main.mts:54와 result.physical.test.mts:13(requiredLanguages), tests/interfaces/contracts/validate.go, tests/dialects/apply_clients_physical_test.go:32가 각각 언어 목록을 손으로 적는다. config/parity.json은 이미 그 목록을 client와 dbspec profile로 갖는다. 수용: 각 reader가 그 profile에서 목록을 읽고, 대응하는 config/parity.json의 languageLists 항목을 같은 commit에서 지우며, 손으로 적은 목록이 profile과 어긋나면 실패하는 node 단위 test가 feature-unit-check 대상에 있다(red 후 green). red: 현재 손으로 적은 목록에서 그 test가 실패한다. green: reader가 profile을 읽은 뒤 통과한다.—근거: commit 468e34b5, d770650c(main: reader가 config/parity.json의 client profile에서 언어 집합을 읽는다. scripts/docs/rules.mts가 profile에서 파생된 requiredLanguages를 받는다): scripts/features/languages.test.mts가 red 후 green이다(main의 reader에서 7개 중 6개 실패, 변경 후 7개 모두 통과). documents-check와 docs:rules-check가 exit 0이다. 소유 feature check 세 개는 main에서 exit 2, 2, 1이며 같은 실패를 보고한다(T198로 등록). 그래서 이 행의 회귀가 아니다.[o]
T77-10-2features의 clients flag와 check.mts:170-180을 없애고 tests.language-parity가 parity check를 쓴다(T77-10에 이어). 원인: contracts/features.json에 clients 객체가 19개 있고, scripts/features/coverage.mts(75, 79, 105줄)와 check.mts:170-180이 읽는다. 제거하려면 scripts/features/schema/features.schema.json 변경, 각 feature의 parity unit 대응, contracts/rules.json의 tests.language-parity를 feature-check에서 parity check로 바꿔야 한다. 수용: clients flag가 없고 규칙이 parity check를 쓰며 feature check가 통과한다. T77-10-1을 기다린다.—근거: scripts/features/coverage.test.mts 22/22, languages.test.mts 8/8, manifest.test.mts 3/3 node --test 통과; make documents-check 통과(문서 쌍 44, checklist 1, changelog 1).[o]
T77-11문서가 모든 구현을 적는다. 원인: 29곳과 그 twin이 아직 구현이 넷이라고 적는다. AGENTS.md:202-203,212의 규칙도 그렇고, docs/spec/codec.md:39는 하나를 빠뜨린다. 완료 기준: config/parity.json이 선언한 구현을 적는다. protocol.md:174-185(§4 표에 Python 행 packages/orm-python/src/polyspec/orm/engine/을 더하고 planner와 client가 다섯이라고 적는다)와 tests/conformance/README.md(runner 다섯)도 그 .ko.md twin과 함께 포함한다. parity 검사가 낡은 수를 거절하고 client 목록이 tests/conformance/check/main.go:101의 runner 목록과 같은지 확인하며, 먼저 지금의 text에서 red다.—Evidence: make docs-rules-check(scripts/docs/rules.mts, 문서 검사)는 영문과 한국어 문서에서 client 프로필과 다른 개수 단어를 거부하고, docs/protocol.md와 docs/protocol.ko.md의 4절 표에 client마다 행이 없으면 거부한다. tests/conformance/check/main.mts의 requiredLanguages(main.mts:54, main.go:101이 아님)가 client 프로필과 같은지 확인하며, 지금은 같다. 고치기 전 RED: 8건, docs/codec.md와 docs/interface-implementation.md의 "four outputs", CONTRIBUTING.md의 "four clients", tests/codec/README.md의 "four outputs", CONTRIBUTING.ko.md의 "네 클라이언트", docs/protocol.md와 docs/protocol.ko.md 4절에 Python 행 없음. GREEN: make documents-stamp가 6개를 다시 찍고, make docs-rules-check 통과(영문 42, 한국어 42), make documents-check 43쌍 통과, make docs-check 통과(docs-build, docs-site/pages, docs-site/links).[o]
T77-11-1문서의 표와 집합 나열이 선언된 모든 client를 적는다. 원인: 전체 집합을 나열하는 목록과 구현 표가 여전히 Python을 뺀다. interfaces.md의 논리 타입과 작업 표, schema.md의 작업 표, perf.md의 표, docs/index.md:17 같은 나열이 그렇다. 완료 기준: make docs-rules-check는 client 언어 둘 이상을 적은 표 머리글에 선언된 client마다 열이 없으면 거부하고, client 이름 둘 이상을 집합으로 쓰는 문장("클라이언트", "구현", "언어", "러너", "플래너")이 모두 적지 않으면 거부한다. 데이터가 없는 client는 그렇게 적는다("측정하지 않음", "없음"). 검사가 적발한 모든 곳을 영문과 .ko.md 쌍에서 고친다.—증거: 고치기 전 text에서 RED 30건(표 머리글 11, 나열 19), commit 메시지에 있다. GREEN: make docs-rules-check 통과(영문 42, 한국어 42), make documents-check 43쌍 통과, make docs-check 통과(docs-build, docs-site/pages, docs-site/links).[o]
T77-11-2T77-11의 규칙 코드가 make lint-ts를 통과한다. 원인: T77-11의 규칙(scripts/docs/rules.mts, 커밋 5e6520a5와 0a1c643c)은 문서 검사만 확인했고, make lint-ts는 거기서 실패한다. 137행은 config/parity.json을 JSON.parse(...) as Parity로 읽는데, any에서 나온 안전하지 않은 as 타입 지정(typescript/no-unsafe-type-assertion)이다. 164행은 줄을 캐럿 정규식 /^|/로 검사하는데, unicorn/prefer-string-starts-ends-with는 이를 startsWith로 쓰라고 한다. 완료 기준: 파일을 모양 검사 guard(isParity, isProfile, isStringArray)로 읽어 다른 모양의 파일에 finding을 기록하고, 표 행은 startsWith('|')로 거르며, make lint-ts가 규칙을 끄거나 inline 억제를 쓰거나 lint 설정을 바꾸지 않고 통과한다.—근거: RED, @polyspec/orm, @polyspec/dbspec-language, @polyspec/dbspec-lsp를 make build한 뒤(lint-ts는 이들의 dist를 읽는다) make lint-ts가 2개 문제를 보고한다. scripts/docs/rules.mts:164:51(prefer-string-starts-ends-with)과 scripts/docs/rules.mts:137:16(no-unsafe-type-assertion)이며 oxlint가 1로 끝났다. GREEN, make lint-ts가 0으로 끝난다(oxlint passed, knip passed). make docs-rules-check가 0으로 끝난다(영문 문서 42개, 한국어 번역 42개 passed). make docs-check가 0으로 끝난다(docs-build, docs-site/pages, docs-site/links passed).[o]
T77-12모든 push가 parity 검사를 실행한다. 완료 기준: parity-check와 parity-declarations-check가 CHECK_TARGETS, CI group, check input에 들어가고, 남은 불일치 없이 0으로 끝난다. T77부터 T77-11, T73, T74와 같은 push 안에 있다. parity-check는 test server와 함께 실행한다. 로컬에서는 make run-databases TARGETS=parity-check로, CI에서는 database를 준비하는 group에서 실행한다. database 단위는 자기가 적은 모든 database에서 모든 case를 실행하며, SQLite case만 실행하지 않는다. 각 binding은 config/parity.json의 ${NAME} 참조(kit K39)로 database DSN을 받으므로, server 없는 실행은 빠진 변수를 적은 선언 finding이 된다. database test의 Go build tag는 Go 구현의 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.—증거: CHECK_TARGETS(Makefile:164)에 parity-check와 parity-declarations-check가 있고, static CI 그룹(182행)에 parity-declarations-check, conformance CI 그룹(185행)에 parity-check가 있으며, contracts/check-inputs.json에 둘 다 있다. make parity-declarations-check: every declaration holds (scratchpad/t148-decl4.log).[o]
T199query, collection 및 나머지 runtime class를 포함한 전체 ORM 표면을 native C로 구현한다.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모든 client의 test entry point가 다음 statement를 주어진 ORM 오류로 실패하게 한다. 원인: consumer의 request-log case는 PHP client의 insert가 DRIVER로 실패해야 하지만, Polyspec\Orm\Db는 final이고 \PDO를 받으므로 test는 \PDO를 subclass해야만 실패를 주입할 수 있다. driver 이름을 source에서 금지하는 소비자(consumer sqlban)는 이를 할 수 없다. 이 항목의 분류는 모든 client가 가지는 testing fault seam이며, 한 소비자의 기능이 아니라 여러 backend에 걸친 표준이다. 그래서 연결을 가진 다섯 client에서 protocol §3.1의 rollback fault를 따른다. 완료 기준: protocol §3.2와 usage가 FailNextStatement(Go, build tag ormtest), fail_next_statement(Rust, feature test-faults), failNextStatement(TypeScript testing subpath), Faults::failNextStatement(PHP), fail_next_statement(Python testing package)를 정의하고, 각각 code와 message를 받는다. 빈 code나 message는 client의 usage 오류로 거절되며 fault를 설정하지 않는다. 연결의 다음 statement는 보내지 않고, driver 오류가 가는 statement 종료 지점을 거쳐 그 code와 message의 OrmException으로 보고된다. fault는 그 statement가 소비하며, 데이터베이스와 다음 statement는 영향을 받지 않는다. 각 client는 contracts/fixtures/statement_fault.json의 모든 case를 SQLite, MySQL, PostgreSQL에서 실행하고, fixture가 적은 case가 실행되지 않으면 실패한다. PHP 확장을 포함한 모든 구현의 dbspec apply와 introspection은 Db 밖의 driver 연결 위에서 실행하며 fault를 소비하지 않는다(protocol 3.2).—증거: RED commit 77bfe13f이 contracts/fixtures/statement_fault.json의 네 case를 각 client에서 SQLite, MySQL, PostgreSQL로 실행하고, 모든 client가 함수 없음으로 실패한다(Go undefined: orm.FailNextStatement, Rust E0425, TypeScript export 없음, PHP undefined method, Python AttributeError). 이 commit의 tree에서 GREEN: Go go test -tags ormtest -run '^Test(RollbackFault|StatementFault)'가 세 database의 모든 StatementFault subtest를 통과하고 태그 없는 package 실행도 통과한다. Rust --test statement_fault 13/13과 --test rollback 10/10, TypeScript statement_fault.mjs 13/13과 rollback.mjs 9/9, Python test_statement_fault.py 7/7과 test_rollback_fault.py 6/6, PHP statement_fault_test.php 12 PASS와 rollback_test.php 11 PASS다. PHP 확장을 포함한 모든 구현의 dbspec apply와 introspection은 Db 밖의 driver 연결 위에서 실행하며 fault를 소비하지 않는다(protocol 3.2). test server 아래의 동적 parity-check 실행은 다른 단위에서 실행되지 않음(R3) finding 154건을 보이며, 이는 T77이 맡는다.[o]
T78-1client test lane의 검사 case와 Python driver error test가 T78의 statement fault test와 함께 통과한다. 원인: T78은 scripts/client-db-test.sh의 PHP lane과 scripts/typescript/sqlite-test.sh에 statement fault test를 하나씩 더했는데, scripts/check/continue.test.mjs의 두 case는 실행된 명령을 손으로 적은 개수(11과 10)와 비교했으므로 lane의 올바른 변경에 test가 실패했다. T78은 Python client의 TxFrame.run에 SQLite row lock이 넘기는 keyword faultable도 더했는데, packages/orm-python/tests/test_driver_errors.py의 test_a_busy_nowait_row_lock_is_lock_not_available의 frame double은 그것을 받지 않았다. 완료 기준: 두 case는 기대하는 명령을 lane 함수 php_lane와 scripts/typescript/sqlite-test.sh에서 읽고 실행된 순서 전체를 비교하므로, lane의 test file을 더하거나 빼도 case를 바꿀 필요가 없다. frame double은 TxFrame.run의 인자를 받고, row lock statement가 statement fault를 소비하면 실패한다(protocol 3.2).—증거: Red: node --test scripts/check/continue.test.mjs가 1로 끝나며 9개 case 가운데 2개가 12 !== 11(PHP lane)과 11 !== 10(TypeScript SQLite test)으로 실패한다. test_driver_errors.py의 Python unit test 실행(python -m unittest discover -s packages/orm-python/tests -p test_driver_errors.py)은 test 12개 가운데 오류 1개, TypeError: ... Frame.run() got an unexpected keyword argument 'faultable'로 실패한다. Green: 같은 명령이 9개 가운데 9개, 12개 가운데 12개를 통과한다.[o]
T78-2make python-unit-check가 database가 필요한 statement fault test를 실행하지 않는다. 원인: python-unit-check는 unittest discover -p 'test_*.py'를 실행하고, 이것이 packages/orm-python/tests/test_statement_fault.py도 찾는다. 이 파일의 네 case는 ORM_TEST_MYSQL_DSN 등 database 변수가 필요하지만 python-check의 needs에는 database가 없으므로, python-check는 check runner 아래에서 "ORM_TEST_MYSQL_DSN is required"로 실패한다. 완료 기준: 파일 이름이 dbspec_introspect.py처럼 test_ 접두어 없는 statement_fault.py여서 discover pattern에 맞지 않는다. python-statement-fault-check, config/parity.json, contracts/features.json이 새 경로를 쓴다. python-check의 needs에 database를 더하지 않는다.—증거: Red: ls packages/orm-python/tests/test_*.py(discover pattern)가 test_rollback_fault.py 외에 test_statement_fault.py를 나열한다. Green: 같은 목록에 이 파일이 없고, python-statement-fault-check가 packages/orm-python/tests/statement_fault.py를 실행한다.[o]
T79contains가 MySQL에서도 PostgreSQL, SQLite처럼 ASCII 대소문자만 구별하지 않는다. 원인: protocol 결함 수정. protocol상 contains는 대소문자를 구별하지 않는다(engine/dialect/dialect.go:13, docs/spec/protocol.md:97). 그러나 MySQL LIKE는 column collation utf8mb4_0900_bin에서 code point를 비교하므로 대소문자를 구별하고, PostgreSQL C의 ILIKE와 SQLite LIKE는 ASCII 글자만 접으므로 docs/spec/dialects.md:12("collation-driven case-insensitivity")는 사실이 아니다. LOWER와 모든 _ci collation은 ASCII 밖까지 접고, REGEXP_LIKE는 숫자 escape를 back-reference로 해석하며 빈 pattern을 거절하고 regexp_time_limit를 넘으면 멈추므로 어느 것도 해결책이 아니다. 완료 기준: 먼저 red: tests/dialects의 probe mysql.like.binary_collation_case_sensitive, mysql.like.ascii_fold_replace, postgres.ilike.c_collation_ascii_only, sqlite.like.ascii_only가 사실을 기록한다. planner.json case가 MySQL SQL을 고정한다. conformance vector contains_ascii_fold(행 Apple, apple, Évian, évian, x_y, pattern PPL, ÉV, _, "")는 지금 tests/conformance/vectors.json과 PostgreSQL, SQLite file 사이에서 다르다(PPL이 Apple을 놓친다). 그다음 Go, PHP, TypeScript, Python, Rust의 MySQL dialect가 F(col) LIKE F(?)를 render한다. F는 A–Z 각각을 소문자로 바꾸는 REPLACE 26개의 중첩이며, like_contains transform은 그대로다. conformance 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 planner.json과 conformance vector를 실행한다. 문서(EN과 .ko.md): dialects.md:12의 MySQL 칸과 그 이유, "세 database를 같게 유지하는 규칙"에 "contains는 모든 database에서 ASCII 글자는 대소문자 구별 없이, 그 밖의 문자는 code point로 비교해 부분 문자열을 찾는다", protocol.md:97(contains는 ASCII 글자를 대소문자 구별 없이, 그 밖의 문자를 code point로 비교하고, contains_binary는 code point로 비교한다).—Evidence: RED: conformance vector contains_ascii_fold가 MySQL에서 [[],[],[5],[1..5]], PostgreSQL과 SQLite에서 [[1,2],[3],[5],[1..5]]였고, planner_contains_ascii_fold가 Go, PHP, TypeScript, Python, Rust에서 실패했다. 다섯 client의 MySQL dialect가 이제 F(col) LIKE F(?)를 쓰며 F는 A-Z만 접는다. probe mysql.like.binary_collation_case_sensitive, mysql.like.ascii_fold_replace, postgres.ilike.c_collation_ascii_only, sqlite.like.ascii_only가 통과하고 vector는 세 vector file에서 같다. PHP extension은 query 계층이 없어 planner와 conformance case를 실행하지 않는다.[o]
T79-1TypeScript runner의 contains 호출을 scan이 보게 한다 (T79에 이어). 원인: T79 runner가 callback parameter에서 andLkName과 andLbName을 불렀는데 orm-gen scan은 그것을 CompositeAccount에 연결하지 않는다. 그래서 생성된 type에 두 method가 없고 main에서 make lint-ts가 실패했다(runner_typescript.mts:388-389의 no-unsafe-call, no-unsafe-return). 완료 조건: 각 호출이 new CompositeAccount()에서 시작하는 chain이고 lint-ts가 통과한다.—Evidence: lint-ts가 finding 넷을 보고했다. chain을 풀어 쓰자 생성된 model이 두 method를 선언하고 lint-ts가 통과한다.[o]
T79-1-1T79-1이 더한 TypeScript symbol을 기록한다 (T79-1에 이어). 원인: contains 호출을 chain으로 쓰자 orm-gen이 CompositeAccount의 andLkName과 andLbName을 선언하는데, T79-1이 symbol도 TypeScript hash도 기록하지 않아 main에서 make interface-check가 실패했다("unexpected … CompositeAccount#field.andLbName"). 완료 조건: 두 symbol과 checker가 계산한 hash를 기록하고 interface-check가 통과한다.—Evidence: interface-check가 예상하지 않은 symbol 둘을 보고했다. contracts/symbols/typescript.json에 둘을 더하고 contracts/interfaces.json에 계산된 hash sha256:9cbbf7a0…를 넣자 interface-check가 통과한다.[o]
T80문자열이 아닌 contains 값은 CODEC_ENCODE이고, NULL은 NULL로 bind된다. 원인: protocol 결함 수정. errors.yaml은 CONFIG를 없는 secret, 잘못된 DSN, 오용으로, CODEC_ENCODE를 slot에 맞게 encode할 수 없는 값으로 정의한다. like_contains나 contains_binary slot의 문자열 아닌 값은 뒤의 경우인데 client는 CONFIG로 거절한다(packages/orm-go/orm/exec.go:257, packages/orm-npm/src/database.ts:834, packages/orm-python/src/polyspec/orm/database.py:770, 같은 transform인 Db.php:1409와 value.rs:364). 또 세 database 모두에서 col LIKE NULL은 unknown인데도 NULL을 거절한다. 완료 기준: 먼저 red: T89의 공유 bind vector tests/bind/vectors.json case(5 → CODEC_ENCODE, null → NULL, "a%" → %a\%%)가 지금은 CONFIG이고, contains null이 어떤 행과도 맞지 않는 conformance vector를 더한다. 그다음 다섯 client가 NULL 값을 NULL로 bind하고 그 밖의 문자열 아닌 값을 CODEC_ENCODE "%s requires a string value"로 거절한다. conformance 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): protocol.md §2 bind_slots(transform). T89가 필요하다.—Evidence: 문자열이 아닌 contains 값이 다섯 client에서 CODEC_ENCODE("%s requires a string value")로 실패한다. 전에는 CONFIG였고 PHP는 text로 바꿨다. NULL은 NULL로 bind되며, 다섯 client의 condition builder가 CONFIG로 거부하던 것을 고쳤다. contains bind case와 conformance vector contains_null(count 0)이 세 vector file에서 같다. RED: 세 database의 다섯 client에서 CONFIG였다. PHP extension의 query vector는 T182 작업이다.[o]
T81MySQL contains_binary는 LIKE BINARY가 아니라 binary collation의 LIKE로 render된다. 원인: protocol 결함 수정. dialects.md:13은 Lb를 LIKE BINARY로 render하는데, MySQL은 8.0.27부터 이 연산자를 deprecated로 둔다(warning 1287). 그래서 dialect가 database가 없앨 문법에 기댄다. orm의 문자 column은 모두 이미 utf8mb4_0900_bin(dbspec.md:93)이고, 그 collation에서 보통 LIKE는 code point를 비교한다. 완료 기준: 먼저 red: MySQL에서 col LIKE ?를 기대하는 planner.json case가 LIKE BINARY ? 때문에 실패하고, probe mysql.like.binary_collation_code_point가 LIKE BINARY와 같은 행을 기록한다(%pp% → 1, 2, %É% → 3). 그다음 Go, PHP(Dialect.php:110), TypeScript(dialect.ts:128), Python(dialect.py:169), Rust(dialect.rs:155)의 MySQL dialect가 col LIKE ?를 render한다. conformance vector contains_binary_code_point는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 planner.json과 conformance vector를 실행한다. 문서(EN과 .ko.md): dialects.md:13의 MySQL 칸 "LIKE(binary collation utf8mb4_0900_bin)".—Evidence: RED: planner_contains_binary_collation이 다섯 client에서 LIKE BINARY ?로 실패했다. probe mysql.like.binary_collation_code_point가 LIKE와 LIKE BINARY의 행이 같음을 보인다. MySQL contains_binary가 이제 다섯 client에서 col LIKE ?를 쓰고, contains_binary_code_point는 세 vector file에서 [[1,2],[3],[],[5]]다. 순서 없는 read가 run마다 PostgreSQL bind 순서를 바꿔, 두 contains vector가 이제 key 순서로 지운다.[o]
T82모든 database에서 NULL은 오름차순에서 처음, 내림차순에서 마지막에 정렬된다. 원인: protocol 결함 수정. 같은 order 요청이 다른 행 순서를 돌려준다. MySQL과 SQLite는 오름차순에서 NULL을 처음에, PostgreSQL은 마지막에 둔다. planner는 NULLS 절을 render하지 않고(engine/planner/planner.go:443-475), dialects.md:141은 index key 순서만 다룬다. 완료 기준: 먼저 red: probe mysql.order.null_first_asc, postgres.order.null_last_asc, sqlite.order.null_first_asc, 접미사가 있고 없는 planner.json PostgreSQL case, conformance vector order_nulls(오름차순, 내림차순, limit_per_parent). 이 vector는 지금 PostgreSQL file에서 다르다. 그다음 NULL은 모든 값보다 작다. 다섯 planner의 PostgreSQL dialect는 NULL일 수 있는 order 식(nullable column이나 그 column 함수, NOT NULL column은 btree 순서를 유지한다)에 ASC면 NULLS FIRST, DESC면 NULLS LAST를 덧붙인다. root order, limit_per_parent의 ROW_NUMBER() OVER (… ORDER BY …), group_count order가 대상이며, MySQL과 SQLite는 아무것도 render하지 않는다. conformance 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): dialects.md 규칙 "order 항목은 오름차순에서 NULL을 모든 값 앞에, 내림차순에서 모든 값 뒤에 정렬한다. MySQL과 SQLite는 본래 그렇게 하고, PostgreSQL은 NULL일 수 있는 식에 ASC NULLS FIRST와 DESC NULLS LAST를 render한다", 그리고 protocol.md §1.4 Order의 같은 규칙. 상태: 다섯 planner의 PostgreSQL dialect는 NULL일 수 있는 order 식(nullable 컬럼이나 그 컬럼 함수)의 ASC에 NULLS FIRST를, DESC에 NULLS LAST를 root order, limit_per_parent의 ROW_NUMBER() OVER (… ORDER BY …), group_count order에서 붙이고, MySQL과 SQLite는 아무것도 렌더하지 않는다(dialect method NullsOrder/nullsOrder/nulls_order 추가). planner.json case 다섯 개(nullable 컬럼, NOT NULL 컬럼, 컬럼 함수, group_count, limit_per_parent)가 Go, PHP, TypeScript, Rust에서 실행되고 Python은 모든 case를 실행한다. 하지 않은 것: probe mysql.order.null_first_asc, postgres.order.null_last_asc, sqlite.order.null_first_asc와 세 database의 conformance vector order_nulls는 T82-1로 나눈다. 문서: protocol.md §1.4, dialects.md.——[o]
T82-1NULL 정렬 probe와 conformance vector order_nulls(T82에서 분리). 원인: T82가 PostgreSQL이 NULLS 절을 렌더하게 했고, 세 database가 이제 일치한다는 probe나 vector가 없다. 인수 조건: probe mysql.order.null_first_asc, postgres.order.null_last_asc, sqlite.order.null_first_asc를 기록하고, conformance vector order_nulls(오름차순, 내림차순, limit_per_parent)가 세 vector file에서 같은 결과를 내며 여섯 구현(Go, PHP, PHP extension, TypeScript, Python, Rust)이 각자 package의 test에서 돌린다.—Evidence: probe mysql.order.null_first_asc, postgres.order.null_last_asc, sqlite.order.null_first_asc가 통과하고 dialects.md가 그것을 인용한다. bench seed가 service 100의 author에게 세 번째마다를 빼고 price를 주며, conformance vector order_nulls(오름차순, 내림차순, groupLimit)는 세 vector file에서 같다. NULL은 오름차순에서 처음, 내림차순에서 마지막이고 PostgreSQL에는 NULLS FIRST와 NULLS LAST가 붙는다. RED: PHP, Rust, TypeScript, Python runner가 세 database에서 unknown vector order_nulls를 보고했다. PHP extension의 query vector는 T182 작업이다.[o]
T83group_by는 count와 group_count의 root에서만 유효하고, group order는 group column을 가리켜야 한다. 원인: protocol 결함 수정. planner가 두 database가 거절하는 SQL을 만들고 입력 하나를 버린다. engine/engine_test.go:239는 primary key를 select하는 GROUP BY 붙인 all을 기대하며(MySQL 1055, PostgreSQL 42803, SQLite에서는 임의의 행), planner.go:294-296은 sum과 avg의 group_by를 무시하고, group column 아닌 column으로 group을 정렬하면 1055, 42803 또는 임의의 결과다. 완료 기준: 먼저 red: planner.json 거절 case(all, sum, join query, subquery, group column 아닌 column의 order)가 지금은 검증을 통과한다. 허용 case는 count, 그리고 group column, 그 column 함수, random으로 정렬한 group_count다. 그다음 다섯 validator(ir.go, Validator.php, validate.ts, validate.py, validate.rs)가 one, all, paginate, sum, avg root와 join, relation, subquery query의 group_by를 IR_INVALID로, group_by 아래에서 group column, 그 column 함수, random이 아닌 order 항목을 IR_INVALID "order by %s, which is not a group column"으로 거절한다. 도달할 수 없게 된 planner의 sum/avg와 subquery GROUP BY 경로는 지운다. engine_test.go:239는 IR_INVALID를 기대한다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 planner.json case를 실행한다. 문서(EN과 .ko.md): protocol.md §1 group_by(count와 group_count의 root에서 유효하고, 그 밖에서는 IR_INVALID)와 §1.4(group_by가 있으면 order 항목은 group column, 그 함수, random 중 하나다), dsl.md:255(groupBy<Col>()는 getsCount()와 group count에 grouping을 설정하고, 다른 읽기에서는 IR_INVALID다). 상태: 다섯 validator는 count와 group_count의 root 밖의 group_by와 group column, 그 함수, random이 아닌 order 항목에 IR_INVALID로 거절한다. 도달할 수 없게 된 planner 경로(행, sum/avg, subquery의 GROUP BY)는 지웠고, engine_test.go는 GROUP BY를 가진 all을 더는 기대하지 않는다. planner.json case 아홉 개(받아들이는 것 넷, 거절하는 것 다섯)가 Go, PHP, TypeScript, Rust에서 실행되고 Python은 파일의 모든 case를 돌며, tests/ir/validate.json이 validator case를 갖는다. PHP extension은 dbspec만 parse하고 validator도 planner도 없으므로 규칙을 싣지 않는다. 문서: protocol.md §1과 §1.4, dsl.md.——[o]
T84scalar column subquery는 그 형태만으로 행을 최대 하나 돌려준다. 원인: protocol 결함 수정. 여러 행과 맞는 agg 없는 scalar Sub(engine/ir/ir.go:826-830)에는 단일 결과가 없다. MySQL은 1242, PostgreSQL은 21000이고 SQLite는 첫 행을 돌려준다. 그 차이를 기록하는 것은 규칙이 아니므로, 요청 형태가 한 행을 보장해야 한다. 완료 기준: 먼저 red: planner.json case(limit 없는 scalar column subquery는 지금 검증을 통과하고 limit 있는 것은 거절된다)와, 맞는 행이 둘인 conformance vector scalar_column_subquery. 그다음 다섯 validator가 scalar column subquery(agg가 ""인 columns.sub)에 count 1인 limit(offset은 무관)와, 그 entity의 primary key column을 모두 포함하고 random 항목이 없는 order를 요구하며, 아니면 IR_INVALID다. ir.go:852-853의 거절은 이 형태에만 열리고, in subquery(MySQL 1235)와 집계 subquery의 limit은 여전히 IR_INVALID이며, group_by가 있는 subquery는 IR_INVALID다(T83). 다섯 planner의 subSelect(planner.go:1533)가 이 형태에 T82의 NULL 순서로 ORDER BY … LIMIT …를 render한다. conformance 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): protocol.md §1.3 Sub("scalar column subquery는 count 1인 limit과 모든 primary key column을 포함하는 order를 가지므로 모든 database에서 행을 최대 하나 돌려준다. 맞는 행이 없으면 NULL이다"). T82와 T83이 필요하다. 상태: 다섯 validator는 스칼라 컬럼 subquery(agg 없는 columns.sub)에 count가 1인 limit(offset은 무엇이든)과 모든 primary key 컬럼을 포함하고 random 항목이 없는 order를 요구하고 아니면 IR_INVALID로 거절한다. limit은 in subquery와 집계 subquery에서 계속 IR_INVALID이고, group_by가 있는 subquery도 IR_INVALID이다(T83). 다섯 planner의 subSelect는 그에 T82의 NULL 정렬을 더한 ORDER BY … LIMIT …을 렌더한다. validator vector 일곱 개와 planner.json case 넷(세 dialect에서 받아들이는 것 하나, 거절하는 것 셋)이 Go, PHP, TypeScript, Rust에서 실행되고 Python은 두 파일의 모든 case를 실행한다. 하지 않은 것: 일치하는 행이 둘인 세 database의 conformance vector scalar_column_subquery는 T84-1로 나눈다. 문서: protocol.md §1.3, dsl.md.——[o]
T84-1conformance vector scalar_column_subquery(T84에서 분리). 원인: T84가 스칼라 컬럼 subquery가 모양만으로 많아야 한 행을 내게 했고, 세 database가 같은 행을 돌려준다는 vector가 없다. 인수 조건: 일치하는 행이 둘인 conformance vector scalar_column_subquery가 세 vector file에서 같은 결과를 내고 여섯 구현(Go, PHP, PHP extension, TypeScript, Python, Rust)이 각자 package의 test에서 돌린다.—Evidence: conformance vector scalar_column_subquery가 맞는 author 둘 중 하나의 name을 price, seq 순서와 limit(0, 1)로 읽는다. 오름차순은 author-99, 내림차순은 author-5099이고 세 vector file에서 같다. RED: 다섯 runner가 세 database에서 unknown vector scalar_column_subquery를 보고했다. PHP extension의 query vector는 T182 작업이다.[o]
T85flatten은 client model의 병합이고, parent의 값이 우선한다. 원인: protocol 결함 수정. protocol.md:73은 flatten이 child column을 parent 행에 병합한다고, usage.md:247은 parent 값이 우선한다고 적고, plan.go:104는 병합을 array와 JSON 형태에 둔다. 모든 model 계층은 child의 array 형태 전체를 병합하는데(model.go:1329-1370, model.ts:782, Model.php:2020, model.py:187, model.rs:455), 이름 충돌, 없는 child, flatten된 손자를 고정하는 vector가 없다. 완료 기준: 먼저 red: conformance vector relations_flatten(parent와 child가 id와 name을 공유, 없는 child, flatten된 손자)을 어떤 변경보다 먼저 쓰고, 다르게 동작하는 model 계층은 이 vector에서 실패한다. 그다음 array 형태가 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)에서 같고, 각 구현은 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): protocol.md §1.2 flatten: "flatten은 one child를 parent의 array와 JSON 형태에 병합한다. parent 자신의 member 뒤에 child array 형태의 member, 곧 그 output, 그 relation, 같은 방식으로 병합된 flatten 손자가 온다. parent가 가진 이름은 parent의 값을 유지하고, 없는 child는 아무것도 더하지 않는다. plan이 조립하는 행과 IR executor의 결과 행은 child를 rel 아래에 두며, 병합은 client model이 한다." usage.md:247은 이 문단으로 link한다.—Evidence: conformance vector relations_flatten이 parent와 column 이름이 겹치는 service region을 평탄화하고 그 안에 평탄화한 service와 member를 담으며, 닫힌 author만 그것을 받는다. RED: Go만 MySQL과 SQLite에서 닫힌 author 406에 service_region_model null을 주었다. bool if_parent를 0/1 cell 그대로 비교했기 때문이다. ifParentMatch가 이제 bool parameter이면 cell을 bool로 읽고, 다섯 client가 세 database에서 일치하며, protocol.md 1.2가 flatten 규칙과 if_parent가 column의 논리 type으로 비교함을 적는다. PHP extension의 query vector는 T182 작업이다.[o]
T85-1T85 뒤의 Go model을 다시 생성한다 (T85에 이어). 원인: Go runner의 relations_flatten vector가 service region의 service relation을 읽어, orm-gen이 packages/go/model/service_region.go에 GetServiceModel을 더한다. 그런데 T85가 다시 생성한 model을 commit하지 않아 main에서 go-model-check와 model_generation coverage가 실패했다(run 38698025914). 완료 조건: commit한 model이 go generate 결과와 같고 go-model-check가 통과한다.—Evidence: go generate가 service_region.go에 GetServiceModel을 더하며, 이것이 CI가 보고한 유일한 차이다. 이를 commit하고 그 symbol과 Go hash를 기록하자 go-model-check, interface-check, go-vet-check가 통과한다.[o]
T86key_by는 non-null column이어야 한다. 원인: protocol 결함 수정. NULL collection key의 결과가 언어마다 다르고(Go는 CODEC_DECODE, PHP는 빈 key와 겹치는 '', T77-6), nullable key_by column이면 결과가 data에 따라 달라진다. 완료 기준: 먼저 red: nullable key_by column인 planner.json 거절 case가 지금은 검증을 통과한다. 그다음 다섯 validator(ir.go:547-552와 그 대응)가 IR_INVALID "key_by %s is nullable"로 거절한다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 case를 실행하며, PHP 확장 실행이 nullability 사실을 증명한다. 문서(EN과 .ko.md): protocol.md §1.2("key_by는 child의 non-null column이다. nullable column은 IR_INVALID다"). 상태: 다섯 validator는 nullable key_by 컬럼에 IR_INVALID "key_by %s is nullable"로 거절한다. validator vector(key-by-non-null-column, key-by-nullable-column)와 planner.json case planner_rejects_nullable_key_by가 Go, PHP, TypeScript, Rust에서 실행되고 Python은 두 파일의 모든 case를 돌며, PHP extension은 dbspec만 parse하고 validator가 없다. 문서: protocol.md §1.2.——[o]
T87on_duplicate의 conflict key는 정확히 하나다. 원인: protocol 결함 수정. upsert의 결과가 database마다 다르고, dialects.md:17과 :398은 규칙 대신 그 차이를 기록한다. MySQL은 primary key, NOT NULL unique key, nullable unique key 순서로 어떤 unique key에서 만난 행이든 update하는 반면, PostgreSQL과 SQLite는 target 하나를 지정하고(conflictTarget, planner.go:1226-1245: 완전히 덮인 첫 unique key, 없으면 primary key) 다른 key에서 만나면 unique 위반이다. 그래서 어떤 key 순서도 둘을 함께 재현하지 못한다. 완료 기준: 먼저 red: probe mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts, sqlite.upsert.other_key_conflicts, 지금은 plan되는 planner.json 거절 case(primary key와 unique key를 모두 할당, unique key를 일부만 할당한 identity table, unique key가 둘인 table)와 허용 case, conformance vector upsert_one_key. 그다음 다섯 validator는 삽입 행과 만날 수 있는 key가 정확히 하나이고 insert가 그 key의 모든 column을 할당할 때만 on_duplicate insert를 허용하고, 아니면 IR_INVALID "on_duplicate needs exactly one conflict key; %s can meet the row"다. 후보는 insert가 빼지 않은 primary key(identity나 IR executor가 할당하는 key는 빠진다)와 각 unique key다. 다섯 planner의 conflictTarget이 그 key를 돌려주고, MySQL은 ON DUPLICATE KEY UPDATE를 유지하며 이때 그 key에서만 만난다. conformance 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행하며, PHP 확장 실행이 key, identity, unique 사실을 증명한다. 문서(EN과 .ko.md): protocol.md §1 on_duplicate(삽입 행이 하나의 conflict key에서 기존 행과 만날 때 적용되고, 아니면 IR_INVALID), dialects.md:17(세 칸이 하나의 conflict key를 적고, MySQL의 key 순서와 PostgreSQL, SQLite의 단일 target은 key가 하나일 때만 일치한다는 이유를 더한다), dsl.md:266(duplication(model)은 conflict key가 하나인 table과 insert를 요구한다). 상태: 다섯 validator는 삽입 행을 만날 수 있는 key(primary key는 insert가 빼먹지 않았을 때만, 그리고 모든 unique key. blind index 컬럼은 그 AES source와 함께 계산한다)가 정확히 하나이고 insert가 그 모든 컬럼을 할당할 때만 on_duplicate insert를 받아들이며, 아니면 IR_INVALID "on_duplicate needs exactly one conflict key; %s can meet the row"로 거절한다. 다섯 planner의 conflictTarget은 그 key를 돌려주고 처음으로 덮이는 key 규칙은 지웠다. validator vector(upsert-one-unique-key, upsert-primary-key-and-unique-key, upsert-unique-key-not-assigned, upsert-no-key, upsert-composite-primary-key)와 planner.json case 넷이 Go, PHP, TypeScript, Rust에서 실행되고 Python은 두 파일의 모든 case를 실행한다. benchmark schema에는 unique key가 둘인 table이나 일부만 할당된 복합 unique key가 없어 그 두 거절 case는 돌리지 않았으며, 규칙은 그 경우에도 같은 후보 목록을 읽는다. 하지 않은 것: probe mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts, sqlite.upsert.other_key_conflicts와 세 database의 conformance vector upsert_one_key는 T87-1로 나눈다. 문서: protocol.md §1, dialects.md의 duplication과 on_duplicate 행.——[o]
T87-1upsert probe와 conformance vector upsert_one_key(T87에서 분리). 원인: T87이 validator와 planner가 충돌 key 하나를 허용하게 했고, 세 database가 그에 일치한다는 probe나 vector가 없다. 인수 조건: probe mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts, sqlite.upsert.other_key_conflicts를 기록하고, conformance vector upsert_one_key가 세 vector file에서 같은 결과를 내며 여섯 구현(Go, PHP, PHP extension, TypeScript, Python, Rust)이 각자 package의 test에서 돌리고 PHP extension 실행이 key 사실을 증명한다.—Evidence: probe mysql.upsert.any_unique_key, mysql.upsert.not_null_unique_first, postgres.upsert.other_key_conflicts(23505), sqlite.upsert.other_key_conflicts가 통과한다. bench table upsert_cell(identity key와 unique code)과 다섯 runner의 conformance vector upsert_one_key: insert하는 upsert, code로 update하는 upsert, code가 없는 upsert이며 마지막은 insert가 conflict key 하나를 쓰지 않아 IR_INVALID로 실패한다. 세 vector file에 같게 기록되고 다시 실행해도 통과한다. vector는 기록된 기대값 없이 red였고, client 일치는 T87이 이미 만들었다. 새 table 때문에 TestMermaidVectors가 요구하는 upsert_cell의 unique key를 tests/dbspec/mermaid.json에 더했다. PHP extension의 query vector는 T182 작업이다.[o]
T88write의 affected는 upsert를 1로, update를 맞은 행 수로 센 값이다. 원인: protocol 결함 수정. update로 끝난 같은 upsert가 MySQL에서는 2, PostgreSQL과 SQLite에서는 1이다. 모든 client가 driver의 수를 그대로 돌려주기 때문이다(exec.go:966, driver.ts:195, Db.php:839, driver.rs:414, database.py:495). update가 맞은 행 수인 것은 Go(dsn.go:58)와 PHP(Orm.php:56)가 found rows를 설정하기 때문일 뿐이고, 어떤 규칙도 이를 적지 않으며 다른 client에 대한 case도 없다. 완료 기준: 먼저 red: conformance vector affected_counts(insert하는 upsert, update하는 upsert, 그중 하나는 이미 그 값인 두 행에 맞는 update)가 tests/conformance/vectors.json에서 update하는 upsert에 2를 돌려준다. 그다음 다섯 client가 on_duplicate insert에는 insert든 update든 1을, update에는 맞은 행 수를 보고한다(모든 MySQL driver 설정에서 MySQL CLIENT_FOUND_ROWS). 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): protocol.md §5 Write와 §2의 "Write result" 문장("affected는 쓴 행을 한 번씩 센 값이다. on_duplicate insert는 모든 database에서 1이고, update는 맞은 행 수다"). 상태: 다섯 client는 on_duplicate가 있는 insert가 삽입이든 갱신이든 1을 보고하고(Go exec.go, TypeScript database.ts, PHP Db.php, Python database.py, Rust model.rs), update는 모든 MySQL driver 설정에서 일치한 row를 계산한다. Go(clientFoundRows), PHP(ATTR_FOUND_ROWS), TypeScript(mysql2 기본 flag. 두 row가 일치하고 하나가 이미 그 값인 update를 라이브 MySQL에서 재니 2), Rust(sqlx의 FOUND_ROWS capability)는 이미 그랬고, Python의 PyMySQL은 그러지 않아(재니 1) 이제 CLIENT.FOUND_ROWS를 넘긴다(재니 2. packages/orm-python/tests/test_mysql_found_rows.py는 이전 코드에서 red). 하지 않은 것: 세 database의 conformance vector affected_counts와 client마다 upsert 수를 보는 test는 T88-1로 나눈다. 문서: protocol.md §5 쓰기.——[o]
T88-1conformance vector affected_counts와 client마다 upsert 수를 보는 test(T88에서 분리). 원인: T88이 다섯 client가 upsert를 1로, update를 일치한 row로 보고하게 했고, 기록된 증거는 MySQL driver마다의 라이브 측정과 Python test 하나뿐이다. 인수 조건: conformance vector affected_counts(삽입하는 upsert, 갱신하는 upsert, 두 row가 일치하고 하나가 이미 그 값인 update)가 세 vector file에서 같은 결과를 내고 여섯 구현(Go, PHP, PHP extension, TypeScript, Python, Rust)이 각자 package의 test에서 돌리며, packages/orm-python/tests/test_mysql_found_rows.py를 contracts/features.json에 선언한다.—Evidence: protocol.md 3.4의 test 진입점이 다섯 client에서 마지막 write의 affected 수를 읽는다(Go ormtest의 orm.LastAffected, Rust test-faults의 testing::last_affected, TypeScript @polyspec/orm/testing의 lastAffected, PHP Faults::lastAffected, Python polyspec.orm.testing.last_affected). conformance vector affected_counts에서 insert하는 upsert, update하는 upsert, 이미 그 값을 가진 행의 update가 모두 1이고 세 vector file에서 같다. 완료 조건은 두 행에 맞는 update를 적었지만 model DSL은 load한 행 하나를 update하므로(usage.md 7절), vector는 바뀌지 않은 한 행으로 규칙(바꾼 행이 아니라 맞은 행)을 증명한다. RED: Go upsert override와 clientFoundRows를 빼자 MySQL에서 Go가 upsert_updated 2, update_unchanged 0이었다. test_mysql_found_rows.py는 contracts/features.json에 선언된다. PHP extension의 query vector는 T182 작업이다.[o]
T89모든 client가 parameter를 slot의 논리 type에 대해 검사하고, ir.ParamUses가 slot을 나열한다. 원인: protocol 결함 수정. T73이 별도 항목으로 미룬 CODEC_ENCODE 표준이 이 항목이다. dialects.md는 client가 초과 scale(:89), 길이(:91), U+0000(:94), p를 넘는 소수 자릿수(:110)를 거절한다고 적지만, bind 경로는 decimal과 SQLite 시간만 검사한다(exec.go:160-197). 그래서 같은 요청에 MySQL은 값을 강제 변환하거나 1406, 1264로, PostgreSQL은 DRIVER(22001, 22021, 42883, 22003)로, SQLite는 CONSTRAINT로 거절하거나 값을 저장한다. IR executor는 plan을 받지 않으므로 slot의 type을 알 수 없다. 완료 기준: 먼저 red: 새 공유 tests/bind/vectors.json(slot의 논리 type, precision, scale, length, nullability, transform, 값, bind된 값 또는 code)에 규칙마다 허용 case와 거절 case를 두고, codec-check와 같은 형태의 bind-check target을 만든다. slot의 Precision과 Length를 고정하는 planner.json case와 ParamUses case(column 함수, interval, contains, if_parent, optimistic), 지금 세 file이 서로 다른 conformance vector datetime_precision_rejected, varchar_length_rejected, text_nul_rejected, uuid_form_rejected를 더한다. 그다음 Go의 ir.ParamUses(model, request)와 다른 네 validator의 대응이 각 parameter의 쓰임을 논리 type과 함께 나열한다. 논리 type은 SQL 쪽 style 이전의 type이고 bind_slots[].col_type과 다르다. 비교되거나 할당되는 column은 precision, scale, length, nullability와 함께, column 함수는 그 결과 type(functionType과 valueFunctionArgType은 planner에서 engine/ir로 옮긴다), 상대 interval은 i32나 f64, contains와 contains_binary는 text, if_parent는 parent column, optimistic은 그 column이다. plan.BindSlot은 time과 datetime에 Precision을, varchar에 새 Length를 가지며, 다섯 planner의 colParam이 이를 설정한다. 어떤 statement보다 먼저, 다섯 bind 경로는 slot의 논리 type 밖의 값을 parameter와 type을 적은 CODEC_ENCODE로 거절한다. 대상은 runtime value model의 host 값 종류, i16/i32/i64 범위, 유한하지 않은 f64, decimal의 precision과 scale, 0001-01-01..9999-12-31 밖의 date, p 뒤에 0 아닌 숫자가 있는 time이나 datetime(0은 허용), 뒤 공백을 포함해 n자를 넘는 varchar, text나 varchar의 U+0000, 소문자 정규형이 아닌 uuid다. NULL은 T90이 정한 경우 말고는 허용한다. codec column의 slot은 값을 첫 stage의 입력에 대해 검사한다. conformance 결과는 어떤 statement보다 먼저 나오는 CODEC_ENCODE이고, 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 bind, planner.json, conformance vector를 실행한다. 문서(EN과 .ko.md): protocol.md §2 bind_slots와 §5 Parameters("모든 client는 어떤 statement보다 먼저 각 parameter를 slot의 논리 type에 대해 검사하고, 그 밖의 값은 CODEC_ENCODE다. IR executor도 같은 결과를 돌려준다"), dialects.md:89, 91, 94, 110(client가 CODEC_ENCODE로 거절하며, p를 넘는 0은 허용), dialects.md:55(논리 slot type과 col_type의 구별), errors.yaml의 CODEC_ENCODE 주석(종류, 범위, scale과 precision, 시간 precision, 길이, U+0000, uuid 형식, encode할 수 없는 styled 입력, non-null column의 SQL NULL). T91과 T92가 필요하다.—Evidence: T89-1(다섯 planner에서 slot 길이와 parameter 쓰임 고정)과 T89-2(statement 전에 모든 parameter를 slot type에 대해 검사, make bind-check와 conformance vector 넷)로 끝났다. 이 항목이 찾은 native time 값은 T89-3이 맡는다.[o]
T89-1다섯 planner에서 slot 길이와 parameter 쓰임을 고정한다 (T89의 일부).—Evidence: Go, PHP, TypeScript, Python, Rust의 모든 varchar slot이 길이를 가지고(PHP runtime model에 varchar 길이를 더함), TypeScript와 Python planner가 Go처럼 모르는 function 결과를 text로 적으며, planner.json이 모든 varchar slot의 length와 param_uses case 다섯(column function, interval, contains, if_parent, optimistic)을 고정한다. RED: TypeScript, Python, PHP, Go runner가 191 대신 length 0으로 실패했다. 다섯 모두 통과한다.[o]
T89-2statement 전에 모든 parameter를 slot type에 대해 검사한다 (T89의 일부).—Evidence: 다섯 client가 SQL connection과 IR executor에서 parameter마다 검사하고 statement 전에 CODEC_ENCODE로 실패한다. tests/bind/vectors.json에 규칙마다 받는 case와 거부하는 case를 둔 bind 절을 더했고 새 make bind-check가 다섯 client에서 실행한다. bench table bind_cell의 conformance vector datetime_precision_rejected, varchar_length_rejected, text_nul_rejected, uuid_form_rejected가 세 vector file에서 같다. RED: MySQL은 varchar에 DRIVER, NUL 저장, uuid에 CONSTRAINT였고, PostgreSQL은 DRIVER와 대문자 uuid 저장이었으며, PHP는 p를 넘는 datetime을 저장했고 Python은 JSON TypeError로 실패했다. 함께 고친 결함: PHP가 p를 넘는 0이 아닌 자리를 버렸고, Python이 datetime JSON과 1년 근처에서 실패했으며, TypeScript가 100 미만의 연도를 1900 이후로 읽었다.[o]
T89-3모든 client가 같은 native time 값을 받는다 (T89에 이어). 원인: 공유 bind vector는 text 값만 쓰고, client마다 date, time, datetime slot에 받는 native 값이 다르다. TypeScript Date는 time slot을 채우지 못하고, Rust에는 native time 값이 없으며, PHP, Python, Go는 각자의 값을 받는다(T89에서 발견). 완료 조건: protocol이 각 언어의 어떤 native 값이 각 time slot을 채우는지 적고, 각 client가 정확히 그것만 받으며, 공유 vector가 다섯 client에서 그것을 증명한다.—Evidence: commit fbca32dc(main): fix 전 RED: Go는 native case 5개 중 5개를 받아들였고, TypeScript와 Rust는 3개가 실패했다(native date와 datetime을 받아들임), PHP와 Python도 실패했다. GREEN: bind-check가 다섯 client에서 통과하고, engine의 go test가 통과한다. 범위: date, time, datetime slot, text만. now slot은 client 시계라서 caller 값에서 묶이지 않는다. interface-check: TypeScript snapshot hash는 checker 값 sha256:cdc43cdb…이다. 생성된 setter는 아직 native 값을 타입으로 받으며 그것은 T189다.[o]
T89-3-1conformance physical 단계가 date와 time에 text 입력으로 통과한다 (T89-3에 이어). 원인: T89-3은 bind 층에서 native 시간 값을 거부하지만, tests/conformance/vectors.json의 write_cycle은 여전히 native datetime과 time 값(Python datetime, Go time.Time, PHP DateTimeImmutable, TS Date, Rust chrono)을 datetime(6)과 time slot에 넣어서, conformance physical 단계가 모든 runner에서 중단된다(Go는 runner_go/main.go:241에서 panic, Python은 runner_python.py:766에서 CODEC_ENCODE). RED: make run-databases TARGETS=conformance-check가 physical 단계에서 중단된다. 완료 조건: write_cycle의 datetime(6)과 time 입력을 vector와 모든 runner에서 정규 text로 바꾸고 다른 것은 바꾸지 않는다. physical 단계가 다섯 runner 모두 끝나고 세 vector file이 같다.—Evidence: commit 9eb65ebb(fix/row-T89-3-2, rebase 전 5e61acd5, ff-merge 후 main): 다섯 runner가 write_cycle, now_defaults, restore, datetime_precision_rejected의 date, time, datetime 입력을 text로 넘긴다. 기대값은 그대로다. RED: physical 단계가 모든 runner에서 중단된다(CODEC_ENCODE: parameter 3 (time.Time) is not a datetime value, PHP와 TS의 setStartDt 입력). GREEN(T89-3-2와 공유): make run-databases TARGETS=conformance-check가 exit 0이고 PASS check/conformance-check이며 FAIL 줄이 없다(log t891-physical3.log).[o]
T89-3-2optimistic update가 읽은 행의 version을 정규 text로 바인딩한다, Go와 Rust에서 (T89-3에 이어). 원인: Core.Update(optimistic=true)가 읽은 행의 update 시각 version을 r.param(version)으로 바인딩하는데, version은 원본 행의 native time.Time이다(packages/go/orm/model.go). Rust의 읽은 행에 대한 update(true)도 같다. T89-3의 bind 검사가 datetime과 time slot의 native 시간 값을 거부하므로, conformance write_cycle의 loaded.SetName(...).Update(true)가 Go와 Rust에서 실패한다. RED: datetime(6)과 time 열에 대한 update(true)가 Go와 Rust에서 CODEC_ENCODE로 실패한다. 완료 조건: version을 바인딩하는 모든 client(Go, Rust, 그 밖에 있으면 그것)가 정규 text 형태로 바인딩하고, RED case가 통과하며, physical 단계가 write_cycle의 optimistic update를 지난다.—Evidence: commit 28b9871b(fix/row-T89-3-2, rebase 전 9b1c879d, ff-merge 후 main): Go와 Rust가 update(true)에서 읽은 행의 version을 정규 text로 바인딩한다. RED: mysql에서 TestWrites가 CODEC_ENCODE: parameter 3 (time.Time) is not a datetime value로 panic하고, Rust write_cycle은 DateTime 매개변수에서 CODEC_ENCODE다. GREEN: TestWrites가 ok이고, 공유 physical 단계가 exit 0이다(T89-3-1 참조). TypeScript와 Python은 이미 충족되어 수정하지 않았다: optimisticText case와 test_optimistic_text가 string seed로 version을 text로 바인딩하며 sqlite, mysql, postgres에서 통과한다.[o]
T89-4Python bind vector를 case harness로 실행한다 (T89-2에 이어). 원인: Makefile의 bind-check 줄이 python -m unittest packages.python.tests.test_markdown.BindMessageVectors …를 실행했고, test_testcase.py는 이를 거부한다(모든 Python test는 packages.python.tests.testcase로 실행). 그래서 main에서 python-check가 실패했다(ci run 38723975175). 완료 조건: 그 줄이 두 class를 harness로 실행하고 python-unit-check가 통과한다.—Evidence: python-unit-check가 그 줄을 적고 실패했다. 이제 그 줄은 같은 두 class로 python -m packages.python.tests.testcase를 실행하고, python-unit-check와 bind-check가 통과한다.[o]
T90non-null column에 bind된 NULL은 CODEC_ENCODE다. 원인: protocol 결함 수정. NOT NULL column에 할당된 NULL parameter는 database까지 가서 DRIVER(MySQL 1048, PostgreSQL 23502, SQLite 1299)가 되는데, errors.yaml의 CODEC_ENCODE 주석은 styled 입력에 대해 이미 non-null column의 SQL NULL을 적는다. 그래서 같은 입력이 경로에 따라 두 code를 가지고, IR executor에는 규칙이 없다. 완료 기준: 먼저 red: tests/bind/vectors.json case와 conformance vector null_into_not_null. 지금 세 file은 세 가지 driver code의 DRIVER를 돌려준다. 그다음 할당 slot(set, rows, on_duplicate, restore set)이 T89의 slot에서 column의 nullability를 가지고, 다섯 bind 경로가 어떤 statement보다 먼저 CODEC_ENCODE "parameter %d assigns NULL to the non-null %s"로 거절하며, 1048, 23502, 1299의 driver mapping은 raw SQL에서 DRIVER로 남는다. conformance 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): protocol.md §5 Parameters와 errors.yaml 주석(raw SQL은 DRIVER 유지). T89가 필요하다.—Evidence: 다섯 planner에서 non-null column의 assignment slot이 not_null과 column을 가지고, 다섯 client가 두 경로 모두에서 statement 전에 CODEC_ENCODE("parameter %d assigns NULL to the non-null %s")로 실패한다. null-into-not-null bind case, planner.json 고정값, conformance vector null_into_not_null이 세 vector file에서 같다. RED: 세 database의 네 client에서 DRIVER였고 PHP typed setter는 TypeError였다. PHP extension의 query vector는 T182 작업이다.[o]
T91IR executor message에서 time 값의 형식은 하나다. 원인: protocol 결함 수정. protocol.md:193은 IR executor message의 runtime value model JSON을 decimal, bytes, datetime에 대해서만 정의하므로, time parameter나 결과 값에는 executor가 일관되게 decode할 형식이 없다(flowmark는 time(0)의 10:00:00.000000을 거절한다). 완료 기준: 먼저 red: time(0)과 time(3)에 대한 tests/bind/vectors.json의 IR message case(encode된 JSON). 지금 time(0)에 여섯 자리를 쓰는 client는 없다. 그다음 다섯 client의 IR executor message encoder가 time을 p 뒤가 0인 HH:MM:SS.ffffff로 쓴다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 case를 실행하고, 아직 IR message encoder가 없는 client는 그 executor 항목(T63부터 T67)의 완료 기준으로 이 case를 가져간다. 문서(EN과 .ko.md): protocol.md §5 Parameters("datetime은 UTC의 YYYY-MM-DD HH:MM:SS.ffffff, time은 HH:MM:SS.ffffff이며, datetime(p)나 time(p) 값은 p 뒤가 0이다").—Evidence: tests/bind/vectors.json의 message 절(10 case)이 다섯 client에서 실행된다. RED: Go, TypeScript, Rust가 10개 중 8개에서 실패했고("10:00:00"이 그대로, "10:00"을 받음) PHP는 time-p0에서 실패, Python은 통과했다. 네 encoder가 이제 time, date, datetime 문자열을 connection zone의 여섯 자리 소수로 쓰고 다른 text에는 CODEC_ENCODE로 답하며, 다섯 모두 통과한다. PHP extension의 query vector는 T182 작업이다.[o]
T91-1Go bind vector test를 case로 시작한다 (T91에 이어). 원인: packages/go/orm/markdown_param_test.go의 TestBindMessageVectors가 packages/go/testcase로 case를 시작하지 않아 main에서 go-vet-check가 실패했다("does not start its case with packages/go/testcase"). 완료 조건: test가 Compute case를 시작하고 go-vet-check가 통과한다.—Evidence: 8b230229의 main ci run이 그 줄로 go-vet-check에서 실패했다. test가 이제 testcase.Start(t, testcase.Compute)를 부르고 통과하며 go-vet-check가 통과한다.[o]
T92SQLite는 datetime(p)와 time(p)를 정확히 p개의 소수 자릿수로 bind한다. 원인: protocol 결함 수정. dialects.md:48은 SQLite datetime이 소수 여섯 자리 text라고 적고 Go bind 경로도 여섯 자리를 쓰지만(sqlite_time.go:40), renderer CHECK는 정확히 p자리를 요구한다(render.go:445-457, length = 20+p와 9+p). 그래서 bind된 datetime(p<6) 값이 제 column의 CHECK를 어긴다. 완료 기준: 먼저 red: conformance vector datetime_p0_roundtrip와 time_p3_roundtrip(insert, 다시 읽기, parameter와 비교). 지금 SQLite에서 datetime(0) parameter의 insert는 CHECK(CONSTRAINT)로 실패한다. 그다음 다섯 client가 SQLite에서 datetime이나 time 값을 정확히 p개의 소수 자릿수로 bind한다(T89가 p를 넘는 0 아닌 숫자를 먼저 거절하므로 버려지는 것은 0뿐이다). 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): dialects.md:48("SQLite datetime은 소수 정확히 p자리의 UTC text YYYY-MM-DD HH:MM:SS, time은 p자리의 HH:MM:SS로, renderer CHECK가 요구하는 형식이며 client가 그 형식으로 bind한다"). 상태: planner가 모든 datetime과 time parameter slot에 column의 precision을 담고(함수의 인자나 결과는 6), 공유 planner fixture가 Go, PHP, Python, Rust, TypeScript에서 이를 고정하며, Go client는 p자리로 bind하고 p자리 뒤의 0은 버리고 0이 아닌 자리는 CODEC_ENCODE로 거절한다(f0eb77d8, TestSQLiteTimePrecision). 여섯 구현의 conformance vector datetime_p0_roundtrip과 time_p3_roundtrip이 남아 있다.—Evidence: bench table time_cell, 다섯 runner의 conformance vector datetime_p0_roundtrip과 time_p3_roundtrip, planner case planner_time_select와 planner_time_insert. RED: Rust가 모든 database에서 실패했고(time value is not HH:MM:SS with 3 fraction digits), PHP는 SQLite에서 time_cell$clock CHECK에 걸렸으며, Python은 MySQL과 PostgreSQL에서 CODEC_DECODE로 실패했고, Go와 Rust는 다른 client가 p자리로 bind할 때 여섯 자리로 bind했다. 이제 모든 client가 datetime과 time text를 정확히 p자리로 bind하고, 나머지 네 planner가 Rust planner의 time(p) SQL을 똑같이 쓰며, vector는 세 vector file에서 같고 다시 실행해도 통과한다. PHP extension의 query vector는 T182 작업이다.[o]
T92-1PostgreSQL uuid column을 모든 planner에서 한 형태로 쓴다 (T92에 이어). 원인: Rust planner는 PostgreSQL uuid column을 CAST(... AS text)로 감싸고 Go, PHP, TypeScript, Python planner는 감싸지 않아, 같은 read에 client마다 SQL이 다르다. 이를 다루는 planner case가 없다(T92에서 발견). 완료 조건: PostgreSQL uuid read의 planner case가 다른 planner에서 실패한 뒤, 다섯 planner가 한 SQL을 쓰고 conformance vector가 모든 client에서 같은 값을 읽는다.—Evidence: planner case planner_uuid_select와 planner_uuid_insert가 PostgreSQL에서 Go, TypeScript, PHP, Python에서 실패했고(맨 column과 $1) Rust에서 통과했다. 네 planner가 이제 Rust처럼 CAST(col AS text)로 읽고 CAST(CAST(? AS text) AS uuid)로 bind하며, 다섯 모두 통과한다. bench table uuid_cell의 conformance vector uuid_roundtrip이 세 database에서 같은 uuid를 읽는다. PHP extension의 query vector는 T182 작업이다.[o]
T92-2Rust value test가 T92의 time 규칙을 확인하게 한다 (T92에 이어). 원인: packages/rust/orm/tests/value_types.rs가 time(3) column의 08:30:00.25에 아직 CODEC_ENCODE를 기대하는데, T92가 그것을 유효하게 했다(모자란 자리는 0으로 읽음). 그래서 main의 clients_db, clients_pooler, feature_verify_rust에서 runtime_value_types가 실패했다("sqlite: time text without p fraction digits"). 완료 조건: test가 p 너머에 0이 아닌 자리가 있는 08:30:00.2501을 쓰고 CODEC_ENCODE를 기대하며, MySQL, PostgreSQL, SQLite에서 통과한다.—Evidence: a807cd4b의 main ci run 38695977729가 모든 job을 통과했다. 그중 clients_db, clients_pooler, feature_verify_rust가 새 assertion으로 MySQL, PostgreSQL, SQLite에서 runtime_value_types를 실행한다. 8b230229의 run은 예전 assertion으로 그 job들에서 실패했었다.[o]
T93statement의 시계 값은 하나이고, 쓸 때는 잘라 내며 비교할 때는 여섯 자리를 유지한다. 원인: protocol 결함 수정. protocol.md:139는 ORM이 쓰거나 비교하는 시계가 microsecond를 유지하고 statement는 시계를 한 번 읽는다고 적는다. 그러나 PostgreSQL CURRENT_TIMESTAMP와 statement_timestamp() 기본값은 timestamp(p<6)에 반올림되고(32.784706 → 33) MySQL CURRENT_TIMESTAMP(p)는 잘라 내며 SQLite는 잘라 낸 slot을 bind한다. PostgreSQL은 한 statement에서 transaction 시작 시각인 now()와 statement_timestamp()를 섞을 수 있다. SQLite planner는 p자리로 비교하는 반면(planner.go:806-810) MySQL과 PostgreSQL은 datetime(p) column을 여섯 자리 NOW(6)과 비교한다. 완료 기준: 먼저 red: probe mysql.now.precision_truncates, postgres.now.precision_rounds, mysql.now.compare_six_digits, postgres.now.compare_six_digits, planner.json PostgreSQL과 SQLite case(slot, SQLite 확장 식), conformance vector clock_truncate_write(tests/conformance의 clock test 진입점으로 .999999에 고정한 client 시계 아래에서 쓴 datetime(0)과 datetime(3))와 clock_compare_six_digits. 지금 PostgreSQL file은 반올림된 초를 보이고 SQLite 비교는 다르다. 그다음 MySQL은 CURRENT_TIMESTAMP(p)와 NOW(6)을 유지한다. PostgreSQL은 update 시각, soft delete, insert가 빼는 모든 default now column에 p로 잘라 낸 now slot을 bind하고(DDL 기본값은 raw SQL을 위해 statement_timestamp()로 남는다), now value function과 상대 형식에는 여섯 자리로 bind한다. SQLite는 지금처럼 slot을 bind하되 value function slot은 항상 precision 6이고, datetime(p<6) column을 시계와 비교할 때 column을 여섯 자리로 늘린다(p = 0이면 col || '.000000', 0 < p < 6이면 col || '<6−p zeros>'). 다섯 planner와 dialect가 그렇게 한다. 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): protocol.md:139를 이 규칙으로 고쳐 쓴다(MySQL insert는 그 column을 database 기본값에 맡기고, PostgreSQL과 SQLite insert는 빠진 모든 default now column에 p로 잘라 낸 now slot을 bind한다). dialects.md:25(PostgreSQL 시계 칸: p로 잘라 낸 now slot), :46(UPDATE 규칙은 mysql.go:18이 render하는 대로 MySQL에 CURRENT_TIMESTAMP(p)를 적는다), :112(Current time), :288(기본값: raw SQL 전용).—Evidence: probe mysql.now.precision_truncates, mysql.now.compare_six_digits, postgres.now.precision_rounds, postgres.now.compare_six_digits가 통과한다. planner case planner_clock_compare와 planner_clock_insert, bench table clock_cell의 conformance vector clock_truncate_write와 clock_compare_six_digits. RED: MySQL이 {at0_ge_now:0, at3_lt_now:1}, SQLite가 {at0_ge_now:1, at3_lt_now:0}을 주었고 PostgreSQL은 반올림 때문에 run마다 달랐다. 이제 PostgreSQL은 now slot을 p로 잘라 bind하고 SQLite는 비교할 때 datetime(p<6) column을 늘린다. 다섯 client에 test clock 진입점이 생겼고(protocol.md 3.3), vector는 세 file에서 같고 다시 실행해도 통과한다. PHP extension의 query vector는 T182 작업이다.[o]
T94ir.Validate가 update와 restore의 primary key나 identity 할당을 거절한다. 원인: protocol 결함 수정. 한 규칙이 두 계층에 있다. on_duplicate는 ir.go:372-374가 거절하지만 update와 restore는 다섯 planner만 거절한다(planner.go:1289, planner.ts:789, Planner.php:864, planner.py:910, planner.rs:1206). 그래서 client가 ir.Validate로 검증만 하고 plan하지 않는 IR executor(protocol.md §5)는 이 거절을 거치지 않는다. 완료 기준: 먼저 red: 지금 ir.Validate가 요청을 받아들이는 validator 전용 case. 그다음 다섯 validator가 primary key나 identity column을 할당하는 update 또는 restore set을 IR_INVALID "cannot update %s"로 거절하고(restore는 soft delete column도, protocol.md:112), 도달할 수 없게 된 planner 검사는 지운다. 기존 planner.json "cannot update" case는 같은 code로 검증 쪽으로 옮기고 restore case를 더한다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 실행하며, PHP 확장 실행이 key와 identity 사실을 증명한다. 문서(EN과 .ko.md): protocol.md §1.4("primary key나 identity column을 할당하는 update는 IR_INVALID로 실패한다"), dsl.md:275와 일치. 상태: 다섯 validator는 primary key나 identity 컬럼(restore는 soft delete 컬럼도)을 할당하는 update나 restore의 set에 IR_INVALID "cannot update %s"로 답하고, 다섯 planner의 도달할 수 없게 된 검사는 지웠다. validator vector(update-plain-column, update-primary-key, update-identity-key, restore-soft-delete-column, restore-primary-key, restore-plain)가 Go, PHP, TypeScript, Rust에서 실행되고 Python도 돌리며, 각 구현의 cannot update seq compile case는 validator를 거쳐 같은 code로 실패한다. PHP extension은 dbspec만 parse하며 validator가 없다. 문서: protocol.md §1.4.——[o]
T95join child는 soft delete된 행을 제외한다. 원인: protocol 결함 수정. protocol.md:118과 dbspec.md:158, 262는 읽기가 항상 soft delete된 행을 제외한다고 적는다. root(planner.go:280)와 subquery(:1555) 읽기는 제외하지만, 다섯 planner는 join rendering에서 이를 빠뜨린다(planner.go:690-708, planner.ts:494, Planner.php:398, planner.py:587, planner.rs:769). 완료 기준: 먼저 red: SQL에 조건이 없는 planner.json join case와, conformance vector joins_soft_deleted(inner join은 parent를 버리고, left join은 null을 준다). 그다음 다섯 planner가 join된 entity의 ON 절에 AND <alias>.<soft_delete> IS NULL을 덧붙인다. 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): protocol.md:118(root, ON 조건 안의 각 join child, 각 relation, 각 subquery)과 dbspec.md:158, 262도 같이.—Evidence: commit 49546d69(main; join 변경은 e8425dff): 다섯 planner가 joined entity의 ON에 AND <alias>.<soft_delete> IS NULL을 더한다. RED: fix 없이 Go, Rust, Python, PHP의 planner.json join case 세 개가 실패하고 TypeScript는 throw한다. vector joins_soft_deleted는 옛 planner에서 DIFF다. GREEN: 다섯 모두 통과하고, vector는 MySQL, PostgreSQL, SQLite에서 같은 행을 준다. Unlinked: main에서 env -u POLYSPEC_CHECKOUTS go test ./packages/go/engine/... 과 make feature-unit-check/owners가 모두 exit 0이다.[o]
T96비교되는 두 column은 dbspec check 피연산자처럼 맞아야 한다. 원인: protocol 결함 수정. ir.Validate는 *_col(ir.go:789-793), in subquery(:705-716), join key(:459-470), relation key(:516-523)의 type을 검사하지 않아 database마다 결과가 다르다. integer = varchar에 PostgreSQL은 42883이고 MySQL은 강제 변환하며 SQLite는 affinity로 비교한다. date = datetime은 1, t, 0이다. SQLite의 숫자 affinity 때문에 decimal(4,2) = i64, scale이 다른 decimal, 2^53 근처의 i64 = f64가 서로 다르다. SQLite는 time과 datetime을 p자리로 저장한다. 어떤 column끼리 맞는지는 dbspec.md:128-140이 이미 적는다. 완료 기준: 먼저 red: 각 위치(*_col, in subquery, join, relation key)의 planner.json 거절, 허용 case와 conformance vector column_pairs. 거절 case는 지금 검증을 통과한다. 그다음 다섯 validator가 *_col 술어, subquery column에 대한 column의 in/not_in, join의 left/right, 각 relation key 쌍의 두 column이 dbspec의 표에 bytes와 bytes를 더한 기준(정수와 정수, scale이 같은 decimal, f64, bool, varchar/text, uuid, date, precision이 같은 time이나 datetime, bytes)으로 맞기를 요구하고, 아니면 OPERATOR_NOT_ALLOWED다. 각 구현에서 표 하나가 dbspec check 검증과 IR validator 모두에 쓰여 둘이 어긋날 수 없다. 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행하며, PHP 확장 실행이 type, precision, scale 사실을 증명한다. 문서(EN과 .ko.md): protocol.md §1.3(column 비교, in subquery, join의 left/right, relation key 쌍은 check 피연산자처럼 맞는 두 column과 두 bytes column을 비교하고, 다른 쌍은 OPERATOR_NOT_ALLOWED다), dbspec.md(IR이 같은 표를 쓴다). 상태: 다섯 validator는 *_col 조건의 두 컬럼, 컬럼 in/not_in subquery 컬럼, join의 left/right, 각 relation key 쌍이 dbspec의 표로 만나도록 요구하고 아니면 OPERATOR_NOT_ALLOWED로 거절한다. validator vector(지점마다 받아들이는 case와 거절하는 case, 여덟 개)와 planner.json case 다섯 개가 Go, PHP, TypeScript, Rust에서 실행되고 Python은 두 파일의 모든 case를 실행한다. 세 database의 conformance vector와 공유 표 하나는 T96-1로 나눈다. 문서: protocol.md §1.3.——[o]
T96-1conformance vector column_pairs와 dbspec check 검증과 IR validator가 함께 쓰는 표 하나(T96에서 분리). 원인: T96이 만나지 않는 두 컬럼을 validator가 거절하게 했고, 각 구현은 meets를 dbspec의 check 코드와 validator에 따로 가지며, 만나는 쌍에서 세 database가 일치한다는 vector가 없다. 인수 조건: conformance vector column_pairs(*_col, in subquery, join, relation마다 만나는 쌍과 만나지 않는 쌍)가 세 vector file에서 같은 결과를 내고 여섯 구현(Go, PHP, PHP extension, TypeScript, Python, Rust)이 각자 package의 test에서 돌린다. 구현마다 표 하나가 dbspec check 검증과 IR validator를 함께 맡아 둘이 어긋날 수 없다.—Evidence: commit 81122bb6, e1953d29, 49546d69(main): client마다 meet table 하나(Go internal/meet, TypeScript meet.ts, Python meet.py, PHP Dbspec/Meet.php, Rust Type::meet_kind는 #[doc(hidden)] pub이며 link-owner 판정에 따라 polyspec-orm 내부용). RED: vector column_pairs는 기록 전 기대값이 없다. GREEN: 세 database에서 같다. Rust의 차이(bytes = bytes)를 고쳤고 dbspec.md에 bytes 행이 있다. Unlinked: main에서 go-model-check, interface-check, go-vet-check, go-fmt-check가 모두 exit 0이다.[o]
T97column 비교 연산자는 column의 type과 codec에 따라 허용된다. 원인: protocol 결함 수정. ir.OpAllowed는 모든 type과 codec에서 bool gt_col이나 uuid lt_col 같은 모든 *_col 연산자를 받아들이지만(ir.go:268-271), opsByType(ir.go:212-226)은 bool, uuid, bytes의 gt를 거절하고, aes column의 eq_col은 무작위화된 암호문을 비교한다. 완료 기준: 먼저 red: 지금 검증을 통과하는 planner.json 거절 case(bool gt_col, uuid lt_col, bytes gt_col, aes eq_col)와 허용 case ip eq_col ip. 그다음 다섯 validator는 *_col 연산자의 기본 연산자(eq_col이면 eq 등)가 왼쪽 column type의 opsByType에 있고 두 column 모두 codec이 없을 때 허용한다. codec이 ip뿐인 두 column은 양쪽이 dialect의 같은 저장 형태이므로 eq_col과 not_eq_col을 쓸 수 있다. 그 밖은 OPERATOR_NOT_ALLOWED다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 case를 실행한다. 문서(EN과 .ko.md): protocol.md §1.3, T96과 함께. 상태: 다섯 validator는 기본 연산자가 왼쪽 컬럼 type의 연산자이고 어느 컬럼도 codec이 없을 때(ip만 있는 두 컬럼은 eq_col과 not_eq_col) *_col op를 허용하고, 그 밖에는 OPERATOR_NOT_ALLOWED로 거절한다. validator vector(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)와 planner.json case 넷(ip eq_col ip 받아들임, bool gt_col, ip gt_col, aes eq_col 거절)이 Go, PHP, TypeScript, Rust에서 실행되고 Python은 두 파일의 모든 case를 실행한다. benchmark schema에는 uuid 컬럼이 없어 uuid lt_col case는 돌리지 않았으며, 규칙은 그 컬럼에도 bool과 같은 표를 읽는다. PHP extension은 dbspec만 parse하며 validator가 없다. 문서: protocol.md §1.3.——[o]
T980001부터 9999 밖의 상대 시간 함수는 NULL이다. 원인: protocol 결함 수정. 범위를 넘으면 MySQL과 SQLite는 NULL이고, MySQL은 0001-01-01 아래에서 orm 범위 밖의 값인 0000-00-00을 주며(dialects.md:111), PostgreSQL은 큰 interval에서 오류("interval out of range")를 내거나 10000년을 돌려준다. 그래서 같은 요청이 행, 빈 결과, 오류 중 하나가 된다. 완료 기준: 먼저 red: probe mysql.relative.out_of_range_null, postgres.relative.interval_out_of_range, sqlite.relative.out_of_range_null과 conformance vector relative_time_out_of_range(seconds_ago(1e300), days_later(4000000), months_ago(200000), 각각과 그 부정이 어떤 행과도 맞지 않음). 지금 PostgreSQL file은 오류다. 그다음 결과가 0001-01-01 00:00:00부터 9999-12-31 23:59:59.999999 밖이거나 interval을 표현할 수 없는 상대 시간 함수는 NULL이다. 다섯 dialect 함수 renderer가 MySQL(functions.go:52-60)과 PostgreSQL(functions.go:94-110) rendering을 interval에 대한 CASE WHEN <arg> BETWEEN <min> AND <max> THEN … END와 결과에 대한 CASE WHEN (expr) BETWEEN '0001-01-01' AND '9999-12-31 23:59:59.999999' THEN … END로 감싸고, SQLite는 probe가 NULL 아닌 값을 보일 때만 결과의 하한 검사를 더한다. 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): dialects.md 규칙과 protocol.md §1.3 value.—Evidence: commit 596fcc9e(main): probe mysql.relative.out_of_range_null, postgres.relative.interval_out_of_range, sqlite.relative.out_of_range_null가 통과하고, 다섯 renderer가 interval을 CASE로 감싸 0001부터 9999 밖에서 NULL을 준다. RED: vector relative_time_out_of_range가 PostgreSQL에서 다섯 runner 모두 interval out of range(22008)로 실패했다. GREEN: 세 database 모두 days_later, months_ago, seconds_ago에 [0,0]을 기록한다. Unlinked: main에서 rust-fmt-check와 Go engine 테스트가 모두 exit 0이다.[o]
T99insert는 값이나 NULL로 할당한다. 원인: protocol 결함 수정. planner는 table로 한정한 plus_p나 minus_p 참조를 VALUES 안에 render한다(planner.go:1259-1266). MySQL은 이를 기본값에서 계산하고(3 + 5 = 8), PostgreSQL과 SQLite는 거절한다. 완료 기준: 먼저 red: 지금 plan되는 planner.json 거절 case와 허용 case. 그다음 다섯 validator의 insert 분기가 set의 plus_p나 minus_p를 IR_INVALID "insert cannot assign %s by plus or minus"로 거절하고, on_duplicate에서는 유효하게 남긴다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 case를 실행한다. 문서(EN과 .ko.md): protocol.md §1.4("insert는 p나 null로 할당한다. plus_p와 minus_p는 거기서 IR_INVALID이고 on_duplicate에서는 유효하다"). 상태: 다섯 validator의 insert 분기는 set의 plus_p나 minus_p에 IR_INVALID "insert cannot assign %s by plus or minus"로 답하며, 그 둘은 on_duplicate와 update에서 유효하다. validator vector(insert-by-value, insert-on-duplicate-plus, insert-set-plus, insert-set-minus, update-set-plus)와 planner.json case 넷(받아들이는 것 둘, 거절하는 것 둘)이 Go, PHP, TypeScript, Rust에서 실행되고 Python은 두 파일의 모든 case를 실행한다. planner의 renderAssign은 update와 on_duplicate가 여전히 쓰므로 지울 planner 경로는 없고, PHP extension은 dbspec만 parse하며 validator가 없다. 문서: protocol.md §1.4.——[o]
T100dialect 문서가 여러 행을 지우는 delete 하나의 RESTRICT 시점을 기록한다. 원인: backend 공통 표준, database 사실만. 참조되는 행과 그 행을 참조하는 행을 함께 지우는 DELETE 하나를, MySQL과 SQLite는 참조되는 행이 그 행 순서에서 먼저 오면 거절하고 아니면 받아들이며, PostgreSQL은 받아들인다. orm은 이 사실을 적지 않는데, 이식 가능한 program에는 필요하다. 그런 delete를 거절하는 것은 orm이 아니라 flowmark의 규칙이다. orm의 SQL backend는 그 규칙을 지키지 않기 때문이다. 완료 기준: 먼저 red: 그 행을 요구하는 docs-check case가 지금의 dialects.md에서 실패하고, tests/dialects의 probe mysql.fk.restrict_row_order, sqlite.fk.restrict_row_order, postgres.fk.restrict_statement_end를 사실에 맞춰 쓴다. 그다음 dialects.md "Foreign keys"의 Actions 행(EN과 .ko.md)이 적는다: "참조되는 행과 그 행을 참조하는 행을 함께 지우는 DELETE 하나는, 참조되는 행이 행 순서에서 먼저 오면 MySQL과 SQLite가 거절하고 PostgreSQL은 받아들인다. 이식 가능한 code는 참조하는 행을 먼저 지운다." client 동작은 바뀌지 않으므로 구현 vector는 더하지 않고 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)의 suite는 그대로이며, probe가 공유 증거다. markdown executor의 거절은 T102의 mirror 표에만 나온다.—Evidence: commit cd105aee(main): tests/dialects의 TestRestrictRowOrder가 dialects.md와 그 twin에 RESTRICT 행 순서를 적을 것을 요구한다. RED: 두 페이지 모두 규칙이 없었다. TestProbeCitations RED: 새 probe 3개가 두 페이지에 인용되지 않았다. GREEN: 둘 다 통과한다. 물리 TestDialectFacts: probe 276개, 실패 0개(mysql 116, postgres 86, sqlite 73). 찾았지만 바꾸지 않은 것: Actions 행의 PostgreSQL 열이 RESTRICT를 즉시 검사한다고 적지만 postgres.fk.restrict_statement_end가 통과한다. 이 정정은 후속 행이 필요하다.[o]
T101dialect 문서가 거절된 statement의 오류 순서를 기록한다. 원인: backend 공통 표준, database 사실만. CHECK와 key를 함께 어기는 행 하나는 세 database 모두에서 CHECK를 보고하고, PostgreSQL과 SQLite는 foreign key를 statement 끝에서, MySQL은 행마다 검사한다. SQL statement는 오류 하나를 보고하므로 여러 오류의 순서와 나열은 flowmark의 규칙이다. 완료 기준: 먼저 red: 그 규칙을 요구하는 docs-check case가 지금의 dialects.md에서 실패하고, probe mysql.fault.check_before_unique, postgres.fault.check_before_unique, sqlite.fault.check_before_unique와 database마다의 fault.fk_statement_end를 사실에 맞춰 쓴다. 그다음 dialects.md 규칙(EN과 .ko.md): "오류가 여럿인 statement는 모든 database에서 행의 CHECK 오류를 key 오류보다 먼저 보고한다. PostgreSQL과 SQLite는 foreign key를 statement 끝에서, MySQL은 행마다 보고한다." 구현 vector는 T100처럼 없다.—Evidence: probe mysql, postgres, sqlite .fault.check_before_unique(3819, 23514, CHECK constraint failed)와 .fault.fk_statement_end(MySQL 1452, PostgreSQL과 SQLite는 같은 statement가 채우는 참조를 받음)가 server에서 통과한다. 새 compute case TestProbeCitations가 dialects.md와 그 twin이 모든 probe를, 그리고 probe나 ddl.json vector만 인용하도록 요구하며(RED: 인용하지 않음 12건), CHECK 표에 Fault order 줄이 생겼다. lock으로 되돌린 설치에서 link 없이 검사가 통과한다(dependency-policy-check 포함).[o]
T102Markdown IR executor 표가 flowmark spec 60과 같다. 원인: protocol 결함 수정. dialects.md:387은 이 표의 판정이 flowmark capability 표의 판정이라고 적지만 둘은 어긋났다. 표에는 사용할 수 없는 table의 행(spec 60:60)이 없고, flowmark T12.4가 ordering, group_by, insert, update, delete/restore, on_duplicate, primary key 비교, relation, scalar column subquery, column 비교, contains, contains_binary, now/soft_delete, soft_delete, fulltext 행을 바꾸거나 더하며, on_duplicate 행은 T87이 없애는 차이를 기록한다. 완료 기준: 먼저 red: 표의 surface와 text를 새 tests/markdown/flowmark.version에 고정한 판의 flowmark docs/spec/60-dialect.md와 비교하는 docs 검사가 지금의 표(사용할 수 없는 table 행 누락, Lb 행)에서 실패한다. 그다음 dialects.md "Markdown IR executor"(EN과 .ko.md)가 spec 60의 행마다 같은 surface와 규칙 text를 Result와 Rule로 나눠 한 행씩 가지고, 검사가 통과한다. 이 항목은 그 규칙을 바꾸는 T79부터 T101 뒤에 들어간다. client 동작은 바뀌지 않으므로 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)의 suite는 그대로다. 상태: tests/markdown/flowmark.version은 flowmark 444f27f(flowmark T12.1-38~T12.4-9의 판정을 담은 spec 60)를 고정하고, Go, PHP, TypeScript, Rust client는 그 직전 판(문서만 다르다)으로 build한 flowmark에서 tests/markdown/cases.json을 통과한다(Python runner는 이 환경에 polyspec.ordered_json이 없어 실행하지 못했다). scripts/docs/markdown-table.mjs(make markdown-table-check, contracts/check-inputs.json에 선언)는 고정한 module을 내려받아 docs/spec/dialects.md와 docs/dialects.ko.md의 "Markdown IR executor" 표를 그 spec 60과 한국어 쌍에 행마다 비교한다. 판정은 첫 최상위 ;, :, —, . 에서 결과와 규칙으로 나누고 상대 link는 절대 link로 바꾼다. 이전 표에서는 실패했고 다시 쓴 표에서는 통과한다. 표는 orm T79~T101의 판정을 이미 담은 flowmark의 spec 60을 그대로 옮기며, 그 항목들에 대한 client의 일(bind 경로, vector, decoder)은 각 항목에 남고 client 동작은 바뀌지 않는다.——[o]
T103MySQL도 dialect 표대로 hex를 client에서 decode한다. 원인: protocol 결함 수정. dialects.md:22는 aes와 hex style이 세 database 모두에서 client 쪽이라고 적지만, MySQL dialect는 hex를 SQL의 UNHEX로 decode한다(engine/dialect/mysql.go:63, 76, conformance SQL UNHEX(a.aes_hex_email)). 그래서 잘못 저장된 hex가 MySQL에서는 NULL, PostgreSQL과 SQLite에서는 CODEC_DECODE다. 완료 기준: 먼저 red: conformance vector hex_invalid_stored(raw SQL로 넣은 잘못된 hex를 다시 읽으면 CODEC_DECODE)가 지금 MySQL file에서 NULL이다. 그다음 다섯 planner의 MySQL dialect가 host hex stage에 HEX/UNHEX를 render하지 않는다. 결과는 세 vector file에서 같다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 vector를 실행한다. 문서(EN과 .ko.md): dialects.md:22에 "잘못된 hex text는 모든 database에서 CODEC_DECODE다"를 더한다. decode가 받아들이는 text는 T74가 정한다.—Evidence: read 쪽은 이미 client가 decode했다(T74, vector hex_malformed가 CODEC_DECODE). RED: write_cycle이 ?를 기대하자 다섯 runner가 MySQL에서 여전히 HEX(?)를 보냈다. 다섯 client의 MySQL planner가 이제 client가 encode한 hex를 ?로 쓰고, write_cycle과 hex_malformed가 세 database에서 통과하며, planner.json case planner_bind_types_update와 _insert가 varchar(255) slot의 ?를 기대한다. lock으로 되돌린 설치에서 link 없이 검사가 통과한다(dependency-policy-check 포함).[o]
T104client가 MySQL session의 sql_mode를 설정한다. 원인: protocol 결함 수정. 어떤 client도 sql_mode를 설정하지 않는다(dsn.go:57-58은 time_zone과 clientFoundRows만 설정하고, 다른 client도 같다). dialects.md:73은 probe server의 STRICT_TRANS_TABLES와 기본 ONLY_FULL_GROUP_BY를 server 사실로 기록한다. MySQL의 강제 변환, 잘라 내기, grouping 결과가 그 mode에 따라 달라지므로, 결과가 protocol이 정하지 않은 server 설정에 달려 있다. 완료 기준: 먼저 red: probe mysql.session.sql_mode_set과, MySQL에서 @@SESSION.sql_mode를 읽는 conformance case. 지금은 server 기본값이다. 그다음 다섯 client가 접속할 때 session sql_mode를 선언된 값 하나, 곧 STRICT_ALL_TABLES,ONLY_FULL_GROUP_BY,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION 또는 probe가 근거를 보이는 집합으로 설정한다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 case를 실행한다. 문서(EN과 .ko.md): config.md와 dialects.md가 session 설정을 적는다.—Evidence: tests/dsn/mysql-session.json이 session sql_mode를 선언하고, probe mysql.session.sql_mode_set이 server가 그것을 다시 읽는 방식을 확인하며, 각 client가 자기 connection으로 @@SESSION.sql_mode를 읽는다(Go TestMySQLSessionSQLMode, PHP test_session_sql_mode, TypeScript session_sql_mode, Rust session_sql_mode_is_the_declared_one, python-mysql-session-check의 Python mysql_session.py). RED: 다섯 모두 STRICT_TRANS_TABLES였고 Rust는 sqlx 때문에 PIPES_AS_CONCAT과 IGNORE_SPACE, TypeScript는 mysql2 때문에 IGNORE_SPACE가 더 있었다. 다섯 모두 ProxySQL을 거쳐서도 통과한다. lock으로 되돌린 설치에서 link 없이 검사가 통과한다(dependency-policy-check 포함).[o]
T105IR executor의 읽기 결과는 JSON 형태가 하나다. 원인: protocol 결함 수정. protocol.md §5 Read는 행이 요청의 형태로 조립된다고 적지만 JSON 형태는 없다. 종류마다 결과를 어떻게 감싸는지, member 순서, 값 형식(protocol.md:193은 parameter만 다룬다)이 정해져 있지 않아 client가 결과를 decode할 수 없고, 두 번째 executor는 다르게 응할 수 있다. 완료 기준: 먼저 red: 공유 결과 fixture contracts/fixtures/ir_answers.json(결과 JSON → 조립된 model array 형태)이 decoder 없는 모든 client에서 실패한다. 그다음 protocol.md §5 Read(EN과 .ko.md)가 적는다: "읽기는 one, all, group_count에 {"rows": [...]}, count에 {"count": n}, sum과 avg에 {"value": v}, paginate에 {"rows": [...], "count": n}을 돌려주며, n은 맞는 모든 행의 수다. 행은 output을 projection 순서로 담은 JSON object이고, 그 뒤에 각 join(object, 또는 맞는 행이 없는 left join이면 null)과 각 relation(one relation은 object나 null, many relation은 array 또는 key_by로 key를 정한 object)이 온다. flatten relation은 one으로 돌려준다(1.2절). 값은 parameter value model을 따른다. datetime은 YYYY-MM-DD HH:MM:SS.ffffff, time은 HH:MM:SS.ffffff, decimal과 bytes는 문자열, 정수와 f64는 JSON 수, bool은 JSON bool, date와 uuid는 그 text다." §5 Write는 {affected, keys}와 T88이 정한 affected를 적는다. 각 client의 IR executor 결과 decoder가 이 형태를 읽는다. Go가 먼저이고, 나머지는 fixture를 완료 기준으로 적는 각자의 executor 항목(T63부터 T67)에서 한다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 fixture를 실행한다. T85와 T91이 필요하다.—Evidence: contracts/fixtures/ir_answers.json이 flowmark에서 얻은 IR read 답 13개를 담고, make ir-answers-check가 다섯 client에서 각 답을 listener에 대한 markdown conformance chain으로 실행하며 다섯 모두 같게 decode한다. 모든 client가 이미 그 형태를 decode했으므로 RED는 없고 fixture가 빠진 증명이다. fixture는 integer, f64, bool, decimal, date, null, join과 one, many, null relation을 다루며, corpus에는 time, bytes, uuid, datetime, key_by, flatten 답이 없다. lock으로 되돌린 설치에서 link 없이 검사가 통과한다(dependency-policy-check 포함).[o]
T106WRITE_CONFLICT는 읽은 뒤의 변경으로 transaction의 write가 다른 write가 되는 경우도 포함하며, 다시 시도하지 않는다. 원인: protocol 결함 수정. errors.yaml:54와 protocol.md:197은 WRITE_CONFLICT를 "읽은 뒤 대상이 바뀜"으로만 정의한다. 그러나 commit 때 transaction의 읽기를 다시 실행하는 IR executor(flowmark의 commit 재실행)는 자기 대상이 바뀌지 않았는데도 읽은 뒤의 변경으로 transaction의 write가 다른 write가 되는 경우를 발견하며, 그 경우의 code가 없다. client가 SQLSTATE 40001을 DEADLOCK으로 mapping하고(packages/orm-go/orm/db.go:558, errors.yaml:79) TransactionOptions가 그것을 다시 시도할 수 있으므로 DEADLOCK은 답이 아니다. 바뀐 읽기 위에서 callback을 다시 실행하는 것은 결코 안전하지 않다. 완료 기준: 먼저 red: 공유 결과 fixture contracts/fixtures/ir_answers.json(T105)에, 읽은 값이 바뀌어 WRITE_CONFLICT로 거절된 commit을 더한다. 모든 client는 retry option이 있어도 callback을 다시 실행하지 않고 WRITE_CONFLICT를 돌려줘야 하며, 지금은 모든 client에서 실패한다. 생성된 catalog 주석(cmd/orm-gen이 errors.yaml에서 생성하는 packages/orm-go/orm/codes.go)은 다시 생성하기 전까지 새 errors.yaml text와 다르다. 그다음 errors.yaml:54는 "읽은 뒤 대상이 바뀌었거나, 읽은 뒤의 변경으로 transaction의 write가 다른 write가 됨. file과 line을 적는다. client는 다시 시도하지 않는다"가 되고, 모든 생성 오류 catalog(Go, PHP, Rust, 그리고 T77-4를 통한 TypeScript와 Python)를 그로부터 다시 생성하며, 다섯 client는 이 code를 다시 시도하지 않고 돌려준다. 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 fixture case와 catalog 비교를 실행한다. 문서(EN과 .ko.md): 같은 의미의 protocol.md §5 Errors, 그리고 dialects.md:395(update 행)와 usage.md의 client 오류 표가 이 의미와, DEADLOCK과 달리 다시 시도하지 않는다는 것을 적는다. fixture를 위해 T105가 필요하다.—Evidence: 커밋 1daef989와 5ff5b555(main): 바뀐 read는 commit에서 다시 시도하지 않는다. RED: ir_answers.json에 commit_write_conflict와 commit_deadlock이 추가되었고 Rust는 교착된 commit을 다시 시도했다. GREEN: 여섯 구현이 ir_answers를 통과하고, main에서 make ir-answers-check가 통과한다.[o]
T107begin message가 dialect 표에서 executor가 지킨다고 적은 read-only option을 가진다. 원인: protocol 결함 수정. protocol.md:198은 begin을 read-only field 없는 {op: begin, name?}으로 정의하는데, dialects.md:409는 markdown IR executor에 대해 "readOnly is honoured"라고 적는다. 그래서 client가 그 option을 전할 방법이 없고, 표가 틀렸거나 client가 option을 버린다. 표에서 주장을 빼지 않고 protocol에 field를 더한다. readOnly는 모든 client의 transaction option이고(usage.md:278), read-only transaction 안의 write는 모든 SQL database에서 READ_ONLY이며(config.md:66, usage.md:458), SQLite read-only transaction은 write lock을 잡지 않는다(config.md:25). option을 무시하는 IR executor는 같은 program이 SQL backend에서 거절당하는 write를 받아들이게 되고, 이는 AGENTS가 금지하는 조용한 fallback이다. 완료 기준: 먼저 red: 공유 IR message case(T91의 tests/bind/vectors.json IR message 부분)가 readOnly transaction의 encode된 begin을 {"op": "begin", "read_only": true}로, 그렇지 않은 transaction의 것을 {"op": "begin"}으로 고정하고, contracts/fixtures/ir_answers.json(T105)의 결과 fixture case가 그 안의 write를 READ_ONLY로 거절한다. 지금 그 field를 encode하는 client는 없다. 그다음 protocol.md §5 Session이 {op: begin, read_only?}를 적는다. read_only가 true면 executor는 그 transaction에서 write lock을 잡지 않고 그 안의 모든 write를 READ_ONLY로 거절한다. 각 client의 IR executor message encoder가 readOnly option에서 그 field를 보내고 READ_ONLY를 decode한다(Go가 먼저이고, 나머지는 이 case를 완료 기준으로 적는 각자의 executor 항목 T63부터 T67에서). 여섯 구현(Go, PHP, PHP 확장, TypeScript, Python, Rust)이 각자 자기 package의 test에서 case를 실행한다. 문서(EN과 .ko.md): protocol.md §5 Session, 그리고 field와 READ_ONLY 결과를 적는 dialects.md:409. T91과 T105가 필요하다.—Evidence: 커밋 2abd1fbb(main): begin 메시지가 read-only 옵션을 포함한다. RED: {op:begin, read_only:true}의 공유 case와 READ_ONLY 답 case가 없었다. GREEN: tests/bind/vectors.json의 control case가 다섯 client에서 통과하고 read_only_write는 여섯 구현에서 통과하며, main에서 make bind-check가 통과한다.[o]
T108base64만 또는 gz만 있는 stage 목록은 모든 client에서 같은 byte를 저장한다. 원인: protocol 결함 수정. Rust는 그런 stage 앞에서 값을 PHP-serialize하지만 Go와 TypeScript는 평문으로 저장한다. 그래서 한 client가 쓴 cell을 다른 client가 다른 값으로 decode한다(T74에서 발견). 완료 기준: tests/codec/vectors.json에 stage 하나짜리 목록마다 공유 case를 두고, 어긋나는 client에서 먼저 red를 본 뒤 여섯 구현이 각자 자기 package에서 일치한다. docs/spec/codec.md(EN과 ko)가 어떤 stage 목록이 serialize하는지 적는다. 같은 suite가 stage 하나짜리 목록의 불일치 두 가지를 더 고정한다(T74 검토에서 발견). PHP는 base64 뒤 UTF-8 검사를 건너뛰므로 +/+/와 /w==가 PHP에서는 decode되지만 Go, TypeScript, Python은 거절한다. 또 ["base64"]로 encode한 ""는 빈 cell을 쓰는데, Go, TypeScript, Python, PHP는 그 cell의 decode를 거절하고 Rust는 왕복한다.—Evidence: 커밋 3c81acf3(main): base64만 또는 gz만 있는 stage list는 UTF-8 text를 저장한다. RED: Rust는 값을 직렬화했고 PHP는 +/+/와 /w==를 decode했으며 네 client는 빈 base64 cell을 거부했다. GREEN: 다섯 client가 일치하고 codec-check가 통과한다(630 test).[o]
T110TypeScript와 Python이 요청의 형태를 Go와 PHP만큼 엄격하게 decode한다. 원인: protocol 결함 수정. ref 안이나 최상위의 모르는 member(sum 요청의 column 등)는 Go와 PHP에서 IR_INVALID인데 TypeScript와 Python은 받아들인다. 문자열이 아닌 column은 Go와 PHP에서 IR_INVALID, TypeScript와 Python에서 COLUMN_UNKNOWN이다. 그래서 한 요청이 두 client에서 통과하고 두 client에서 실패한다(T73 검토에서 발견). 완료 기준: tests/ir/validate.json의 형태 case(층마다 모르는 member, 틀린 member type)가 먼저 TypeScript와 Python에서 red이고, 그다음 다섯 요청 검증기가 각자 자기 package에서 일치한다. protocol.md(EN과 ko)가 모르는 member와 틀린 type은 IR_INVALID라고 적는다.—Evidence: 커밋 03df6529, 98a4d667, fb941879(main): request의 shape를 엄격하게 decode한다. RED: TypeScript와 Python이 알 수 없는 member와 정수 slot의 소수 literal을 받아들였다. GREEN: tests/ir/validate.json의 shape case 26개와 소수 literal case가 다섯 client에서 통과하고, TypeScript는 ordered-json으로 decode하며, go test ./packages/go/engine/...이 통과한다.[o]
T109ip encoder는 공백이 있는 입력을 잘라 내지 않고 거절한다. 원인: protocol 결함 수정. encoder들은 ip 입력을 pack하기 전에 둘레의 공백을 지운다. 그래서 서로 다른 두 입력이 같은 byte로 저장되고, 저장된 값이 입력으로 되돌아오지 않는다. T74는 decode 쪽에서 이를 거절한다(T74에서 발견). 완료 기준: 앞, 뒤, 안쪽 공백을 가진 공유 case가 여섯 구현 모두에서 CODEC_ENCODE로 끝난다. 먼저 red. docs/spec/codec.md(EN과 ko)가 이를 적는다.—Evidence: 커밋 c5e1f085(main): ip 인코더가 공백이 든 입력을 거부한다. RED: 모든 client가 공백을 잘랐고 PHP는 NUL에서 코드 없는 ValueError를 던졌다. GREEN: 다섯 client 모두 CODEC_ENCODE로 거부하며 codec-check가 통과한다(690 test).[o]
T111PHP 확장이 자기가 구현하는 dbspec 공유 suite를 실행한다. 원인: protocol 결함 수정. PHP 확장은 dbspec 표면을 구현하지만 schema-definition suite와 tests/dbspec/cases.json의 sets 절에 대한 binding이 없어서, 그 parse와 set 결과가 다른 다섯 구현과 비교되지 않는다(T77의 선언에서 발견). 완료 기준: 확장이 두 suite를 자기 package에서 실행하고, 어긋나면 먼저 red를 보이며, parity 검사가 확장에 대해 빠진 binding을 보고하지 않는다.—Evidence: 바인딩은 commit f0449c01(#T148-3-3)에서 추가되었다. config/parity.json이 schema-definition suite를 packages/php-ext/tests/CoverageSchemaDefinitionTest.php에, dbspec sets를 packages/php-ext/tests/DbspecSetsTest.php::test_sets에 묶는다. 오늘 main에서 make dbspec-php-extension-check/sets가 exit 0(test 7개, assertion 106개), make dbspec-php-extension-check/schema-definition이 exit 0(test 7개, assertion 27개)이다. parity-declarations-check는 extension에 missing binding을 보고하지 않는다. 엄브렐라 target의 연결 누락은 새 행(T188)이다.[o]
T112모든 공유 vector case는 모든 runner가 보고할 수 있는 이름을 가진다. 원인: protocol 결함 수정. tests/codec/vectors.json은 Go subtest가 보고할 수 없는 /로 case 이름을 짓고(json/null), tests/codec/aes-vectors.json에는 case 이름이 없다. 그래서 Go codec binding을 선언할 수 없고, AES case는 key로 이름이 붙는다(T77의 선언에서 발견). 완료 기준: /와 공백이 없는 case 이름, 모든 AES vector의 명시적 name, 여섯 구현의 reader가 이를 따르며, parity 선언이 Go를 codec suite에 binding한다.—Evidence: commit 9797c81c(main, rebase 전 fix 06f7713d): AES vector에 명시적 name(aes.bench_salt)을 더하고 모든 reader(Go, PHP, TypeScript, Python, Rust)가 name을 읽는다. Go reader는 빈 name이나 slash, 공백이 든 name을 거부한다. tests/codec/vectors.json의 / 이름은 4b2f1a25(T148-3-3-1)가 이미 점으로 이었다. Linked red→green: fix를 빼면 go test -run TestCodecAESVectors가 "vector name "" is empty or holds a slash or white space"로 실패하고, fix를 넣으면 그 test와 나머지 reader 네 개가 통과한다.[o]
T113Python은 어느 기계에서나 database datetime을 UTC로 읽는다. 원인: protocol 결함 수정. PyMySQL과 psycopg는 datetime과 timestamp column을 naive 값으로 돌려주는데, _time_form(packages/orm-python/src/polyspec/orm/database.py)이 거기에 astimezone(utc)를 부르고, Python은 그 값을 기계의 local 시각으로 읽는다. 그래서 +0900 기계에서 읽은 updated_ts가 아홉 시간 이르게 돌아온다. 그 결과 write_cycle의 낙관적 update가 어떤 행에도 맞지 않아 MySQL과 PostgreSQL에서 OPTIMISTIC_LOCK으로 실패하고, SQLite(text)는 통과한다. UTC 기계의 CI에서는 드러나지 않는다(T74 검증 중 발견). 완료 기준: conformance runner를 UTC가 아닌 TZ로 실행해 먼저 Python에서 MySQL과 PostgreSQL이 red임을 보이고, 모든 client가 naive database datetime을 UTC로 읽으며, 다섯 client가 그 TZ에서 모든 conformance case에 일치한다. docs/spec/dialects.md(EN과 ko)가 이를 적는다.——[o]
T116PHP는 offset 없는 datetime을 연결 zone으로 bind한다. 원인: protocol 결함 수정. bindTimeValue(packages/orm-php/src/Db.php:902와 :889)는 offset 없는 datetime을 zone 없이 createFromFormat으로 읽으므로 date.timezone 설정을 쓴다. 일광 절약 시간이 있는 zone에서는 그 틈에 든 유효한 UTC 벽시계 값을 거절한다(America/New_York에서 2026-03-08 02:30:00은 CODEC_ENCODE). 다른 client는 그 값을 bind한다(T113 검토에서 발견). 완료 기준: 일광 절약 date.timezone에서의 unit case가 먼저 red이고, 두 자리 모두 연결 zone을 넘기며, 다섯 client가 실행하는 공유 bind vector가 일치한다.—Evidence: commit 4911eda0(main, rebase 전 fix e7648377): packages/php/src/Db.php의 두 createFromFormat 지점(datetime과 date)이 offset이 없는 wall clock을 connection zone에서 파싱한다. 행의 경로는 낡았다. 함수는 packages/php/src/Db.php의 bindTimeValue다. RED(fix 전 unlinked 실행): test_bind_offsetless_datetime_uses_the_connection_zone_under_dst가 Db.php:1192에서 CODEC_ENCODE로 실패한다. GREEN: test_bind_ 통과(test 3개, assertion 97개). 새 case는 단독으로도 통과한다. 공유 vector datetime-dst-gap(tests/bind/vectors.json)이 다섯 client의 bind-check에 있다. 연결된 red→green: fix를 빼면 case가 다시 red가 된다. 남은 차이: DST gap이 있는 connection zone은 PHP가 여전히 거부한다(T116-1).[o]
T116-1연결 zone의 일광 절약 시간 gap에 든 offset 없는 datetime을 모든 client가 거부한다 (T116에 이어). 원인: T116은 connection zone을 넘기지만, UTC가 아닌 zone(America/New_York, 2026-03-08 02:30:00)의 gap wall clock은 존재하지 않는 시각이다. PHP는 CODEC_ENCODE로 거부하고, Go는 조용히 바로잡는다(무대체 규칙 위반). RED: Go가 그것을 바인딩한다. 완료 조건: 공유 bind vector에 UTC가 아닌 zone과 gap 시각 case를 두고, 다섯 client 모두 slot 이름과 함께 CODEC_ENCODE를 기대한다. 모호한(overlap) wall clock도 같은 vector에서 확인하며, 임의로 하나를 고르는 client는 실패한다.—근거: 도달 불가로 닫고 구현하지 않는다: docs/spec/dialects.md("Time zone decision")의 정해진 결정에 따라 모든 연결은 UTC이고, DSN은 다른 zone을 거부하므로(packages/go/orm/dsn_test.go), 연결 zone에 일광 절약 시간 gap이나 overlap이 생길 수 없다. bind vector도 +00:00 zone을 요구한다(packages/go/orm/markdown_param_test.go, bind_check_test.go). 앞선 gap 결정은 이 규칙을 보지 않고 내려졌으며, owner가 UTC 규칙 (a)를 골랐다.[o]
T117Python이 MySQL과 PostgreSQL의 time과 date column을 읽는다. 원인: protocol 결함 수정. PyMySQL은 TIME을 timedelta로, psycopg는 datetime.time으로 돌려주는데 _time_form(packages/orm-python/src/polyspec/orm/database.py:161-175)은 둘 다 다루지 않는다. read_time은 오류 메시지를 json.dumps로 만들어서, 읽기가 OrmError가 아니라 TypeError로 끝난다. bench schema에 time이나 date column이 없어 어떤 conformance vector도 이를 덮지 않는다(T113 검토에서 발견). 완료 기준: bench schema에 time과 date column을 두고, 모든 runner가 세 database에서 실행하는 conformance vector가 먼저 Python에서 red이며, Python이 둘을 읽고, 오류 메시지가 값을 표시하다 실패하지 않는다.—Evidence: commit d788f2cd(main, ff-merge됨; rebase 전 2e2a0b19): Python이 MySQL과 PostgreSQL의 time과 date 셀을 읽는다(timedelta와 datetime.time을 native time reader로 처리). RED: test_values.py NativeTimeReadTest가 subtest 4개에서 CODEC_DECODE로 실패. GREEN: 테스트 23개 통과. bench 스키마에 day_cell(label, day date, at_time time(0))이 추가되고, 다섯 runner에 vector date_roundtrip과 date_null이 있다. conformance 기록은 다섯 언어에서 vector 50개가 같고, 깨끗한 재실행도 통과한다(make run-databases TARGETS=conformance-check exit 0). date setter는 text를 받는다(T189). rebase된 tree에서 사전 검사가 통과한다.[o]
T118모든 구현의 test는 kit의 test runner로, 각 도구의 기본 출력으로 실행한다. 원인: kit의 run-tests와 그 parity 검사(kit K47)는 각 도구의 기본 실행 출력을 읽는다. node:test TAP, go test JSON, cargo test, PHPUnit TeamCity, Python unittest가 그것이다. 그런데 orm의 test는 자체 harness 줄로 보고하고, orm의 PHP test는 PHPUnit을 쓰지 않는다. 그래서 orm만 다른 polyspec 저장소와 공유하지 않는 두 번째 test 인터페이스를 가진다. 완료 기준: 각 구현의 test와 parity binding이 기본 도구로 run-tests 아래에서 실행되고(PHP는 고정한 PHPUnit), harness 줄 형식은 지운다. orm은 K47이 든 kit release를 sync하고, parity-check가 모든 binding을 읽는다. 먼저 red: K47의 parity 검사가 orm의 binding을 실행되지 않음으로 보고한다.——[o]
T118-1Go test가 kit의 runner로 실행된다. 원인: Go test(T118 red 중 114건)가 config/parity.json에 선언되지 않았고 packages/go/testcase의 harness 줄로 보고한다. 수용: 모든 Go test 파일이 선언되고, 출력은 kit의 runner가 읽는 native go test JSON이며, make -s parity-declarations-check가 Go의 미선언·위치 오류 test를 보고하지 않는다. red: parity-declarations-check의 Go 미선언 test 파일 114건(R5, go).—근거: commit dc992eec(main, ff-merge됨; rebase 전 582bc8a7): docs/spec, config/parity.json, Go feature unit 118개. RED: make -s parity-declarations-check가 Go finding 133건(선언되지 않은 test 114, 잘못 놓인 test 19)과 R10 문서 수 6줄을 보고한다. GREEN: Go finding 0건, R10 0줄, gate 17개 exit 0. 벤치마크(packages/go/bench)와 분석기(tests/analyzers)는 tools에 있다. 저장소 전체 parity-check는 MySQL date_roundtrip(Go와 Rust) 때문에 빨간불이며, 이는 이 행이 아니라 main 결함으로 T121-1에서 추적한다.[o]
T118-2Rust test가 kit의 runner로 실행된다. 원인: Rust test 파일 106건이 선언되지 않았고(R5, rust) packages/rust/testcase의 harness로 보고한다. 수용: 모든 Rust test가 선언되고, 출력은 kit이 읽는 cargo test의 native 출력이며, parity-declarations-check에 Rust finding이 없다. red: 선언되지 않은 Rust test 파일 106건.—근거: commit 6f067f5d(main; rebase 전 cee962d6과 7ce2e0c0의 행 commit들): make -s parity-declarations-check에서 Rust R5와 R6이 0이다. rust-check(check, clippy, clippy-live-db, clippy-test-faults)가 통과한다. insert_email_is_an_aes_hex_envelope의 바인딩은 kit runner가 실행하는 형태로 돌렸다. benchmarks/rust에서 cargo test --locked --offline --bin native insert_email_is_an_aes_hex_envelope -- --test-threads=1이며, 통과 줄은 test tests::insert_email_is_an_aes_hex_envelope ... ok, test result: ok. 1 passed; 0 failed이다(exit 0, DB 불필요).[o]
T118-3Python test가 kit의 runner로 실행된다. 원인: Python test 파일 89건이 선언되지 않았고(R5, python) packages/python/tests/testcase.py로 보고한다. 수용: 모든 Python test가 선언되고, 출력은 kit이 읽는 Python unittest의 native 출력이며, parity-declarations-check에 Python finding이 없다. red: 선언되지 않은 Python test 파일 89건.—근거: commit 9d8a5abb, e39f7d42, 11910889 (main; rebase 전 74a34236, 99ae86a7, ccfbd1ba): Python coverage 프로그램이 unittest class다(메서드 34개, 각각 resolver 항목을 subTest(db=...)로 도는 python-feature unit). make -s parity-declarations-check에서 Python R5는 전 15건에서 0건이 되었다(전: 15). python-unit-check와 lint-python이 통과한다. 15개 feature의 owner/python targeted 실행은 main과 같은 PASS 집합(각 30줄)이다. worktree에서 make install-php 후 interface-check와 client-unit-check가 통과한다. rust-fmt-check는 main에서도 exit 2다(이 행의 것이 아님). feature-unit-check/owners도 main에서 실패한다(T193).[o]
T118-4PHP test가 kit의 runner와 고정된 PHPUnit으로 실행된다. 원인: PHP test 파일 73건이 선언되지 않았고(R5, php) tests/testcase.php가 harness이며, 많은 PHP test가 이미 packages/php의 composer manifest와 lock에 고정된 PHPUnit으로 실행된다. 수용: 모든 PHP test가 선언되고, 남은 non-PHPUnit test는 그 고정된 PHPUnit으로 옮겨지며, 출력은 kit이 읽는 PHPUnit TeamCity 출력이다. red: 선언되지 않은 PHP test 파일 73건.—근거: commit ca7a5f6a(main; 8602f233를 rebase): config/parity.json 충돌은 python-internal과 php-internal 단위를 모두 남겼다. JSON은 유효하고 단위 id 중복은 없다. PHP R5는 전에 31이었고 php 구현 기준 지금 0이다(make -s parity-declarations-check; 남은 R5 6건은 php-extension이며 T118-6 소관). PHP R6은 0이다. server를 띄운 targeted PHPUnit: ModelMethodsTest 3건, SchemaSetTest 16건, DecimalModelDbTest 9건, ModelTest 69건이 통과한다(1건 skip). 전체 PHP suite는 사용자 규칙에 따라 최종 배치에서 검증한다. feature-unit-check/owners는 T193 경로에서만 실패한다(main도 동일).[o]
T118-5TypeScript test가 kit의 runner로 실행된다. 원인: TypeScript test 파일 62건이 선언되지 않았고(R5, typescript) tests/testcase.mts로 보고한다. 수용: 모든 TypeScript test가 선언되고, 출력은 kit이 읽는 node:test TAP이며, parity-declarations-check에 TypeScript finding이 없다. red: 선언되지 않은 TypeScript test 파일 62건.——[o]
T118-5-1feature resolver가 각 coverage test에 자기 database와 DSN을 준다. 원인: coverage runner가 항목마다 ORM_FEATURE_DATABASE와 ORM_FEATURE_DSN을 주입하고(scripts/features/coverage.mts:609-612), 각 DSN을 process.env[command.dsn_env]에서 읽는다(:573). 그래서 feature 바인딩 하나가 자기 database를 정할 수 없고, TypeScript coverage 파일 약 30개가 하나의 env에서 R3로 실패한다. 수용: scripts/features/coverage.mts가 test 파일의 coverage 항목에 대한 database와 DSN을 manifest에서 돌려주는 resolver를 내보내고, 각 coverage test가 시작할 때와 feature-check runner가 이를 호출하며, feature-check는 env 주입을 멈추고, go, php, python, rust feature reader도 같은 resolver를 쓰며, feature-check 결과는 그대로다. red: 하나의 env에서 R3 실패 약 30건. green: 통과하고 feature-check 결과가 이전과 같다.—근거: commit 2c3706d1과 5cb177d3(main; rebase 전 0ba6cb99와 199dda42): coverage resolver가 각 coverage test 파일에 manifest 항목의 database를 준다(파일 단위 실행, database별로 나눔, CASE <id> [<db>] PASS). Python bare case를 호출하고, feature-check 결과는 기준선과 같다. 결정 25, CASE … PASS 파서와 각 파서를 증명한 검사: (1) scripts/features/coverage.mts의 observedCases, feature coverage의 유일한 파서이며 scripts/features/coverage.test.mts와 12개 feature의 owner/typescript, owner/python targeted 실행(part 24개, 각각 1 contracts executed)으로 증명; (2) scripts/features/coverage.test.mts fixture, 갱신 후 실행(18 통과, 이번 변경 밖의 기존 실패 3건); (3) packages/python/tests/coverage_case_test.py, 실행(5 통과); (4) packages/ts/tests/coverage-case-unit.mjs, 실행(통과); (5) scripts/group-rows-physical-check.mts:32, feature coverage 밖의 정확한 일치이며 변경하지 않음; (6) kit에는 CASE 파서가 없고 tests/kit/parity.test.mts의 CASE_DIRECTORIES 상수만 있다. Go, PHP, Rust는 각자의 형식을 읽는다. tree의 전체 feature-check: FC_EXIT 2, PASS 348, FAIL 11(T191의 schema_definition 실패만, 깨끗한 main 3417edde에서 재현).[o]
T118-5-2TypeScript test가 선언되고 kit의 runner로 실행된다. 원인: TypeScript test 파일 62건이 선언되지 않았고(T118-5), 약 33건이 harness runCase를 직접 호출해서 이름 있는 case를 보고하지 않는다. 수용: runCase 호출 17곳이 native node:test case가 되고, ts-internal 프로필이 docs/spec/parity.md#implementation-internal-tests를 인용하며, TypeScript test 하나마다 바인딩하는 feature unit 62개가 있고, case를 보고하지 않는 helper 파일은 support glob이며, make -s parity-declarations-check와 make -s parity-check가 TypeScript finding을 보고하지 않는다. T118-5-1을 기다린다. red: 선언되지 않은 TypeScript test 파일 62건.—근거: commit bcd31484(main의 선언, rebase 전 30897c45와 11964059): TypeScript R5는 WIP 상태에서 26, 지금 0이다(행 전 HEAD에서 62). packages/ts R6은 0이다(type-only fixture 두 개를 decimal_model.types.ts와 group_rows.types.ts로 바꾸고 tsc 입력에 남겼으며 feature scope 경로를 고쳤다). npm exec --workspace packages/ts -- tsc -p tsconfig.json --noEmit이 exit 0이다. targeted node test는 46개 unit 중 27개가 통과한다. DB에 의존하는 19개 unit은 최종 배치에서 검증한다. feature-unit-check/owners는 T193 경로에서만 실패한다(main도 동일). R7(T77-4)과 R10(T118-9)은 이 행 밖이다. 근거: 모든 하위 행이 닫혔다(T118-1부터 T118-9까지, T118-5-1, T118-5-2). main의 근거: make -s parity-declarations-check에서 R6 0, R4 0이며 각 runner의 자체 test가 선언된 unit을 갖는다(남은 R5 5건은 runner가 아니라 T169-4의 plan cache 파일이다). K47 parity 읽기는 각 계층의 native 출력이다(T118-7).[o]
T118-6PHP extension test가 kit의 runner로 실행된다. 원인: php-extension test 파일 6건이 선언되지 않았다(R5, php-extension). 수용: 선언되고, 출력은 kit이 읽는 native 출력이다. red: 선언되지 않은 php-extension test 파일 6건.—근거: commit fe1e7790(main; php-extension 선언): make -s parity-declarations-check에서 php-extension R5가 6에서 0이 되었다. php-extension-internal profile(사유와 spec anchor docs/spec/parity.md#implementation-internal-tests)이 PHPUnit test method 16개를 바인딩하며, bootstrap과 declarations helper는 support다. targeted: DbspecNamesTest 9건, DbspecApplyCleanupTest 5건 통과. DeclarationsTest의 test_mirrors는 실패한다(이 행이 아니며 T195로 등록). DbspecApplyTest는 server가 필요해서 최종 배치에서 검증한다. documents-check, docs:rules-check, layout-check가 통과한다.[o]
T118-7harness 줄을 모든 test 계층에서 없애고 T24 check를 다시 쓴다. 원인: tests/testcase.mts, packages/go/testcase, tests/testcase.php, packages/python/tests/testcase.py, packages/rust/testcase의 RUN, STEP, PASS, FAIL harness 줄이 두 번째 test interface이며, T24 testcase-check가 그 형식을 검사한다. 수용: 어느 test 계층도 harness 줄을 출력하지 않고, T24 check가 native 출력을 검사한다. red: 한 계층에서 harness 줄을 없애면 T24 testcase-check가 형식에서 실패하고, parity check는 여전히 그 줄을 읽는다.—근거: commit feaeb955(main; 03225869를 7ad390a6 뒤로 rebase): 다섯 test 계층이 native 출력(node:test TAP, go test 이벤트, unittest 줄, libtest)을 내고, T24 check가 그것을 검사한다. 계층별 red 후 green: Node scripts/harness/testcase.test.mts exit 1 후 6 of 6; Go packages/go/testcase --- FAIL: TestReportForm 후 ok; Python 5개 중 3 실패 후 4 통과(남은 1건은 T169-4의 test_plan_cache_bound.py); Rust report_form FAILED 후 6 passed; runner 11개 중 1 실패 후 11 통과. main에서 documents-check, docs:rules-check, harness test 3개 파일(17 통과)이 통과한다. scripts/check/report.mts는 check runner의 보고에서 harness 줄을 아직 파싱한다. 이는 test 계층이 아니라 check runner이므로 별도 행으로 추적한다.[o]
T118-8잘못 놓인 test와 선언되지 않은 suite를 선언한다. 원인: parity-declarations-check에서 test 76건이 잘못 놓였고(R6), suite 4건이 선언되지 않았다(R4). 수용: 잘못 놓인 test는 소속 feature로 옮겨지고, suite는 선언되며, check에 R4와 R6 finding이 없다. red: R6 76건과 R4 4건.—근거: commit 592f2f74(main의 선언, rebase 전 0f30300e; layout 수정은 세 harness test를 이미 있는 도구 root인 scripts/harness로 옮겼고 S7 규칙은 그대로다): make -s parity-declarations-check에서 R6 misplaced-test가 29에서 0, R4 undeclared-suite가 4에서 0이다. scripts, packages/dbspec-*, tests 도구 root는 사유를 가진다. vector 파일 네 개가 suite(ir-answers, mysql-session, dbspec-set-decimal, dbspec-set-bench)다. 옮긴 harness test 세 개는 자체 단위 test 17개가 통과한다. layout-check, documents-check, docs:rules-check가 통과한다. suite의 case id는 최종 배치에서 검증한다(행별 R3은 돌리지 않는다). R5 php-extension, R7, R10은 다른 행이다.[o]
T118-9문서의 test 수가 선언과 맞는다. 원인: R10 finding 6건이 docs/features.md와 docs/operations/usage.md의 test 수가 선언된 test와 다르다고 보고한다. 수용: 수는 선언에서 생성되거나 바로잡히며, EN과 KO 문서가 모두 맞고, check에 R10 finding이 없다. red: R10 finding 6건. R7 error catalog finding은 T77-4의 몫이므로 여기서 다시 적지 않는다.—근거: commit d7a298f5(main; docs/feature-coverage.md와 .ko.md의 개수 문장을 나눠 각 목록이 선언된 profile을 가리키게 했고 KO는 다시 stamp): make -s parity-declarations-check의 R10 docs-count가 전 2, 후 0이다(main도 2라서 행 본문의 이전 '6'은 낡은 값이었고, 실제 2건을 고쳤다). worktree에서 make install-tools와 make install-packages 후 documents-check, docs:rules-check, layout-check가 통과한다. 선언 검사만 돌렸다(suite와 server 없음). R7은 T77-4에 남는다. 근거: 모든 하위 행이 닫혔다(T118-1부터 T118-9까지, T118-5-1, T118-5-2, 방금 닫은 T118-5). main의 근거: make -s parity-declarations-check에서 R6 0, R4 0이며 각 runner의 자체 test가 선언된 unit을 갖는다(남은 R5 5건은 runner가 아니라 T169-4의 plan cache 파일이다). K47 parity 읽기는 각 계층의 native 출력이다(T118-7).[o]
T119dbspec의 store setting이 시간 shard 절을 받는다. 원인: orm이 이미 정의한 setting의 protocol 결함 수정. store(T62)는 여섯 구현이 모두 parse하고 manifest hash에 넣는다. 그런데 polyspec/flowmark의 T12.1-35는 어떤 행도 소유하지 않고 시간에 따라 쌓이는 table(event log)이 시간 shard를 선언하기를 요구한다. 어떤 SQL backend도 이를 render하지 않지만, 모든 구현이 똑같이 parse, emit, hash해야 한다. 완료 기준: 절은 non-null date나 datetime column과 기간(year, month, day)을 적는다. parse, emit(emit(parse(s)) == s), setting 오류(nullable이거나 시간이 아닌 column, parent나 order를 가진 table, block table)와 manifest hash를, 여섯 구현이 각자 자기 package에서 make dbspec-compare-check로 통과하는 tests/dbspec/cases.json의 공유 case로 고정한다. docs/dbspec.md와 그 twin이 SQL backend는 이를 무시한다고 적는다. 여섯 구현 모두 먼저 red.——[o]
T120행을 identity 값과 함께 적재하고, compile된 statement를 공개 호출로 실행한다. 원인: backend 공통 표준. 이미 있는 행을 database에 적재하는 일(polyspec/flowmark의 export, T12.7)은 identity key를 지켜야 한다. 그런데 planner는 지정한 identity 값을 거절하고("cannot set identity column"), orm에는 PostgreSQL identity를 적재한 key 너머로 옮기는 statement가 없으며, Go client는 compile된 statement의 parameter를 비공개 (*DB).args에서만 bind한다. 완료 기준: identity 값을 지정하는 insert의 적재 방식을 두고, 적재 뒤 MySQL, PostgreSQL, SQLite에서 각 identity를 가장 큰 key 너머로 다시 시작한다. 모든 client가 compile된 statement를 parameter와 함께 실행하는 공개 호출을 가진다. 여섯 구현이 각자 자기 package에서 실행하는 공유 case가 먼저 red다. 문서(EN과 ko)를 둔다.—Evidence: main에서 이미 충족됨을 조항별로 확인했다(main c1fb4d3e): (a) load mode의 insert가 identity 값을 대입하고 일반 insert는 거부한다: packages/go/engine/planner/planner.go:1058-1065, Go에서 load_insert, load_insert_rows, load_needs_identity_value, insert_refuses_identity 통과; (b) MySQL, PostgreSQL, SQLite에서 가장 큰 key 뒤로 restart: planner.go:185와 restartIdentityStep(planner.go:1449), execute_load_then_restart와 execute_restart_after_delete가 세 driver에서 통과; (c) 모든 client에 컴파일된 statement를 매개변수와 함께 실행하는 공개 execute: 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) contracts/fixtures/identity_load.json의 공유 case 16개가 각 package에서 실행된다: Go TestIdentityLoad, Python python-identity-load-check(test 16개 OK), TypeScript identity-load.mjs(16 통과), Rust identity_load(16 통과), PHP IdentityLoadTest(test 16개, assertion 93개). 구현은 606510f7과 bbf4b7d2(T120 자체)에서 들어갔고, test는 229dc8aa와 964f9949다. parity는 이 suite에 다섯 구현을 나열하며, 행의 여섯 번째는 PHP extension이고 query 계층은 T182 작업이다.[o]
T121모든 client가 toArray에서 시간 값을 한 형식으로 쓴다. 원인: protocol 결함 수정. Go와 PHP는 toArray에서 date나 datetime을 YYYY-MM-DD HH:MM:SS.ffffff로 쓰고, Python과 TypeScript는 value-model 텍스트 YYYY-MM-DD를 그대로 둔다. 그래서 같은 행이 client마다 다르게 비교된다. markdown executor의 relation_many case는 README 주석 없이는 Python에서 통과하지 못했다(T65에서 발견). 완료 기준: protocol이 toArray의 date, datetime, time마다 시간 형식을 하나로 정한다. 모든 client가 자기 package에서 실행하는 공유 vector가 어긋난 client에서 먼저 red다. 문서는 EN과 ko로 쓴다.—Evidence: commit ae1e180d(main, ff-merge됨): toArray가 datetime과 time을 모든 client에서 열 정밀도와 무관하게 소수 여섯 자리로 쓴다(protocol 5절). RED: time_p3_roundtrip과 datetime_p0_roundtrip이 여섯 자리 기대값에 대해 다섯 client 모두 DIFF다(conformance-check exit 2). GREEN: make run-databases TARGETS=conformance-check가 exit 0이고 48개 vector가 다섯 언어에서 같다. Rust ir-answers는 아직 컴파일되지 않는다(packages/rust/tests/tests/markdown_conformance.rs:374의 E0308). 이는 T190을 기다린다.[o]
T121-1MySQL date_roundtrip이 모든 client에서 통과한다. 원인: conformance vector date_roundtrip이 MySQL에서 Go와 Rust에서 실패한다(MySQL이 vector가 기대하지 않는 시간 형식을 돌려주며, 50개 중 49개 통과); 이는 main의 빨간불이며 저장소 전체 parity-check를 막는다(T118-1에서 발견). 수용: MySQL, PostgreSQL, SQLite에서 make run-databases TARGETS=conformance-check가 다섯 runner 모두 exit 0이고 50/50이다. 원인은 vector를 도입한 commit이나 그 읽기 경로에서 찾고, protocol 5절(시간은 소수 여섯 자리)과 어긋난 쪽을 고친다. red: Go와 Rust가 MySQL에서 date_roundtrip을 실패한다. green: 세 데이터베이스에서 다섯 runner 모두 50/50.—근거: commit 3417edde(main; be86ab39의 vector 수정을 rebase): protocol의 여섯 자리 시간 형식이 정식이므로(5절) vector date_roundtrip이 틀린 쪽이었다. 다섯 runner는 이미 10:11:12.000000을 쓴다. RED: make run-databases TARGETS=conformance-check가 exit 2이며 date_roundtrip이 MySQL, PostgreSQL, SQLite에서 다섯 runner 모두 DIFF다(runner당 50개 중 49개 통과). GREEN: 같은 target이 exit 0이며 세 데이터베이스에서 다섯 runner 모두 50/50이다.[o]
T122어떤 행과도 맞지 않는 markdown group_count가 빈 결과를 돌려준다. 원인: 맞는 행이 없으면 Go markdown executor의 validateGroupColumns가 column을 보지 못해 답이 INTERNAL: group result has no row_count로 실패한다. PHP client에도 같은 결함이 있었고 T64가 그쪽을 고쳤으며, 그 request_message case가 먼저 이 결함으로 red였다(T64에서 발견). 완료 기준: 어떤 행과도 맞지 않는 group_count의 공유 case를 tests/markdown/cases.json에 두고 Go에서 먼저 red다. 모든 client의 응답이 같다.—근거: commit bd6aab37(main, ff-merge됨; rebase 전 3c99b41c): 어떤 행과도 맞지 않는 markdown group_count가 빈 결과를 돌려준다. RED: group_count_no_rows가 Go와 Python에서 {"error":"INTERNAL"}을 돌려줬다(결과가 없는 첫 행의 출력을 읽었다). GREEN: make -s markdown-conformance-check가 go, php, python, rust, typescript에서 exit 0이다(새 case는 []를 기대한다). Go와 Python reader는 출력을 요청에서 읽는다.[o]
T123install-python이 wheel lock을 hash와 함께 설치한다. 원인: T115는 uv를 손으로 실행해 packages/orm-python/pylock.toml(hash가 있는 package 9개)을 썼다. 그런데 install-python은 여전히 고정 package를 hash 없이 설치하고, pyproject.toml이나 constraints가 바뀔 때 lock을 다시 쓰거나 검사하는 것이 없다(T115에서 발견). 완료 기준: kit 0.0.26(그 K54) 뒤에 make python-lock이 pyproject.toml과 constraints로 lock을 쓰고, offline 검사가 자기가 쓸 내용과 다른 lock을 거절하며, lock이 kit의 pythonLocks에 들어 있고, install-python이 --require-hashes로 그 lock에서 설치한다. 낡은 lock에서 먼저 red다. 문서(EN과 ko)가 이를 적는다.—근거: commit f83d2636(main, ff-merge됨): install-python이 hash가 있는 lock으로 설치한다(pip install --require-hashes -r packages/python/pylock.toml, 이어서 package를 --no-deps로 설치). lock은 packages/python/pylock.toml이다(행의 packages/orm-python 경로는 낡았다). RED: make -s python-lock-check가 exit 2('declares no pythonLocks'), constraints.txt의 낡은 pin(rapidyaml 0.15.2를 0.15.1로 바꾼 것)이 exit 2로 pylock.toml과 constraints.txt를 가리키며, 새 설치 test가 이전 Makefile에서 실패한다(--require-hashes 없음). GREEN: python-lock-check가 exit 0('1 lock(s) match their sources'), 설치 test가 통과하고, python-unit-check는 test 1328개가 통과한다. test extra(jsonschema-rs)는 lock에 없어 hash 없이 설치된다. 이를 hash로 고정하려면 후속 행이 필요하다.[o]
T125모든 client가 연결 handle에서 request hook을 받는다. 원인: 연결의 모든 request에 같은 predicate, assignment, 매개변수를 더하려는 program은 model 호출마다 고쳐야 했고, client에는 plan 전에 그 일을 할 일반적인 자리가 없었다. 완료 기준: Go, PHP, Rust, TypeScript, Python의 withRequestHook은 같은 연결(pool이나 PDO, cache, subscriber, transaction frame)의 handle을 돌려주고, 그 hook은 읽기, count, page, insert(행 하나와 여러 행), update, delete, restore, getQuery의 각 request를 plan 직전에 받으며, client의 매개변수 추가 호출로 매개변수를 더하고, 기존 planner가 검증한다. handle의 행과 transaction은 hook을 지킨다. client는 hook에 의미를 붙이지 않는다. docs/operations/usage.md "Request hook"(EN과 ko)이 이를 적는다. contracts/fixtures/request_hook.dbs의 공유 SQLite case(without_hook, read_filter, insert_assign, loaded_row_writes, invalid_param)가 모든 client에서 먼저 red로 실행된다. PHP extension은 query 연결이 없으므로 hook이 없다. 증거(local, SQLite. MySQL과 PostgreSQL은 CI에서 실행): RED: TypeScript node packages/orm-npm/tests/request_hook.mjs는 5개 중 5개가 db.withRequestHook is not a function으로 실패하고, Go go test ./packages/orm-go/orm -run '^TestRequestHook$'는 db.WithRequestHook undefined로 build에 실패하며, PHP php packages/orm-php/tests/request_hook_test.php는 5개 중 5개가 Call to undefined method Polyspec\Orm\Db::withRequestHook()로 실패하고, Rust cargo test -p polyspec-orm --test request_hook는 E0599 no method named with_request_hook로 build에 실패하며, Python unittest -p test_request_hook.py는 'Db' object has no attribute 'with_request_hook' 오류 5개다. GREEN: 같은 다섯 명령이 각각 case 5개를 모두 통과한다. 다섯 snapshot과 hash를 --record로 다시 기록한 뒤 make interface-check가 통과하고(manifest, go, php, rust, typescript, python, php-extension), go test ./contracts와 npm run typescript:check가 통과한다. server 없는 TypeScript test(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)가 통과하고, PHP dsn 13, markdown 6, relation_keys 1, hostcodec 1, engine 1, runtime_model 9, orm_gen 6, perf_extensions 2가 통과한다. Go go test ./packages/orm-go/...는 최상위 test 101개가 통과하고 server DSN이 필요한 71개만 실패한다. Python unit discovery는 135개 중 129개가 통과하고, 5개는 server DSN이 필요하며, test_markdown은 main이 추적하지 않는 polyspec.orm.markdown을 import하지 못한다. Rust cargo test -p polyspec-orm --features test-faults --lib는 29개가 통과하고 11개는 server DSN이 필요하며, cargo clippy -p polyspec-orm --lib --test request_hook -- -D warnings는 깨끗하다.——[o]
T125-3개발 규칙이 표준 도구의 자기 동작을 받아들인다. 원인: AGENTS.md가 모든 심볼릭 링크를 금지했으므로 Composer가 path repository packages/orm-php에 만드는 심볼릭 링크가 이 저장소가 만든 것이 아닌데도 규칙을 어겼고, 규칙은 경로를 정하는 방법을 적지 않았다. 완료 기준: AGENTS.md와 AGENTS.ko.md가 이 저장소가 만드는 것에 심볼릭 링크를 금지하고, 각 생태계의 표준 도구(Python interpreter, 환경, lock은 uv, PHP 의존성은 Composer)를 그 동작대로 쓰게 하며, 경로를 선언적으로 또는 발견으로 정하고 상대 경로로 정하지 않게 한다.—증거: make documents-check가 39쌍으로 통과하고 make docs-rules-check가 통과한다.[o]
T125-4request hook이 database coverage가 있는 contracts/features.json의 기능이다. 원인: T125는 hook을 SQLite에서만 검증했고, language-parity 검사는 언어의 모든 test file이 기능에 속하기를 요구한다. 완료 기준: 기능 request_hook이 Go, PHP, Rust, TypeScript owner를 MySQL, PostgreSQL, SQLite의 공유 case 다섯 개로 선언하며, 각 case는 자기 case database에서 실행되어 검사하는 database가 바뀌지 않고, unit 명령을 검증으로 둔다. coverage checker에 Python runner가 없으므로 Python은 planned이고 그 unit test는 make python-unit-check에서 실행된다.—증거: node scripts/features/check.mjs가 기능 contract 19개로 통과하고, scripts/check/databases.sh create의 환경에서 node scripts/features/coverage.mjs --feature request_hook이 24번의 실행(go, php, rust, typescript × mysql, postgres, sqlite × 2)을 같은 전후 상태로 통과한다.[o]
T125-7make python-unit-check가 server 없는 Python test만 실행한다. 원인: identity load test의 이름이 test_identity_load.py였으므로 unit discovery unittest discover -p 'test_*.py'가 server 환경 없이 그 MySQL과 PostgreSQL case를 실행해 case 다섯 개가 실패했고, make python-identity-load-check는 같은 file을 그 환경으로 실행한다. T78-2는 같은 이유로 statement fault test를 statement_fault.py로 이름 지었다. 완료 기준: file은 packages/orm-python/tests/identity_load.py이고 Makefile, config/parity.json, contracts/features.json이 그 이름을 적는다.—증거: RED: make python-unit-check가 test 139개를 실행하고 ORM_TEST_MYSQL_DSN is required for the mysql identity load cases로 5개가 실패한다. GREEN: test 138개가 OK이고, make python-identity-load-check가 SQLite, MySQL, PostgreSQL에서 case 6개를 통과하며, node scripts/features/check.mjs가 통과한다.[o]
T125-8request hook이 make check의 저장소 검사를 통과한다. 원인: T125의 전체 검사가 빈틈 세 개를 찾았다: Go request hook test가 case를 internal/testcase로 시작하지 않았고(make repo-check), Python interface test는 연결과 도구 rule을 14개로 셌는데 Db.withRequestHook이 15번째이며(make python-check), docs/features.md가 기능 request_hook을 적지 않았다(make feature-unit-check). 완료 기준: 각 Go test가 testcase.Start로 case를 시작하고, Python test가 rule 15개를 세며, 기능 문서를 다시 생성한다.—증거: RED: make check에서 repo-check가 case를 internal/testcase로 시작하지 않는 test 여섯 개로, python-check가 AssertionError: 15 != 14로, feature-unit-check가 generated document is stale: docs/features.md로 실패한다. GREEN: node scripts/repo/check.mjs와 make python-check가 통과하고 make feature-docs가 기능 행 19개를 쓴다.[o]
T132Go의 model_generation coverage case가 기한 안에 끝난다. 원인: TestCoverageModelGenerationCheck는 orm-gen --check를 제자리와 module의 새 복사본에서 실행하는데, Go generator scan은 찾은 build-tag 설정(model 패키지의 featurecoverage)마다 읽을 모든 패키지를 다시 읽었고 복사본에서는 go list -export가 그것을 다시 compile했다. 그래서 전체 묶음에서 두 번째 실행이 coverage checker의 120초 기한을 넘었다(model_generation/owner/go/none run 2: timeout after 120000 ms, 두 번). 완료 기준: 기본 설정은 읽을 모든 패키지를 한 번 읽고, build-tag나 platform 설정은 그 파일을 가진 패키지만 읽으며(generator/gen_go_scan.go), 생성하는 model은 같다. docs/spec/dsl.md(EN과 ko)가 이를 적는다. 기한은 바꾸지 않는다.—증거: RED: 새 TestScanLoadsTaggedConfigurationsOnlyForTheirPackages가 the integration configuration loads ["./..."], want only ["<dir>/tagged"]로 실패한다. GREEN: 그 test와 TestScanGeneratesUsedMethods(tag와 GOOS)가 통과하고 go test ./generator가 통과한다. load average 40-60에서 coverage case node scripts/features/coverage.mjs --feature model_generation --part owner/go는 전에 run 1 1m38s, run 2 1m14s였고 뒤에 31.7초와 33.6초다. model 패키지의 orm-gen --check는 CPU를 전에 27초, 뒤에 21초 썼다.[o]
T115-1Python package가 자기가 test되는 Python을 선언한다. 원인: packages/orm-python/pyproject.toml은 requires-python >=3.11을 선언하고 CI leg 하나가 3.11을 실행한다. 그런데 의존하는 polyspec-ordered-json 0.0.8이 3.14를 요구해서, package를 3.11–3.13에 설치할 수 없다. ordered-json의 T1.32-2 규칙은 requires-python이 test하지 않은 하한이 아니라 CI가 test하는 minor(고정한 .python-version)를 적는다는 것이다(T115에서 발견). 완료 기준: requires-python을 .python-version과 비교하는 test가 >=3.11에서 먼저 red다. requires-python >=3.14로 바꾼다. 3.11 CI leg와 그 repo-check test, ci.yml 주석을 지운다. README와 문서(EN과 ko)가 지원하는 Python을 적는다. 근거(로컬): RED: python3.11 -m unittest discover -s packages/orm-python/tests -p 'test_requires_python.py'가 하한 >=3.11에서 AssertionError: '>=3.11' != '>=3.14'로 실패한다(test 1개). GREEN: requires-python = ">=3.14"로 바꾼 뒤 같은 test가 통과한다(test 1개 OK). make repo-check/unit은 test 89개 중 87개가 통과한다. Python matrix case와 leg suffix는 사라졌다. 실패한 2개(every workflow job runs on the runner of .github/runner, every workflow that runs on push cancels the run of the push before it)는 이 항목이 건드리지 않는 .github/workflows/release.yml만 읽는다. make version-check(선언 25개)와 make docs-rules-check(39쌍)가 통과한다. 고정 의존성을 설치한 scratch venv의 Python 3.14.2로 Python 단위 discovery를 실행하면 test 131개 중 126개가 통과한다. 나머지 5개는 make client-db-check의 database 서버가 필요하거나, 이 항목이 건드리지 않은 test_driver_errors의 SQLite NOWAIT 잠금 case에서 실패한다. make owner-check는 이 checkout에서 고를 수 없다. contracts/check-inputs.json이 CHECK_TARGETS에 없는 parity-check를 선언해서(commit 15f5faf3), owner 선택이 고르기 전에 멈춘다.——[o]
T115-2Python venv는 고정된 interpreter로 만든다. 원인: install-python(Makefile)은 PATH의 python3 -m venv를 실행한다. Xcode의 Python 3.9가 있는 기계에서는 3.9 venv가 만들어지고, python-check가 package가 선언하지 않은 Python에서 client를 import하게 된다(T115-1). 반면 config/toolchain.json은 pythonBuild를 선언하고, kit의 make python-venv가 그것으로 venv를 만든다(T115에서 발견). 완료 기준: install-python이 고정된 package와 함께 kit의 python-venv를 쓰고, python-venv-check가 interpreter가 .python-version과 다른 venv를 거절한다. 다른 minor의 PATH python3로 먼저 red. 근거(로컬): RED: 3.9.6 venv(/usr/bin/python3로 만든 임시 venv)에서 python-venv-check가 통과한다(exit 0). PATH의 python3이 3.9.6인 상태에서 make install-python PYTHON_VENV=<임시>는 3.9.6 venv를 만들고(pyvenv.cfg home이 Xcode), pip 21.2.4가 psycopg[binary]==3.3.6을 찾지 못해 실패한다. scripts/repo/python-venv.test.mjs는 3개 중 2개가 실패한다(거부 case와 install-python의 dry run이 python3 -m venv를 보인다). GREEN: 3.9.6인 packages/orm-python/.venv를 지우고 make install-python이 var/tools/bin/python3(Python 3.14.8, home var/tools/python-build)으로 venv를 만들어 고정 package를 설치한다(import 확인). make python-venv-check 통과; scripts/repo/python-venv.test.mjs 3개 통과; make repo-check/unit 93개 통과; node scripts/repo/check.mjs 통과; make -k python-check: python-unit-check 131개 중 126 통과, 4 실패와 1 error는 TEST_ENV의 database DSN이 필요한 statement-fault case이거나(이 세션에서 설정하지 않았다) test_driver_errors의 SQLite fault frame(그 test 자신의 코드의 TypeError, 이 item과 무관); python-interface-unit-check 13개 통과; python-model-check와 conformance-result-check/python 통과. database 서버는 시작하거나 멈추지 않았다.——[o]
T115Python package가 wheel로 release된다. 원인: config/release.json은 packages/orm-python을 kit의 wheel 종류(K29)가 아니라 python 종류로 release한다. 그래서 polyspec/fsm이 설치하는 PEP 427 wheel을 어떤 release도 싣지 않는다. 완료 기준: packages/orm-python/pyproject.toml이 각 Requires-Dist를 name==version으로 고정한다. config/release.json이 wheel 종류를 선언한다. release proof가 wheel을 offline으로 설치하고 import한다. 문서(EN과 ko)가 release 형식을 적는다. 먼저 red: wheel을 찾지 못하는 release-proof 실행. 완료: acceptance의 name==version 고정은 registry package에 적용된다. T115에서 sync한 kit 0.0.25(K49)는 polyspec 의존성을 release asset과 그 sha256의 direct reference로 요구하므로 polyspec-ordered-json은 @ https://github.com/polyspec/ordered-json/releases/download/v0.0.8/polyspec_ordered_json-0.0.8-py3-none-any.whl#sha256=db98…645a이고, config/release.json은 consumers.wheel.lock(packages/orm-python/pylock.toml, sha256이 있는 package 9개, kit가 고정한 uv 0.12.11로 작성)과 consumers.wheel.polyspec을 선언한다. Red: main 9ddc9665에서 release.mjs assets v0.0.7은 wheel을 만들지 않았다(proof.gitTag.packages/orm-python/pyproject.toml.kind is "python"). Green(rebase된 tree): assets가 polyspec_orm-0.0.7-py3-none-any.whl을 만들었고, release-consumer 경로가 lock(requirement 9개, --require-hashes)과 wheel(--no-index)을 설치하고 pip check를 통과하고 polyspec.orm을 import했다. kit-check, release coverage, repo-check, interface-check, documents-check, docs-rules-check가 통과한다. 반복 가능한 make python-lock과 그 offline 검사는 kit 0.0.26(K54)과 함께 온다.——[o]
T114Release 0.0.8. 원인: polyspec/fsm이 orm의 Python wheel release를 기다린다(그 S-34-3). 그런데 markdown executor를 싣는 release(T69)는 아직 멀어서, 사용자가 main에 있는 것으로 중간 release를 내기로 했다. 완료 기준: T73, T74, T75, T113과 다른 session의 T78이 main에 있고 진행 중인 항목이 없다. 0.0.8의 release 절차를 이전 release 항목(T52, T56)과 같이 밟고, asset에 Python wheel이 들며, maintainer의 tag로 끝난다. T73, T74, T75, T113, T78, T115가 필요하다. 상태: release commit이 모든 manifest, lock, 판을 인용하는 문서에 0.0.8을 두고, 2026-10-11의 의존성 검토(모든 client가 ordered-json 0.0.8)를 기록하며, 0.0.8의 consumer lock을 둔다. 나중에 지운 local tag로 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, make release-consumer TAG=v0.0.8이 exit 0이다. asset에 polyspec_orm-0.0.8-py3-none-any.whl이 있고, consumer는 그것을 lock한 요구 9개와 함께 설치하고 pip check를 통과하고 polyspec.orm을 import하며, Composer package를 설치하고 Polyspec\Orm\Db를 찾는다. 마지막 단계는 maintainer의 tag다. maintainer의 tag가 마지막 단계다. 불가능: bump commit e49cea09가 batch로 push되어 head가 다른 commit이므로 check run이 없고, tag된 commit의 check run을 읽는 make release-verify를 통과할 수 없다. main은 이미 그것을 지났고 다른 commit은 0.0.8 선언을 가지지 않는다. 0.0.8은 대체된다: 준비된 선언과 이 dependency review는 T61, T62와 flowmark의 T12.12가 고정할 T83–T102 series도 함께 담은 0.0.9(T126)에 실린다. 불가능: bump commit e49cea09가 batch로 push되어 head가 다른 commit이므로 check run이 없고, tag된 commit의 check run을 읽는 make release-verify를 통과할 수 없다. main은 이미 그것을 지났고 다른 commit은 0.0.8 선언을 가지지 않는다. 0.0.8은 대체된다: 준비된 선언과 dependency review는 T61, T62와 flowmark의 T12.12가 고정할 T83–T102 series도 담은 0.0.9(T126)에 실린다.——[-] cause: polyspec/fsm이 orm의 Python wheel release를 기다린다(그 S-34-3). 그런데 markdown executor를 싣는 release(T69)는 아직 멀어서, 사용자가 main에 있는 것으로 중간 release를 내기로 했다. 완료 기준: T73, T74, T75, T113과 다른 session의 T78이 main에 있고 진행 중인 항목이 없다. 0.0.8의 release 절차를 이전 release 항목(T52, T56)과 같이 밟고, asset에 Python wheel이 들며, maintainer의 tag로 끝난다. T73, T74, T75, T113, T78, T115가 필요하다. 상태: release commit이 모든 manifest, lock, 판을 인용하는 문서에 0.0.8을 두고, 2026-10-11의 의존성 검토(모든 client가 ordered-json 0.0.8)를 기록하며, 0.0.8의 consumer lock을 둔다. 나중에 지운 local tag로 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, make release-consumer TAG=v0.0.8이 exit 0이다. asset에 polyspec_orm-0.0.8-py3-none-any.whl이 있고, consumer는 그것을 lock한 요구 9개와 함께 설치하고 pip check를 통과하고 polyspec.orm을 import하며, Composer package를 설치하고 Polyspec\Orm\Db를 찾는다. 마지막 단계는 maintainer의 tag다. maintainer의 tag가 마지막 단계다. 불가능: bump commit e49cea09가 batch로 push되어 head가 다른 commit이므로 check run이 없고, tag된 commit의 check run을 읽는 make release-verify를 통과할 수 없다. main은 이미 그것을 지났고 다른 commit은 0.0.8 선언을 가지지 않는다. 0.0.8은 대체된다: 준비된 선언과 이 dependency review는 T61, T62와 flowmark의 T12.12가 고정할 T83–T102 series도 함께 담은 0.0.9(T126)에 실린다. 불가능: bump commit e49cea09가 batch로 push되어 head가 다른 commit이므로 check run이 없고, tag된 commit의 check run을 읽는 make release-verify를 통과할 수 없다. main은 이미 그것을 지났고 다른 commit은 0.0.8 선언을 가지지 않는다. 0.0.8은 대체된다: 준비된 선언과 dependency review는 T61, T62와 flowmark의 T12.12가 고정할 T83–T102 series도 담은 0.0.9(T126)에 실린다
T1260.0.9 릴리스. 원인: flowmark의 T12.12가 T61, T62와 T83–T102 validator series를 담은 release tag를 요구하는데 T114의 0.0.8은 tag되지 못했으므로(bump commit에 check run이 없다) 그 section과 그 뒤의 모든 변경이 0.0.9로 나가고, polyspec/fsm의 wheel 대기(S-34-3)도 같은 release가 채운다. 인수 조건: 이전 release 항목(T52, T56, T114)의 절차를 0.0.9로: manifest, lock, VERSION, contracts/features.json의 version, PHP extension의 version, 문서, tests/release-consumer의 consumer lock이 0.0.9를 선언한다. tag를 local에서 만들었다 지운 뒤 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, make release-consumer TAG=v0.0.9가 0으로 끝나고, asset이 polyspec_orm-0.0.9-py3-none-any.whl을 가지며, consumer가 wheel을 locked requirement와 함께 설치해 pip check를 통과하고 polyspec.orm을 import하며, Composer package를 설치해 Polyspec\Orm\Db를 찾는다. 그런 뒤 maintainer가 release commit에 v0.0.9(annotated)를 만들어 push하고 release workflow가 green이다. main이 green일 것. 상태: release commit ddcdca4f는 모든 곳에 0.0.9를 적는다(조상 commit들이 version 설정과 local tag로 맞춰 쓴 consumer lock, checklist를 담는다). 그 CI 실행은 timing에 민감한 test-server case 하나의 rerun 뒤 success로 끝났고 maintainer의 tag v0.0.9가 release를 공개했다. workflow가 archive 넷(npm tgz, Composer zip 둘, Python wheel)을 만들고 draft를 증명했다 — npm, Composer, wheel의 consumer 설치, git-tag 설치, Go module tag — 그리고 공개했다. gh release view v0.0.9가 asset 넷을 보인다. release는 길에서 다섯 고침을 필요로 했고 각각은 그것을 맡는 파일에 기록되었다: kit의 install-go가 scratch module 안에서 toolchain을 내려받고(K65), release workflow가 설치된 tool을 PATH에 올리며(K66) .php-version의 PHP를 coverage none으로 설치하고, release-tools가 checkout의 Go module과 node module을 데우며, go.mod는 repository 규칙이 말하는 minor Go를 지킨다. polyspec/fsm의 wheel 대기(S-34-3)는 polyspec_orm-0.0.9-py3-none-any.whl이 채웠다.——[o]
T126-1kit을 release tag에서 vendor하고, release asset을 선언된 설치 위에서 build한다. 원인: 0.0.9 release를 위해 orm은 kit main에 없는 수정(K65, K66)을 가진 kit branch에서 kit을 vendor했으므로, kit main을 sync하면 release가 다시 깨진다; 또 그 branch의 release-tools는 orm의 build 요구(Go module, node module)를 GOPROXY 기본값으로 추측했는데, 이것은 fallback이다. 인수 조건: kit main이 K65와 K66을 담고 release된다(v0.0.31이며, 그 정정 K66-1, K66-2와 소비자 배치 실행 K67도 담는다); orm이 KIT_TAG=v0.0.31를 sync하고 make kit-check가 통과한다; orm의 release-assets가 typescript-build 전에 install-go와 install-node를 선언하므로 release는 자신이 설치한 module 위에서 build한다; CI가 green이다.—근거: kit main이 K65, K66, K66-1, K66-2, K67을 담고 v0.0.31으로 tag되었다; make kit-sync KIT_TAG=v0.0.31; config/toolchain.json을 uv 선언으로 옮겼고 make install-tools가 uv(sha256 검사)와 Python 3.14.8을 설치했다; 옛 Python build의 venv는 이름이 밝혀진 채 거부되어 다시 만들었다; make kit-check repo-check toolchain-check dependency-policy-check documents-check release-coverage docs-rules-check feature-unit-check kit-test가 통과한다(kit-test 576 통과; v0.0.31은 K66-2와 K67을 더한다).[o]
T126-2kit v0.0.35를 vendor한다(T126-1에 이어). 원인: kit v0.0.31의 python-lock test는 checkout의 var/ 아래에 tree를 만드는데 CI static group에는 그것이 없어, run 38413640279에서 kit-test가 ENOENT: mkdtemp …/var/python-lock-check-XXXXXX로 실패했다; kit v0.0.34는 시스템 임시 directory 아래에 만든다. 인수 조건: 옛 kit에서의 sync가 요구하는 대로 두 번 실행한 make kit-sync KIT_TAG=v0.0.35; K79 전환은 T137에 남는다.—증거: make kit-check가 vendor 파일이 lock과 맞다고 보고하고, make kit-test가 test 575개를 통과하며, make documents-check가 PRINCIPLES.md와 함께 문서 쌍 42개를 읽는다; docs.translation-shape가 PRINCIPLES.ko.md에서 실패한 뒤, AGENTS.md의 규칙대로 scripts/docs/rules.mjs가 .kit/kit.lock.json이 vendor로 선언한 파일을 읽지 않으며, npm run docs:rules-check가 통과한다.[o]
T126-2-1workflow test에서 vendor한 workflow만 판정한다(T126-2에 이어). 원인: kit v0.0.35는 release.yml 외에 PRINCIPLES.md와 그 짝도 vendor하는데, workflow test는 vendor 파일이 정확히 release.yml이기를 요구해 CI run 38421077014의 repo-check가 실패했다. 인수 조건: test가 vendor한 workflow가 release.yml이기를 요구한다.—증거: make repo-check/unit에서 그 case가 통과한다.[o]
T128모든 runtime의 표준으로서의 dbspec editor stack. 원인: owner가 polyspec/template 같은 highlighter와 formatter를 요청했다; 첫 시도(Go 전용 cmd/orm-fmt, cmd/orm-lsp, cmd/orm-tokens와 regex 분류기 engine/dbspec/tokens.go)는 문법을 중복했고 template도 표준도 따르지 않았다. 인수 조건: 한 round가 아무것도 찾지 못할 때까지 적대 검증한 editor stack 계획의 판정과 항목이 모두 끝난다: runtime마다 문법 하나, 여섯 구현 모두의 analyze와 format, 선언된 set 파일, 선언 위치, browser-safe parser 진입점, dbspec-fmt를 가진 language service, LSP, CodeMirror와 VS Code adapter, docs fence, 그리고 그 release; 계획이 지정한 template, kit, flowmark 항목은 그 repository의 checklist에서 관리한다. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Retry: 그 orm release가 공개되면 시작한다. 결정(사용자, 2026-10-18): 개발은 릴리스 없이 로컬 링크로 한다. orm 릴리스를 기다리는 retry는 변경이 orm main에 있으면 충족된다. 상태(2026-10-18): T128-R을 뺀 모든 하위 항목이 main에서 완료됐다. 이 항목은 T128-R만 기다린다.——[!] cause: owner가 polyspec/template 같은 highlighter와 formatter를 요청했다; 첫 시도(Go 전용 cmd/orm-fmt, cmd/orm-lsp, cmd/orm-tokens와 regex 분류기 engine/dbspec/tokens.go)는 문법을 중복했고 template도 표준도 따르지 않았다. 인수 조건: 한 round가 아무것도 찾지 못할 때까지 적대 검증한 의 판정과 항목이 모두 끝난다: runtime마다 문법 하나, 여섯 구현 모두의 analyze와 format, 선언된 set 파일, 선언 위치, browser-safe parser 진입점, dbspec-fmt를 가진 language service, LSP, CodeMirror와 VS Code adapter, docs fence, 그리고 그 release; 계획이 지정한 template, kit, flowmark 항목은 그 repository의 checklist에서 관리한다.; retry: 그 orm release가 공개되면 시작한다. 결정(사용자, 2026-10-18): 개발은 릴리스 없이 로컬 링크로 한다. orm 릴리스를 기다리는 retry는 변경이 orm main에 있으면 충족된다. 상태(2026-10-18): T128-R을 뺀 모든 하위 항목이 main에서 완료됐다. 이 항목은 T128-R만 기다린다
T128-1Go editor 도구 제거. 원인: T128의 잘못된 설계이며(계획 Context) main을 red로 만든다: feature-unit-check가 behaviour test 없는 cmd/orm-*/main.go를 찾고, docs build의 go build ./cmd/orm-tokens가 toolchain을 찾지 못한다. 인수 조건: 새 제거 commit(history를 다시 쓰지 않는다)이 cmd/orm-fmt, cmd/orm-lsp, cmd/orm-tokens, engine/dbspec/tokens.go와 그 test, 그 feature 항목, Makefile의 tools target, docs 연결(dbs.ts, config의 fence와 import, style class, build 단계)과 dbs fence를 지운다; Go symbol은 -record로 다시 기록하고 diff가 정확히 지운 줄인지 검토한다; ci와 docs-pages가 green이다.—근거: 네 commit을 새 commit으로 되돌렸다; interfaces 검사의 -record는 아무것도 바꾸지 않으며, 계산한 Go symbol hash가 기록된 sha256:a65cb65f…7bed와 같고 contracts/symbols/go.json의 diff는 정확히 지운 8줄이다; make feature-unit-check repo-check dbspec-go-check docs-rules-check docs-verify-idempotent와 npm run docs:check가 통과한다.[o]
T128-2판정을 명세에 적는다. 인수 조건: docs/spec/dbspec.md(+ko)에 editor stack 계획의 판정대로 Syntax analysis와 Formatting을 더하고, docs/spec/editor.md(+ko)가 layer, import 규칙, 결과, fixture를 적는다.—증거: docs/spec/dbspec.md(+ko)의 "Syntax analysis"와 "Formatting"이 판정 1–3을 적으며, token class는 lexer의 token 네 종류로부터 정의한다; lexer에게 숫자는 단어이므로 계획의 lexical 전용 규칙을 거기서 바로잡았다; docs/spec/editor.md(+ko)가 계층, import 규칙, 결과, 생태계 방식, fixture를 적고 site sidebar에서 연결된다; make documents-check, npm run docs:rules-check, npm run docs:check가 통과한다.[o]
T128-2-1구문 분석 규칙에서 숫자를 그 형태로 정의한다(T128-2에 이어). 원인: T128-2는 TypeScript lexer만 보고 lexer가 숫자를 단어로 읽는다고 적어, lexical 전용 줄의 숫자를 word로 만들었다. Go lexer는 숫자와 선택적 소수부로 된 단어를 숫자로 읽으므로, 그 문장은 한 runtime의 구현 세부였고 계획의 원래 규칙을 뒤집었다. 인수 조건: docs/spec/dbspec.md(+ko)가 숫자를 형태로 정의하고 lexical 전용 줄에서도 그 class를 유지한다; 계획(+ko)이 정정을 적는다.—증거: make documents-check와 npm run docs:rules-check가 통과한다.[o]
T128-2-2regex 문법 판정의 근거를 올바른 이름에 둔다(T128-2에 이어). 원인: docs/spec/dbspec.md "Syntax analysis"와 계획의 판정 1이 table을 올바른 column 이름이라 했지만 table은 예약어다(docs/spec/dbspec.md "Names"). 인수 조건: 둘 다 올바른 column 이름인, 예약되지 않은 keyword 단어를 든다.—증거: column key, text, references, codec을 가진 table의 Go parse는 성공하고 table은 name.format을 보고한다; make documents-check와 npm run docs:rules-check가 통과한다.[o]
T128-2-3editor package를 폴더 표준대로 둔다(T128-2에 이어). 원인: docs/editor.md와 계획은 S5가 component의 부분을 중첩 folder에 두도록 허용한다고 읽어 editor stack을 packages/dbspec/<part>/에 두었다. S5(kit PRINCIPLES, K70)는 packages/<component>/<runtime>/에 runtime port만 중첩하며, kit의 component-ports 규칙은 다른 중첩 folder를 거부한다. 인수 조건: docs/spec/editor.md(+ko), 계획(+ko), checklist가 packages/dbspec-language, packages/dbspec-lsp, packages/dbspec-codemirror, packages/dbspec-vscode를 적으며, 각각 repository prefix 없는 독립 component다.—증거: packages/dbspec/는 이 정정을 적는 문장에만 나온다; make documents-check와 npm run docs:rules-check가 통과한다.[o]
T128-2a모든 runtime의 set 파일 dbspec-set.json. 원인: 문서 묶음이 다섯 가지 생성기 형식의 명령 인자로 두 번 선언된다; owner는 선언 파일 하나로 판정했다. 인수 조건: 파일과 그 검증을 vector와 함께 명세한다; Dbspec.readSet이 모든 runtime에 interface 규칙, symbol, parity unit, test와 함께 있다; 모든 생성기가 --set(Rust Builder::set)을 받고 옛 형식은 제거한다; 내부 호출처, 문서, 파괴적 변경 changelog 항목이 따른다; Python의 orm-gen은 argparse로 인자를 읽는다; parseSet은 T128-4의 함수와 같은 @polyspec/orm/dbspec subpath에서 export하며, node: module, Node global, driver가 없다.—증거: sub-item T128-2a-1부터 T128-2a-7과 그 후속이 끝났다: set 파일은 vector 61개와 함께 명세되었고, 모든 runtime이 그것을 읽어 vector를 통과하며, 모든 generator가 set 파일만 받고, 문서, interface rule, parity unit이 이를 따른다.[o]
T128-2a-1set 파일을 vector와 함께 명세한다(T128-2a에 이어). 인수 조건: docs/spec/dbspec.md(+ko) "Document sets"가 dbspec-set.json, 경로 정규형, reader의 네 단계, 바뀌는 tree에 대해 밝힌 한계, 결과, set.* rule을 적는다; tests/dbspec/set-files.json이 모든 rule과 오류 순서의 받아들이는 file tree case와 거부하는 case를 담는다.—증거: 세 번의 adversarial review가 finding 없이 끝났다(finding 27개 반영, identity 경합은 파일마다 handle 하나로 풀고 그 한계를 밝힘); make documents-check, npm run docs:rules-check, npm run docs:check가 통과한다; vector는 61 case다.[o]
T128-2a-1-1root를 끝의 구분자 없이 조사한다(T128-2a-1에 이어). 원인: root는 주어진 set 경로의 prefix이고 구분자로 끝나는데, POSIX에서 lstat("alias/")은 symbolic link를 따르므로 vector root-symlink가 link된 root를 통과시킬 수 있었다; Rust agent가 찾았다. 인수 조건: 단계 1이 root를 끝의 구분자 없이 조사하며, /인 root는 / 그대로다.—증거: docs/spec/dbspec.md(+ko) 단계 1이 이를 적는다; make documents-check와 npm run docs:rules-check가 통과한다.[o]
T128-2a-1-2중복 key의 깊이와 schema의 읽기를 정한다(T128-2a-1에 이어). 원인: Python reader가 열려 있던 두 읽기를 찾았다: "어떤 key도 두 번 나오지 않는다"가 중첩 object에도 걸리는지({"schema": {"a": 1, "a": 2}}가 set.key인지 set.schema인지), 그리고 1.0000000000000000001이 1인지. 인수 조건: 단계 2가 모든 깊이의 모든 object를 검사하고, schema를 RFC 8259가 권하는 대로 IEEE 754 double로 읽는다.—증거: docs/spec/dbspec.md(+ko) 단계 2가 둘을 적는다; make documents-check와 npm run docs:rules-check가 통과한다.[o]
T128-2a-2Go set reader와 orm-gen gen --set(T128-2a-1에 이어). 인수 조건: dbspec.ReadSet이 set-files.json의 모든 case를 실행한다; runtimemodel.LoadFileSet과 --document/--use를 모든 호출자와 함께 지운다.—증거: 변경 전 go test ./engine/dbspec -run TestReadSetVectors가 undefined: ReadSet으로 실패했고, 변경 후 61 case가 실제 link로 모두 통과하며, TestGoGenerationCommandReportsSetErrors가 set 오류나 파일 diagnostic에서 exit 1과 출력 없음을 확인하고, 세 Go model package의 --set go generate가 model을 byte 그대로 두며, GOOS=windows go build ./engine/dbspec이 Windows reader를 compile하고, Go interface 검사가 통과한다.[o]
T128-2a-3TypeScript set reader, parseSet, --set(T128-2a-1에 이어). 인수 조건: @polyspec/orm/dbspec의 parseSet(bytes, path)와 readDbspecSet이 set-files.json을 실행한다(parseSet은 tree 없는 case); --schema/--use를 지운다.—증거: 변경 전 runner는 browser entry가 parseSet을 export하지 않아 실패했고, 변경 후 packages/orm-npm/tests/dbspec-set.mjs가 61 case 모두를 실제 tree에서 readDbspecSet으로, tree 없는 case를 두 entry의 parseSet으로 통과하며, browser entry test가 parseSet export와 함께 통과하고, generator test가 --schema와 --use를 거부하고 모든 set 오류를 출력하며, TypeScript interface 검사가 통과한다.[o]
T128-2a-4Python set reader와 argparse의 --set(T128-2a-1에 이어). 인수 조건: read_dbspec_set이 set-files.json을 실행한다; 손으로 쓴 인자 loop를 argparse로 바꾼다; --schema/--use를 지운다.—증거: 변경 전 vector runner가 read_dbspec_set import에서 실패했고, 변경 후 set-files.json의 61 case가 실제 symbolic link와 hard link로 모두 통과하며, test_orm_gen.py가 --schema와 --use의 거부, set 오류나 문서 diagnostic이 모든 message를 출력하고 아무것도 쓰지 않음을 확인하고, make python-model-check가 --set schema/dbspec-set.json으로 통과하며, Python interface 검사가 통과한다.[o]
T128-2a-4-1Python test model을 set 파일로 생성한다(T128-2a-4에 이어). 원인: T128-2a-4가 test helper generate가 --set을 넘기게 바꿨지만 test module 20개가 아직 schema/bench.dbs나 fixture 문서를 넘겨, CI python group이 orm-gen gen --set …bench.dbs에서 실패했다. 인수 조건: 호출자는 schema/dbspec-set.json이나 corpus set 파일을 넘기고, 문서 하나짜리 fixture는 그것을 자기 set 파일 옆에 복사하는 generate_document를 거친다.—증거: 변경 전 python -m unittest test_utils가 orm-gen 종료 상태 1로 실패했고, 변경 후 바뀐 module 18개와 test_sqlite가 test 69개를 통과하며, 서버로 도는 module 둘이 compile된다.[o]
T128-2a-5PHP와 extension의 set reader와 --set(T128-2a-1에 이어). 인수 조건: Dbspec::readSet과 그 native 짝이 set-files.json을 실행한다; 위치 인자 파일을 지운다.—증거: 변경 전 61 case 모두 Call to undefined method Dbspec::readSet()으로 실패했고, 변경 후 make dbspec-php-check가 vector와 집중 set case 69개를 통과하며, make dbspec-php-extension-check가 69개 모두에서 native 결과가 PHP client와 같음을 요구하고, orm_gen_test.php가 위치 인자 파일과 --use를 거부하고 모든 set 오류를 출력하며 아무것도 쓰지 않고, php와 php-extension interface 검사가 통과한다.[o]
T128-2a-5-1각 PHP test를 한 번만 실행한다(T128-2a-5에 이어). 원인: set target인 dbspec-php-check/set과 dbspec-php-extension-check/set이 show-create target이 소유한 dbspec_show_create_test.php도 실행해, 한 recipe가 독립 test 둘을 실행했고 repository 검사가 이를 거부한다. 인수 조건: 각 set target은 자기 set test만 실행한다.—증거: 변경 전 node scripts/repo/check.mjs가 그 recipe를 보고했고, 변경 후 검사가 통과하며 make dbspec-php-check/set이 통과한다.[o]
T128-2a-6Rust set reader와 Builder::set(T128-2a-1에 이어). 인수 조건: dbspec::read_set이 set-files.json을 실행한다; Builder::new([..])와 uses를 지운다; cargo:rerun-if-changed가 set 파일과 나열된 모든 문서를 적는다.—증거: 변경 전 vector test가 cannot find function read_set으로 compile에 실패했고, 변경 후 61 case가 실제 link로 모두 통과하며, orm-build test가 모든 set 오류와 diagnostic을 보고하고 아무것도 쓰지 않음과 cargo:rerun-if-changed가 set 파일과 각 문서를 적음을 확인하고, make rust-150-check와 clippy가 통과하며(Windows code는 x86_64-pc-windows-msvc에서 clippy 통과), Rust interface 검사가 통과한다.[o]
T128-2a-6-1Rust set test를 test 규칙 안에 둔다(T128-2a-6에 이어). 원인: dbspec_set.rs가 측정한 CPU 시간으로 test를 실패시켰는데 저장소 규칙은 이를 금지하며(측정은 경고한다), dbspec-rust-check/set이 한 recipe에서 독립 test 둘을 실행했다. 인수 조건: 측정은 dbspec.rs처럼 경고를 출력하고, target은 part vectors와 unit으로 나뉜다.—증거: 변경 전 node scripts/repo/check.mjs가 둘을 보고했고, 변경 후 검사가 통과하며 make dbspec-rust-check/set이 두 part를 통과한다.[o]
T128-2a-7set 파일의 호출자와 문서(T128-2a-2부터 T128-2a-6에 이어). 인수 조건: Makefile target, script, docs/operations/usage.md, docs/spec/schema.md, docs/spec/dsl.md, docs/operations/packaging.md(+ko)가 set 파일을 쓴다; interfaces rule Dbspec.readSet과 symbol, parity; breaking changelog 항목.—증거: docs/operations/usage.md, docs/spec/schema.md, docs/spec/dsl.md, docs/operations/packaging.md(+ko)가 다섯 generator 모두에 set 파일을 쓰고, 바뀐 명령은 각각 한 번 실행해 exit 0이었다(Go, PHP, TypeScript는 --check 유무 둘 다, Python, Rust는 make rust-150-check로); rule Dbspec.readSet이 모든 언어와 extension의 reader를 선언하고 make interface-check가 통과한다; parity unit dbspec-set-files가 R4 undeclared-suite finding을 없앴다; set test를 schema_definition이 소유한 채 node scripts/features/build.mjs --check가 통과한다.[o]
T128-2a-8coverage model을 set 파일로 생성한다(T128-2a에 이어). 원인: CI feature_coverage group이 TestCoverageModelGenerationCheck에서 실패했다: Go test는 set 파일 없이 schema/bench.dbs만 복사했고(set.file: … lists no dbspec-set.json), PHP 검사는 문서 경로를 넘겼다(unexpected argument: schema/bench.dbs). 인수 조건: 둘 다 schema/dbspec-set.json으로 생성한다.—증거: hyper session의 agent가 두 message를 먼저 재현했다; go test -tags featurecoverage -run TestCoverageModelGenerationCheck$ ./packages/orm-go/model과 PHP model_generation_check가 통과한다.[o]
T128-3s모든 runtime에서 반복된 state machine 줄 거부. 원인: parser가 한 machine의 반복된 initial, terminal, <from> -> <to> 줄을 require 목록이 달라도 아무 말 없이 받아들인다(parse.ts:2249-2284). 인수 조건: 그런 반복은 뒤쪽 줄 keyword 위치의 setting 오류이며, docs/spec/dbspec.md(+ko)에 적고, 모든 parser에서 검사하며, cases.json의 거부 vector 세 개와 changelog 항목을 가진다.—증거: 거부 vector 셋(state-machine-initial-repeated, 다른 require 목록의 -terminal-repeated, -transition-repeated)이 모든 runtime(Go, TypeScript, PHP, PHP client와 비교한 PHP extension, Python, Rust)에서 먼저 실패했다(문서를 parse함); 변경 뒤 각 runtime은 뒤 줄의 keyword에서 setting error를 보고하고, 다른 column의 줄은 기계 하나 error만 유지하며, 공유 vector가 여섯 모두에서 통과한다; docs/spec/dbspec.md(+ko)가 줄의 identity를 적는다; Go와 TypeScript symbol이 계산된 hash와 함께 machineLine을 기록한다.[o]
T128-3p모든 runtime parse 결과의 선언 위치. 원인: flowmark 같은 도구가 text를 다시 훑어 선언을 찾고, 1행 1열 fallback을 가진다. 인수 조건: 성공한 parse 결과가 editor stack 계획에 정의한 {kind, path, line, column}의 source 순서 목록 positions(반복 규칙에서 온 setting identity, primary key, document 이름 포함)를 DbspecDocument 밖에 가진다; docs/spec/dbspec.md(+ko)에 적고 모든 kind의 vector와 모든 runtime의 parity를 가진다.—증거: 여섯 runtime이 모두 positions case의 위치를 돌려준다(Go 156e07fe, TypeScript 91ffbc4b, Python 16015424, Rust 520877b3, PHP 4d1da64c, extension ecabd61e); make dbspec-compare-check가 client와 extension 사이에서 이를 비교한다(12 run 일치); parity unit dbspec-positions와 schema_definition coverage case dbspec_parse_positions가 이를 선언한다; make interface-check가 모든 언어에서 통과한다.[o]
T128-3모든 runtime의 analyze와 format. 인수 조건: 여섯 구현이 editor stack 계획의 불변식을 지키며 표준 token과 canonical format을 돌려준다; tests/dbspec/syntax.json이 모든 runtime 검사와 dbspec-compare-check에서 실행된다; interface 규칙, symbol, parity unit, feature, php-extension 규칙, docs/spec/interfaces-model.md(+ko)가 그것을 기록한다.—증거: 모든 runtime의 test가 tests/dbspec/syntax.json을 엄격히 읽어 일치한다; make dbspec-compare-check가 여섯 구현에서 304 syntax case 모두의 token, 분석 diagnostic, format과 거부를 같게 준다(12 run 일치); parity unit dbspec-syntax, coverage case dbspec_analyze_tokens, dbspec_format, dbspec_format_refusal, extension의 method 문장, docs/spec/interfaces-model.md(+ko)의 Dbspec 행이 이를 기록한다; make interface-check가 통과한다.[o]
T128-3-1모든 runtime에서 encoding 중단 앞의 줄을 parse한다(T128-3에 이어). 원인: Python은 encoding 오류에서 어떤 줄도 parse하기 전에 멈추지만, Go는 그 앞의 완전한 줄을 parse해 그 diagnostic을 먼저 보고한다; 그런 경우를 담은 vector가 없었다. 인수 조건: cases.json의 encoding-after-a-syntax-error(4행 syntax 오류, 8행 단독 CR)가 모든 runtime에서 두 diagnostic을 보고한다.—증거: Python(c785848a), TypeScript(0abd4c06), PHP와 그 extension(0b7bb4f6)은 변경 전 encoding diagnostic만 보고하며 이 case를 실패했고 변경 후 통과한다; Go와 Rust vector test(go test ./engine/dbspec, cargo test --test dbspec)가 통과한다; make dbspec-php-check와 make dbspec-php-extension-check가 통과한다.[o]
T128-3-2T129 case에 맞춰 syntax vector를 다시 생성한다(T128-3에 이어). 원인: T129가 tests/dbspec/cases.json에 case 아홉을 더하고 셋을 지웠는데, landing에서 tests/dbspec/syntax.json을 다시 생성하지 않아 vector가 없는 case를 적고 새 case를 빠뜨렸다. 인수 조건: node scripts/dbspec/syntax-vectors.mjs가 현재 case의 vector를 쓴다; 기존 case의 token은 바뀌지 않는다; TypeScript, Go, Python, Rust의 syntax test가 통과한다.—증거: 변경 전 node --test packages/orm-npm/tests/dbspec-analyze.mjs가 case id와 순서에서 실패했고, 변경 후 generator가 정확히 case 아홉을 더하고 셋을 지웠으며 TypeScript(897 pass), Go(go test ./engine/dbspec), Python(test_dbspec_syntax_vectors.py), Rust(cargo test --test dbspec syntax) test가 통과한다.[o]
T128-3-3editor stack test case를 case harness로 시작한다(T128-3에 이어). 원인: T128-3p와 T128-3이 더한 engine/dbspec의 Go test, Rust dbspec_analyze.rs test, T128-4의 packages/orm-npm/tests/browser-entry.mjs가 case를 직접 선언해 deadline과 RUN 줄 없이 돌았으며, 이는 make repo-check가 강제하는 규칙에 어긋난다. 인수 조건: 각각 internal/testcase, polyspec_orm_testcase, caseTest로 시작한다.—증거: 변경 전 node scripts/repo/check.mjs가 이 파일들에서 문제 11개를 보고했고, 변경 후 검사가 통과하며 Go, Rust, TypeScript test가 RUN과 PASS 줄과 함께 통과한다.[o]
T128-4@polyspec/orm의 browser-safe parser subpath. 인수 조건: parse, emit, analyze, format, readDbspecBytes를 node: module, Node global, driver가 없는 module에서 export하며, module graph test로 검사하고, interface 규칙에 기록한다.—증거: @polyspec/orm/dbspec이 node 없는 module(document.ts, read_bytes.ts, entry.ts)에서 parseDbspec, emitDbspec, analyzeDbspec, formatDbspec, readDbspecBytes를 export한다; packages/orm-npm/tests/browser-entry.mjs가 subpath의 module graph를 따라가며 먼저 node:crypto, node:fs, Buffer, driver를 지목하며 실패했고 이후 통과한다; make dbspec-ts-check 1335 통과; 옮긴 symbol로 typescript interface 검사가 통과한다.[o]
T128-4-1T129 이후에도 browser-safe entry가 Node module에 닿지 않게 한다(T128-4에 이어). 원인: T129가 @polyspec/orm/dbspec에서 parser가 닿는 dbspec/names.ts를 node:crypto로 hash하고 Buffer로 길이를 재게 하여 browser-safe entry가 Node module에 닿았다; landing에서 browser entry test를 돌리지 않았다. 인수 조건: names.ts는 SHA-256을 @noble/hashes(동기, browser와 Node 겸용, 의존 없음)에서, byte 길이를 TextEncoder에서 얻는다; browser entry test는 선언한 browser package를 허용하고 그 module을 같은 규칙으로 따라간다; 의존성에 review 기록이 있다.—증거: 변경 전 node --test packages/orm-npm/tests/browser-entry.mjs가 names.js의 node:crypto import와 Buffer 사용을 보고했고, 변경 후 @noble/hashes의 module까지 따라가며 통과한다.[o]
T128-5packages/dbspec-language. 인수 조건: openDocument가 range를 가진 diagnostic, token, folding, format을 준다; test가 cases.json, syntax.json, 불변식, 멱등성, editor fixture를 다룬다; module graph가 Node module, Node global, driver에 닿지 않는다.—증거: make dbspec-language-check가 635 case를 통과한다: canonical, normalize, invalid case, format이 지키는 hash, 멱등성, UTF-16으로 본 모든 syntax vector의 token, tests/editor의 editor fixture 다섯, 그리고 유일한 bare import가 packages/orm-npm/tests/browser-entry.mjs가 검사하는 parser entry로 해석됨을 보는 module graph test; folding은 {와 } token의 짝이므로(docs/spec/editor.md "Results") keyword를 읽지 않는다; sets/repeated-document-name vector 둘은 Record 형태가 없어 제외됨을 test가 확인하며, T128-5-1이 resolveSet으로 실행한다.[o]
T128-5-1dbspec-fmt CLI와 resolveSet(T128-5에 이어, T128-2a 다음). 인수 조건: resolveSet이 주입된 host를 받는다; CLI가 template-fmt 규칙과 exit code 0, 1, 2를 따르고 make install-cli가 symlink 없이 설치한다; sets/repeated-document-name vector를 resolveSet으로 실행한다.—증거: 변경 전 resolve-set과 vector test가 resolveSet 없이, CLI case 20개가 bin/dbspec-fmt.mjs 없이 실패했고 make install-cli에 rule이 없었다; 변경 후 make dbspec-language-check가 710 case를 통과한다: node host로 resolveSet을 거친 set-file vector 41개와 이유와 함께 닿을 수 없음을 test가 확인한 나머지 20개, resolveSet을 거친 repeated-document-name vector 둘, CLI mode, 종료 상태, stderr 형식; make install-cli가 symbolic link 없이 설치한다; docs/spec/editor.md(+ko)가 집합 해석과 명령줄을 적는다.[o]
T128-5-2language service의 typescript를 밝힌 예외로 6.0.3에 둔다(T128-5에 이어). 원인: 2026-10-13 review가 typescript 7.0.2 release를 찾았고 packages/dbspec-language는 6.0.3을 고정한다; 그 module graph test는 typescript API로 syntax tree를 읽는데, native compiler인 typescript 7에는 그 API가 없다. 인수 조건: config/dependency-policy.json이 packages/orm-npm처럼 이유, 제거 조건, 검증을 가진 예외를 담는다.—증거: make dependency-policy-check가 이 package를 더는 보고하지 않는다; make dbspec-language-check가 통과한다.[o]
T128-6packages/dbspec-lsp. 인수 조건: utf-16 위치, 전체 동기화, 표준 class의 semantic token, folding, formatting, 감시 파일을 가진 set 파일 결정을 갖춘 vscode-languageserver 서버; fixture에 대한 stdio session test.—증거: make dbspec-lsp-check가 build된 server에 대해 stdio session 12개를 통과한다: 선언한 capability와 legend; 다섯 editor fixture의 diagnostic, semantic token, folding, formatting; 가장 가까운 set 파일에서 온 문서의 집합; use 줄을 보고하는 단독 문서; set 오류를 보이고 문서를 단독으로 분석하는 것; disk보다 우선하는 열린 buffer(buffer를 무시하면 실패); 감시 파일 변경; 다른 파일의 diagnostic을 그 URI에 게시하는 것.[o]
T128-7docs fence가 language service로 dbs block을 highlight한다. 인수 조건: markdown-it fence hook이 밝고 어두운 색으로 token을 그리고, fence는 자기 묶음을 선언하며(dbs set=…), docs build는 fence 진단이 하나라도 있으면 실패한다.—증거: docs/spec/dbspec.md(+ko)의 fence가 shop 문서를 그 집합의 core 문서와 함께 렌더한다; npm run docs:check가 class span을 가진 site를 build한다; make docs-dbs-check가 7 case를 통과한다: use diagnostic이 있는 fence가 dbspec.md:24:5: use: …로 실패하는 것, header와 다른 선언 이름이 use 규칙으로 실패하는 것, 잘못된 info string, escape; docs/spec/editor.md(+ko)가 document= 속성을 적는다.[o]
T128-8packages/dbspec-codemirror와 packages/dbspec-vscode. 인수 조건: EditorState로 시험한 CodeMirror 6 mark, lint, fold, format 명령; semantic token scope를 가지고 문법 파일이 없으며 semantic highlighting을 기본으로 켜고 묶어 넣은 LSP client를 가진 VS Code 확장을 integration으로 시험한다; CI job은 kit 도구로 VS Code를 설치한다. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Retry: 그 orm release가 공개되면 시작한다. 결정(사용자, 2026-10-18): 개발은 릴리스 없이 로컬 링크로 한다. orm 릴리스를 기다리는 retry는 변경이 orm main에 있으면 충족된다.—증거: 하위 항목 T128-8-1, T128-8-2, T128-8-3이 main에서 완료됐다.[o]
T128-8-1packages/dbspec-codemirror(T128-8에 이어). 인수 조건: language service만 읽는 CodeMirror 6 extension dbspec({set})이 token mark, lint diagnostic, folding, format 명령을 준다; test는 editor fixture를 EditorState로 읽는다.—증거: make dbspec-codemirror-check가 fixture 다섯을 포함한 8 case를 통과하며 fixture의 token을 mark로, diagnostic, folding, format된 text를 준다; marks extension이 없으면 5 case가 실패한다; package는 typescript 7.0.2로 build되고, @codemirror/* review를 기록한 make dependency-policy-check가 finding 없이 통과한다.[o]
T128-8-2packages/dbspec-vscode(T128-8에 이어). 인수 조건: 언어 dbspec, 문법 없음, TOKEN_CLASSES 순서의 semantic token type과 각각의 scope, 기본으로 켠 semantic highlighting, vscode-languageclient로 부르는 묶인 language server, .vsix build를 가진 VS Code extension.—증거: make dbspec-vscode-check가 6 case(기여 선언, bundle, stdio로 TOKEN_CLASSES를 legend로 답하는 묶인 server)를 통과한다; npm run --workspace polyspec-dbspec package가 .vsix를 만든다; 의존성 review가 그 package를 기록한다; .gitignore가 모든 package의 node_modules를 무시한다.[o]
T128-8-3VS Code integration test를 실행한다(T128-8-2에 이어, T137과 T165 다음). 원인: editor fixture에 대한 vscode.provideDocumentSemanticTokens test는 orm이 T137로 받는 kit의 VS Code 설치기가 필요하다. 인수 조건: make dbspec-vscode-integration이 fixture 다섯을 통과하고(기여 선언을 깨 red를 먼저 본다), kit 설치기로 CI에서 실행된다. template처럼 ci.yml의 editor job이며 make check의 target이 아니다: 설치된 VS Code와 display(xvfb-run)가 필요한데 make check는 그것을 마련하지 않는다.——[o]
T128-9editor package를 검사와 release 설정에 연결한다. 인수 조건: orm 규칙을 가진 kit import 검사, feature owner, check input, CI group, workspace build와 test script, release manifest와 consumer smoke, dependency review 기록, AGENTS.md(+ko)의 package 배치.——[o]
T128-Reditor stack release. 인수 조건: green CI 뒤의 chore(release) bump commit, maintainer의 tag, release workflow, 모든 npm package의 release-verify. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Retry: 그 orm release가 공개되면 시작한다. 결정(사용자, 2026-10-18): 개발은 로컬 링크로 하고, 일괄 릴리스 전에는 아무것도 릴리스하지 않는다. 이 항목은 그 릴리스를 기다린다.——[!] cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다.; retry: 그 orm release가 공개되면 시작한다. 결정(사용자, 2026-10-18): 개발은 로컬 링크로 하고, 일괄 릴리스 전에는 아무것도 릴리스하지 않는다. 이 항목은 그 릴리스를 기다린다
T129renderer가 만드는 이름을 하나의 hash 규칙으로 63 bytes에 맞춘다(T8.1.5 후속). 원인: renderer는 <table>$<column> CHECK와 <table>$<event> trigger·function을 쓰는데, service_module_board_comment_history table과 service_module_board_post_seq column의 history는 66 bytes CHECK 이름을 받아 audit table을 선언할 수 없다. 선언된 이름은 여전히 거부하며 줄이지 않는다. 인수 조건: docs/spec/dbspec.md "이름"과 docs/dialects.md가 규칙을 정한다: 63 bytes 이하이면 plain form, 아니면 처음 54 bytes, $, SHA-256 소문자 16진수 8자리. 이름은 ASCII이므로 bytes가 곧 문자다. 생성 이름은 선언된 이름과 함께 유일하며, 충돌은 두 출처를 적는 name.duplicate다. tests/dbspec/cases.json은 정확히 63 bytes인 plain case와 64 bytes인 hash case를 허용하고, plain과 hash의 충돌, hash끼리의 충돌, 사용자가 고른 64 bytes 이름을 거부한다. tests/dbspec/ddl.json은 66 bytes history case와 정확한 생성 이름을 가진다. tests/dbspec/introspect.json은 그 문서를 MySQL, PostgreSQL, SQLite에서 다시 읽어 이름을 다시 계산한다. 모든 runtime(Go, PHP, PHP extension, TypeScript, Python, Rust)이 compare check에서 이 vector를 실행하고, make dbspec-ddl-check와 dbspec-introspect-check의 database 검사가 통과한다.——[o]
T129-1renderer 이름의 interface symbol을 기록한다(T129 후속). 원인: T129는 rendererName과 renderedColumn, generated-name 검사기와 Names class 등 symbol을 추가하고 없앴는데, 승인될 때 interface snapshot을 기록하지 않았다. 전체 make conformance-check가 그 기록이 없음을 찾았다.—근거: T129 tree(6cfcbac4)에서 --record로 interfaces 검사를 실행해 contracts/symbols의 다섯 snapshot을 썼고, contracts/interfaces.json은 계산된 hash를 가진다: go sha256:35e0d986f546547aeeb6070e9c2dc85382a6f4026c1a73536f57f7376b6ae8cc, php sha256:b011a44b9e3acfbca2f0bf1f358b7fc5e9a099bf1caac2fc4968c70af3f422b3, rust sha256:8cdc879e4e245937b87b3bc679bf67b0dab32c41c4d707ffd159b00278a65dbc, typescript sha256:6a7fccd4fe61e011534bf9cb221a086d7a4fa60415e43ea697a08ec54fd51da1, python sha256:1b21698d94db68f4b28193bafbd6f7a2fff5a066facbaa0bf3074fc66d0ae5ef. record가 없는 검사(non-record)는 다섯 언어와 PHP extension 모두에서 통과한다.[o]
T129-2PHP extension에 renderer 이름을 선언한다(T129에 이어). 원인: T129가 PHP client에 render, ofTable, column을 가진 Polyspec\Orm\Dbspec\Names를 더했는데, dbspec surface의 모든 class를 갖는 extension(docs/spec/dbspec.md "PHP extension")에는 Native\Names가 없어 make dbspec-php-extension-check가 declaration에서 실패했다. 인수 조건: C의 Native\Names가 PHP class와 같이 동작하며 stub, arginfo, PHP class와 비교하는 test를 갖는다.—증거: 변경 전 declaration 검사가 Native\Names 누락을 보고했고, 변경 후 make dbspec-php-extension-check가 63 byte와 64 byte 이름, parse한 table의 ofTable, 배열과 iterator, 일치 뒤로 읽지 않는 generator에 대한 column을 포함해 789 case를 통과한다; php와 php-extension interface 검사가 통과한다.[o]
T130한 table의 immutable과 audit setting을 각자의 trigger 이름을 보고 알아본다(T129 후속). 원인: audit과 immutable을 함께 쓴 table은 trigger가 다섯 개인데, 모든 client는 immutable을 정확히 두 개일 때만, audit을 정확히 세 개일 때만 알아보았으므로 introspection이 다섯 개를 미지원으로 보고했다(CONFIG: the database differs from the document set: unsupported_trigger …$audit_*/immutable_*). consumer 구성원이 보고했다(S-284). 인수 조건: docs/spec/dialects.md "Triggers"가 규칙을 정한다. 각 setting은 재계산한 자기 trigger의 이름을 보고 알아보며 개수로 알아보지 않는다. 두 setting을 함께 쓴 table은 두 renderer 출력의 합을 가지며 trigger 집합은 이름과 내용이 그 합과 정확히 같아야 한다. 더 있거나 빠지거나 내용이 다른 trigger는 그 table의 모든 trigger를 미지원으로 만든다. tests/dbspec/introspect.json은 audit과 immutable을 함께 쓴 table을 MySQL, PostgreSQL, SQLite에서 같은 문서로 다시 읽는 case를 두며, 모든 client의 introspection 검사가 이를 실행한다.—근거: 변경 전 make dbspec-introspect-check는 세 database에서 이 case를 실패시켰다. 변경 후 Go, TypeScript, PHP, PHP extension, Python, Rust의 introspection 검사가 세 database에서 통과하며, Go, TypeScript, PHP, Python의 단위 검사와 PHP extension compare가 통과한다.[o]
T130-3finder 규칙을 밝히고 공유 vector 하나로 모든 runtime에서 검증한다(T130-2 후속). 원인: T130-2는 vector 없이 완료로 표시되어 기준을 낮췄고, docs/usage.md는 모든 언어에서 모든 체인이 동작한다고 했는데 generator는 그렇게 하지 않는다. T130 변경은 선언된 symbol 다섯 개도 바꾸므로, 각 T130 code commit이 자기가 바꾼 symbol을 기록한다. 인수 조건: docs/interfaces.md의 "조회 체인"({#finder-chains})이 규칙을 정한다. 체인은 컬럼 key, 연산자, 연결자의 임의 조합이다. 정적 client(Go, Rust, TypeScript)는 모델 생성 scan이 찾은 체인을 선언하고, 동적 client(PHP, Python)는 호출이 실행될 때 이름을 해석하며, 같은 체인은 같은 row를 준다. docs/usage.md와 docs/usage.ko.md는 이 절을 연결한다. 공유 vector terminal_by는 unique가 아닌 column service_seq와 is_close에 getsByServiceSeqAndIsClose(7, false)를 실행하며, 다섯 runtime 모두 conformance 실행에서 MySQL 통과했다.—근거: fix(dbspec): Recognize settings by name in Go (#T130)가 catalog.triggerSetting을, fix(dbspec): Recognize settings by name in TypeScript (#T130)가 triggerSetting과 triggerModel을, fix(dbspec): Recognize settings by name in Rust (#T130)가 model과 trigger_setting을 기록하며, 각각 자기 hash를 contracts/interfaces.json에 둔다. record가 없는 interfaces 검사는 branch의 모든 commit에서 통과한다. tip의 hash는 go sha256:f821b72f2e3f202dfb6ea744a7c1006471f75851da1d61f2b6fbd816fa6368b7, rust sha256:5556d40413039166d9f1437b015c0070a90b6271aebcb55f0a893b525f5ed309, typescript sha256:2d9235f2b4660f07cae430d6269c72a776f5dc0a6fb94500c4494e96c016bb43이다(php와 python은 바뀌지 않았다).[o]
T130-2TypeScript finder를 scan으로 만들지 모든 column에 만들지 정하고, 그다음 client를 같게 한다(T130 후속). 원인: S-284 보고는 node 생성 모델이 finder를 unique key에만 만들고 php는 다른 column에도 만든다고 한다. 이 저장소에서 확인하면, TypeScript generator는 scan된 코드가 호출하는 체인마다 finder를 선언하며 unique가 아닌 column도 scan되면 getByStatus, getsByStatus, getCountByStatus를 얻는다. PHP는 finder를 생성하지 않고 Model::__call에서 모든 체인을 호출 시점에 해석한다. docs/interfaces.md는 두 설계를 모두 밝히고, docs/usage.md는 모든 언어에서 finder가 "모든 컬럼 체인을 받는다"고 밝히는데, scan이 보지 못한 TypeScript 체인은 그렇지 않다. 결정: TypeScript는 scan을 따른다(scan된 코드가 호출하는 체인마다 finder). PHP는 호출 시점에 해석한다. generator는 바꾸지 않는다.—근거: docs/usage.md와 docs/usage.ko.md가 그 내용을 적는다.[o]
T131code 주석을 영어로 번역한다. 원인: AGENTS가 code 주석은 한국어라고 적어, 주석은 영어라는 owner 규칙(kit PRINCIPLES, K84)과 어긋났다. 2026-10-13 기준 728개 파일에 한국어 주석 10,059줄이 남아 있다. 인수 조건: code 파일의 주석에 한글이 없으며, 이를 어기면 실패하는 검사가 check target에 들어 있다; AGENTS가 영어 규칙을 적는다. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Evidence(T131): tracked code 파일의 한글 주석 10,469줄을 모두 번역했고(T131-2..T131-7), make comment-language-check가 전체 tree에서 통과한다. Resumed: link-owner 판정(2026-10-18): 지금 로컬 전용 개발로 시작한다. 주석 번역은 protocol 항목과 충돌하지 않는다. Evidence(T131-1): scripts/comments/check.mts의 검사가 단위 case 23개를 통과하고, make comment-language-check는 tree에서 한글 주석 10,469줄을 파일과 줄을 대며 실패한다.——[o]
T131-1주석 언어 검사: 각 생태계 자신의 tokenizer(Go go/scanner, TypeScript scanner, Python tokenize, PHP token_get_all; tokenizer가 없는 언어는 먼저 S1의 rebuild 기록)로 모든 code 파일의 주석 token을 꺼내 한글이 있으면 파일과 줄을 대며 실패하는 얇은 도구; 현재 tree에서 red다. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Resumed: link-owner 판정(2026-10-18): 지금 로컬 전용 개발로 시작한다. 주석 번역은 protocol 항목과 충돌하지 않는다. Evidence(T131-1): scripts/comments/check.mts의 검사가 단위 case 23개를 통과하고, make comment-language-check는 tree에서 한글 주석 10,469줄을 파일과 줄을 대며 실패한다.——[o]
T131-1-1정규식 lexer 대신 각 생태계의 tokenizer로 주석을 읽는다(T131-1을 따른다, coordinator 검토). Go: go/scanner(scripts/comments/tokens/go-comments.go). TypeScript: 고정된 interface 프로젝트(packages/ts/testing/interfaces/typescript, TypeScript 6.0.3)의 TypeScript parser. Python: tokenize와 docstring의 ast(tokens/python-comments.py). PHP: token_get_all(tokens/php-comments.php). Rust, C, hash 주석 파일은 scripts/comments/own-lexer.mts를 쓰며 재구현 기록 docs/rebuild/rust-comments.md, c-comments.md, hash-comments.md가 그 이유를 적는다. Cause: 정규식 lexer가 tokenizer를 다시 구현했고 한글 주석 295줄을 놓쳤으며, 이 항목에서 번역한다.—Evidence: scripts/comments/check.test.mts가 단위 case 25개를 통과한다(먼저 red: 모듈에 commentsByFile이 없었다). node scripts/comments/check.mts가 전체 tree에서 통과한다. 번역된 모든 파일의 code는 주석을 뺀 cf01f139와 같다(tokenizer와 AST 비교, 일회성 검증).[o]
T131-2Go 주석을 영어로. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Resumed: link-owner 판정(2026-10-18), 로컬 전용 개발. Evidence(T131-2): packages/go, examples의 Go 주석은 영어이며, T131의 Go unit commit마다 해당 파일이 주석 검사를 통과한다.——[o]
T131-3TypeScript와 JavaScript 주석을 영어로. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Resumed: link-owner 판정(2026-10-18), 로컬 전용 개발. Evidence(T131-3): packages/ts/src, packages/ts/tests, packages/ts/testing의 TypeScript 주석은 영어이며, node scripts/comments/check.mts packages/ts가 통과한다.——[o]
T131-4PHP와 PHP extension 주석을 영어로. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Resumed: link-owner 판정(2026-10-18), 로컬 전용 개발. Evidence(T131-4): packages/php, packages/php-ext, PHP 예제의 주석은 영어이며, 각 unit commit이 해당 파일의 주석 검사를 통과한다.——[o]
T131-5Python 주석을 영어로. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Resumed: link-owner 판정(2026-10-18), 로컬 전용 개발. Evidence(T131-5): packages/python의 주석은 영어이며, 각 unit commit이 해당 파일의 주석 검사를 통과한다.——[o]
T131-5-1Python docstring 세 개를 복구한다(T131-5 follows). Cause: 한 줄 docstring 번역이 닫는 triple quote를 잃어 packages/python/src/polyspec/orm/dbspec/emit.py, dbspec/parse.py, testing/init.py 세 module이 compile되지 않았다(T131-1-1의 tokenizer 검사가 찾았다).—Evidence: 각 파일이 python3 -c "compile(...)"로 compile되며, 번역된 모든 Python 파일의 code는 docstring을 뺀 AST 비교에서 cf01f139와 같다.[o]
T131-6Rust 주석을 영어로. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Resumed: link-owner 판정(2026-10-18), 로컬 전용 개발. Evidence(T131-6): packages/rust, benchmarks/rust, Rust 예제의 주석은 영어이며, 각 unit commit이 해당 파일의 주석 검사를 통과한다.——[o]
T131-7Makefile, script, 나머지 파일을 영어로; T131-1의 검사가 통과하고 check target에 들어간다. Cause: owner 판정(2026-10-15): flowmark가 받을 release는 protocol 항목을 먼저 끝내고, 이 항목은 그 release 뒤로 미룬다. Resumed: link-owner 판정(2026-10-18), 로컬 전용 개발. Evidence(T131-7): Makefile, scripts, tests, 예제의 주석은 영어이며, comment-language-check는 CHECK_TARGETS와 CI static group의 target이고 전체 tree에서 검사가 통과한다.——[o]
T131-8Rust interface snapshot가 번역된 doc comment와 일치한다 (T131에 이어). 원인: T131 번역이 contracts/symbols/rust.json에 기록된 Rust signature 40개의 doc text를 바꿔서, cebf00eb에서 통과하던 interfaces/rust가 833af162에서 실패했다.—증거: 재기록 뒤 make interface-check/self-test가 exit 0이고 interfaces/rust가 ok이다(var/ic-after.log); 재기록은 그 40개 symbol의 기록된 signature text만 바꿨다(41행).[o]
T131-8-1Unicode word 표를 영어 text로 생성한다 (T131-8에 이어). 원인: T131이 생성된 다섯 표(php-ext, php, python, rust, ts)를 손으로 번역했지만 생성기 tests/dbspec/unicode/main.go는 한국어 header를 그대로 두었다. 그래서 go run ./tests/dbspec/unicode -write가 표를 한국어로 다시 쓰고 TestCommittedTablesAreGenerated가 다섯 파일에서 실패했다(bccfc382의 전체 parity 실행 unit go-internal-tests-dbspec-unicode-main, exit 1). 완료 기준: 생성기가 영어 text를 내고, 커밋된 표가 그 출력과 같으며, unicode test가 통과한다.—증거: RED: go test -run TestCommittedTablesAreGenerated ./tests/dbspec/unicode가 다섯 표에서 실패. GREEN: 생성기의 literal 14개를 영어로 바꾸고 표를 다시 생성한 뒤 같은 test가 통과한다(코드 줄은 없고 주석 20줄이 다섯 파일에서 바뀜).[o]
T131-9owner 선택에서 comment-language-check를 기대한다 (T131에 이어). 원인: T131이 모든 source 파일을 입력으로 하는 comment-language-check를 추가해 source 변경이 이것을 선택하지만, scripts/features/owners.test.mts의 선택 case 두 개는 여전히 이것 없는 target을 기대했다. 완료 기준: 두 case가 comment-language-check를 기대하고 feature-unit-check가 통과한다.—증거: RED: main에서 make -s feature-unit-check가 a change across the four clients selects no full-suite target과 a generator source selects the check that regenerates the models로 실패했고, 각각 + 'comment-language-check'였다. GREEN: 두 기대값이 이것을 이름 붙인 뒤 make -s feature-unit-check가 exit 0이다.[o]
T131-10stub 번역 뒤 extension의 arginfo header를 다시 생성한다 (T131에 이어). 원인: T131(4168b88d)이 packages/php-ext/stubs/orm_dbspec.stub.php의 주석을 번역해 gen_stub.php가 src/orm_dbspec_arginfo.h에 쓰는 stub hash가 바뀌었지만 header를 다시 생성하지 않았다. 완료 기준: header를 gen_stub.php로 다시 생성하고 그 검사가 통과한다.—증거: RED: main에서 make -s dbspec-php-extension-check/arginfo가 src/orm_dbspec_arginfo.h differs from the stub로 실패했다. GREEN: php packages/php-ext/scripts/arginfo.php .runtime/bin/gen-stub/gen_stub.php write 뒤(stub hash 줄만 바뀜) 검사가 exit 0이다.[o]
T131-11번역 뒤 extension의 mirror test를 client와 같게 유지한다 (T131에 이어). 원인: T131이 mirror test 다섯 개(DbspecPlanTest, DbspecMermaidTest, DbspecIntrospectTest, DbspecApplyCleanupTest, DbspecApplyTest)의 주석을 packages/php/tests와 packages/php-ext/tests에서 다르게 번역했지만, DeclarationsTest::test_mirrors는 두 본문이 같기를 요구한다. 완료 기준: 각 extension 사본이 header 뒤에 client test의 본문을 갖고 test_mirrors가 통과한다.—증거: RED: main에서 make -s dbspec-php-extension-check/declarations가 packages/php-ext/tests/DbspecPlanTest.php differs from packages/php/tests/DbspecPlanTest.php after its header로 실패했고, 다섯 파일 모두 주석과 docblock 줄만 달랐다. GREEN: client 본문을 복사한 뒤(주석 53줄) target이 두 번 통과하고 각각 test 5개다.[o]
T133모든 runtime에서 공유 dbspec vector의 모든 절을 엄격하게 읽는다. 원인: tests/dbspec/cases.json의 TypeScript, Python, Rust reader는 새 positions 절만 엄격하게 읽고, 다른 절은 모르는 field를 무시하고 위치로 읽으므로, 철자가 틀린 field가 조용히 버려진다(T128-3p agent들이 찾음). 인수 조건: 각 runtime의 reader가 모든 절에서 모르는 field와 없는 field를 이름을 대며 거부하고, runtime마다 거부 test가 있다; Go의 reader도 마찬가지다(정정: item은 Go reader를 기준이라 했지만 그것도 모르는 field를 받아들였다).—증거: 변경 전 go test ./engine/dbspec -run TestVectorLoadersRejectMalformedVectors가 새 mutation 11개에서 실패했고 make dbspec-compare-check/runners가 319 case 중 120개(mutation 20개 × runner 6개)에서 실패했으며, 각 runner는 잘못된 field를 지나쳐 읽었다; 변경 후 runtime마다 엄격한 reader 하나(tests/dbspec/cases.mjs, tests/dbspec/cases.php, dbspec_cases_reader.py, common/dbspec_cases.rs, Go fields helper와 compare/go/cases.go)가 <file>: <location> <problem>으로 이를 거부하고, runner가 319개 모두 통과하며, runtime별 거부 test가 통과한다.[o]
T134Python test를 설치된 package에 대해 실행한다. 원인: test 파일 54개가 src를 sys.path에 넣는데, S3는 이를 금지한다(test는 설치된 package를 대상으로 하며 경로를 바꾸지 않는다). 인수 조건: test는 venv에 설치된 package를 import하고, sys.path 변경이 남지 않으며, 검사가 그것을 거부한다.——[o]
T134-1packages/orm-python/bin/orm-gen의 sys.path 삽입을 없앤다 (T134에 이어). 원인: venv가 orm-gen console script를 설치하는데도 이 script가 여전히 src를 삽입한다. 완료 조건: wrapper를 없애거나 설치된 package를 import하고, 모든 호출자가 console script를 쓴다.——[o]
T135test 서버 socket을 Unix socket 경로 한도 안에 둔다. 원인: scripts/test-servers.sh가 socket을 checkout 아래에 두어, 긴 worktree 경로에서 proxysql-admin-pgsql.sock이 104 byte로 macOS 한도 103을 넘어 database test가 시작되지 못했다(T128-3p Python agent가 찾음). 인수 조건: socket 경로는 길이를 검사하는 짧은 선언된 장소에서 오며 넘으면 이름을 댄 오류를 내고, 긴 경로로 red를 먼저 본다.——[o]
T135-1scripts/test-servers.sh와 그 테스트의 주석을 영어로 쓴다 (T135에 이어). 원인: check_socket_paths와 테스트 주석이 한국어인데 AGENTS는 code 주석을 영어로 정한다. 완료 조건: 그곳의 모든 주석이 영어다.——[o]
T136새 worktree에서 make install을 재현하고 고친다. 원인: T128-3p agent가 새 worktree에서 make install이 Python 도구에서 실패하며 uv가 선언되지 않았다고 보고했다; uv는 선언되어 있으므로(config/toolchain.json) 보고된 원인은 틀렸고 진짜 원인은 모른다. 인수 조건: 새 worktree에서 실패를 정확한 출력과 함께 재현하고, 원인을 고치거나 보고가 틀렸음을 보이며, red를 먼저 본다.—증거: 원인은 kit v0.0.31(T126-1)이다. 그 install-tools는 Python을 uv로 설치하며 uv 없이 요청한 python을 거부하는데, install-python은 TOOLS=python을 요청했다; CI run 38413640279에서 Python이 필요한 모든 group이 Python client dependencies 단계에서 실패했다. 변경 전 새 checkout의 make install-python이 install-tools에서 실패했고, 변경 후(TOOLS="uv python") uv Python으로 packages/orm-python/.venv를 만든다.[o]
T136-1install-python test에서 uv를 Python 앞에 고정한다(T136에 이어). 원인: scripts/repo/python-venv.test.mjs가 아직 install-tools.mjs python을 대조해 T136 뒤 CI run 38421077014의 repo-check가 실패했다. 인수 조건: test가 install-tools.mjs uv python을 요구한다.—증거: make repo-check/unit이 그 case를 통과한다; make -n install-python이 install-tools.mjs uv python을 출력한다.[o]
T137kit의 현재 release와 그 표준을 받아들인다. 원인: orm은 kit이 표준 S1–S9를 담은 PRINCIPLES.md와 TypeScript 도구를 갖기 전의 v0.0.31을 vendor한다; orm AGENTS.md:31은 아직 packages/orm-<kind>를 정하는데, 폴더 표준 S5는 이를 거부한다(repository prefix 없음, runtime port는 packages/<runtime>/). 인수 조건: kit 최신 release로 make kit-sync; AGENTS.md(+ko)가 vendor한 PRINCIPLES.md를 link하고 orm 고유 규칙만 남긴다; config/layout.yml이 kit 공통 규칙 위에 orm을 매핑하며 예외마다 이유를 단다; runtime package를 packages/<runtime>/(go, ts, php, php-ext, python, rust)로 옮기고 모든 경로, manifest, module path, workflow를 호환 경로 없이 갱신한다; make layout-check와 make kit-check가 통과한다.——[o]
T137-1폴더를 kit layout의 S5, S7, S9에 맞춘다(T137 다음; kit 0.0.38에서 kit K79가 기록한 red). 원인: client 폴더가 runtime 이름을 가진 뒤에도 make layout-check가 223개 finding을 보고하며 첫 줄은 bench: error: layout-root-folders: S5: the root holds only packages/, contracts/, docs/, scripts/, tests/, examples/, benchmarks/ and config/이다. 루트 Go module(engine/, generator/, cmd/, internal/)이 S5를 어기며, engine, generator, tests/의 Go program이 어디에 있을지 결정이 필요하다. 수용 기준: make layout-check가 통과하고, config/layout.yml의 모든 예외가 이유를 가진다.—Evidence: 모든 하위 항목이 완료됐다(T137-1-1부터 T137-1-9, 그리고 하위 항목을 가진 T137-1-4). Go module은 packages/go이고, root에는 S5의 folder와 file만, tests/에는 선언된 runner만 있으며, 모든 S9 예외가 이유를 담는다. make layout-check가 finding 없이 통과한다.[o]
T137-1-1config/layout.yml에 packages/dbspec-vscode를 선언하고 T134가 codec-check/python과 tests/conformance/result_python.py에 남긴 PYTHONPATH를 없앤다 (T137-1에 이어). 완료 조건: layout-check가 둘 다 보고하지 않고, Python codec vector와 conformance runner test가 통과한다.——[o]
T137-1-2PHP client를 PSR-4만으로 map한다 (T137-1에 이어). 원인: lint-composer-policy(S2)가 packages/php/composer.json의 classmap을 거부한다. source file 11개(Codec, Markdown, Engine, GroupRows, Model, AesKeyring, Db, Planner, Collection, Utils, Orm)가 class를 여럿 담아 classmap이 필요하고, library가 autoload-dev를 map하지 않는다. 완료 조건: file마다 class 하나로 PSR-4를 따르고, classmap이 없으며, autoload-dev를 PSR-4로 map하고, PHP 검사가 통과한다.——[o]
T137-1-3workspace의 Rust lint를 받는다 (T137-1에 이어). 원인: lint-cargo-lints가 모든 Cargo.toml(11)을 거부한다. workspace가 unsafe_code와 clippy::allow_attributes_without_reason을 deny하고 각 package가 그것을 받아야 한다. orm에는 unsafe 사용 5곳(case-clock, SQLite authorizer, planner scope)과 이유 없는 allow attribute 20개가 있다. 완료 조건: lint를 선언하고, 각 unsafe 사용을 없애거나 이유와 함께 허용하며, 모든 allow가 이유를 적고, rust-check가 통과한다.——[o]
T137-1-5Go module을 packages/go로 옮긴다 (T137-1에 이어). 원인: root Go module(go.mod, go.sum, engine/, generator/, cmd/, internal/)이 S5를 어겼고, ordered-json 0.0.9는 같은 layout을 위해 Go module을 packages/go로 옮겼다. 완료 조건: module은 release tag packages/go/vX.Y.Z의 github.com/polyspec/orm/packages/go이고, engine, generator, cmd/orm-gen, internal/testdb가 그 아래에 있으며, tests/의 Go program이 import하는 testcase와 procevent는 거기의 공개 package다. layout-check가 그 경로를 하나도 보고하지 않는다.—Evidence: packages/go에서 go build와 go vet이 통과한다. database 없이 engine과 그 하위 package 여섯, generator, testcase의 go test가 통과하고 model generation coverage test가 통과한다. Go symbol을 다시 기록해(경로 접두어만 바뀜) make interface-check가 통과하고, repo, feature, release-coverage, dependency-policy, 문서 검사가 통과한다.[o]
T137-1-6tests/와 examples/의 Go program에 workspace 안의 자기 module을 준다 (T137-1-5에 이어). 원인: root module을 옮긴 뒤 tests/와 examples/ 아래 Go program과 test는 어느 module에도 속하지 않았고, kit의 Go lint는 replace를 금지한다. 완료 조건: tests/go.mod와 examples/go.mod가 config/layout.yml이 선언한 root go.work의 module이고, replace가 필요 없으며, contracts/의 Go test는 tests/interfaces/contracts에 있다.—Evidence: tests와 examples에서 go build와 go vet이 통과하고, tag를 붙인 vet(physical, bench, markdown, decimalphysical, examples, featurecoverage, ormtest)이 세 module에서 통과한다. database 없이 tests/interfaces/contracts, tests/dbspec/unicode, tests/conformance/runner_go, tests/dialects의 go test가 통과하고, make go-vet-check, go-fmt-check, testcase-check/go가 통과한다.[o]
T137-1-7bench folder와 root file을 layout으로 옮긴다 (T137-1에 이어). 원인: layout-check가 root의 bench/와 schema/(S5), tests/schema(S7), 그리고 mise.toml이 선언한 Composer release를 되풀이하는 .composer-version(S5)을 보고했다. 완료 조건: bench/는 benchmarks/, 공유 bench schema와 그 set은 contracts/fixtures/bench/, cargo runner의 binaries schema는 scripts/schema/binaries.schema.json이고, .composer-version을 지우고 repository 검사는 smol-toml로 mise.toml의 Composer release를 읽으며, make layout-check가 통과한다.—Evidence: make layout-check가 finding 없이 통과한다. 옮긴 경로로 python-unit-check, PHP engine, runtime-model, orm-gen-scan unit, TypeScript engine, generate, generate-scan test, Rust dbspec_runtime과 ir_validate test, benchmarks/rust의 cargo check, engine과 generator의 go test, model generation coverage test가 통과한다. scripts/repo와 scripts/check 143건이 통과하고, smol-toml 1.9.1을 review해 dependency-policy-check가 통과한다.[o]
T137-1-8layout 이동 뒤 CI를 통과시킨다 (T137-1-5와 T137-1-7에 이어). 원인: e7d8b309의 CI가 job 여섯에서 실패했다. TypeScript, PHP, PHP extension, Rust의 introspection test가 지운 schema/ folder를 훑었고, Python native rule test가 contracts/validate.go를 옛 위치에서 읽었으며, rust-fmt-check가 format하지 않은 test 둘을 찾았고, T137-1-4-6에서 옮긴 conformance unit test가 임시 folder 17개를 남겼다. 완료 조건: test가 contracts/fixtures/bench와 tests/interfaces/contracts를 읽고, cargo fmt --check가 통과하며, conformance unit test가 임시 folder를 남기지 않는다.—Evidence: CI record가 각 실패를 적는다. 변경 뒤 python-interface-unit-check가 통과하고, packages/rust와 benchmarks/rust에서 cargo fmt --all --check가 통과하며, tests/conformance/check의 main, state, state-run, lock test가 7개 넘게 남기던 folder를 0개 남기고 통과한다. scripts/repo와 scripts/check 143건이 통과하고 make layout-check가 통과한다.[o]
T137-1-9Go analyzer의 owner를 선언한다 (T137-1-4-2에 이어). 원인: d14cb25c의 CI가 feature-unit-check/owners에서 실패했다. T137-1-4-2가 더한 tests/analyzers의 file이 behaviour test를 고르지 않고 scope도 선언하지 않았으며, merge가 owners 검사를 실행하지 않았다. 완료 조건: contracts/check-inputs.json이 analyzer를 build하고 실행하는 go-vet-check의 input으로 tests/analyzers/**를 적고, feature-unit-check/owners가 통과한다.—Evidence: CI record가 analyzer file 8개와 testdata file 11개를 적는다. 변경 뒤 make feature-unit-check/owners가 통과하고 repository와 feature 검사가 통과한다.[o]
T137-1-3-1Rust model을 allow attribute 없이 생성한다 (T137-1-3에 이어). 원인: orm-build가 모든 model에 #[allow(unused_mut)]와 #[allow(clippy::new_without_default)]를 써서, allow attribute를 거부하는 consumer(kit workspace lint를 쓰는 fsm)가 실패했다.—Evidence: build test generated_code_carries_no_allow_attribute가 수정 전 실패하고 수정 후 통과한다. rust-check, rust-fmt-check, interface-check가 통과한다. Evidence: make layout-check가 lint-cargo-lints finding을 보고하지 않는다(이전 11건). rust-check, rust-fmt-check, dbspec-rust-check, interface-check, dependency-policy-check가 통과한다.[o]
T137-1-4언어를 넘는 infrastructure를 TypeScript로 쓴다 (T137-1에 이어). 원인: S9(layout-infrastructure-typescript, layout-infrastructure-shebang)가 scripts/와 tests/ 아래 다른 interpreter의 script 198개를 거부한다. 완료 조건: 각각을 TypeScript로 옮기거나 config/layout.yml에 이유와 함께 S9 예외로 선언한다.—Evidence: 모든 하위 항목이 완료됐다. infrastructure script와 언어 간 orchestrator는 TypeScript이고(T137-1-4-1, -5, -6, -9), reader는 parser이며(-2, -2-1, -2-2), 모든 runner가 공유 schema로 vector를 읽고(-3, -3-1), kit 0.0.47 lint가 통과한다(-7, -7-1). make layout-check가 S9 finding을 보고하지 않는다.[o]
T137-1-4-1JavaScript 기반 스크립트를 TypeScript로 옮긴다 (T137-1-4에 이어). 원인: scripts/와 tests/의 .mjs 스크립트와 test 82개(kit의 사본 제외)는 어떤 compiler도 검사하지 않는 타입 없는 JavaScript였고, JSON file을 JSON.parse와 손으로 쓴 field 검사로 읽었으며, 인자 해석, glob, 경로 결합, version 비교와 JSON Schema 검증기를 다시 구현했다. 완료 조건: 각각이 kit의 엄격한 option으로 tsc가 검사하는 .mts file이고(make scripts-typecheck, tsconfig.json을 소유하는 CHECK_TARGETS target), 읽는 모든 JSON file(기능 manifest, check input, npm과 Composer manifest와 lock, go test와 cargo 메시지, 실행 기록, rule registry, dbspec과 conformance vector)을 ajv가 JSON Schema로 읽으며(scripts/json/schema.mts), schema가 말할 수 없는 규칙만 code에 남는다. node:util parseArgs, minimatch, path.posix, semver가 다시 만든 구현을 대신하고, 동작과 출력은 바뀌지 않는다.—Evidence: tsc -p tsconfig.json이 오류를 보고하지 않는다. TypeScript compare runner는 바꾸기 전과 후에 같은 27524줄을 출력하고 변형한 vector 53개를 공유 형식으로 거부한다. interface reader는 같은 3507줄의 symbol을 쓴다. 저장소, 기능, version, 문서, 규칙 검사와 scripts/, tests/의 test가 바꾸기 전부터 알려진 로컬 실패를 빼고 통과한다.[o]
T137-1-4-2기반 스크립트의 손으로 쓴 해석기를 바꾼다 (T137-1-4-1에 이어). 원인: scripts/repo/ci.mts가 orm이 이미 의존하는 yaml package 대신 GitHub workflow를 줄 단위로 읽고, scripts/repo/testcases.mts가 셸 명령을 자기 segments로 나누며, scripts/repo/gosource.mts와 scripts/repo/check.mts의 Go 규칙은 Go source를 정규식으로 읽는다. S9은 언어별 검사를 그 생태계 분석기(go/analysis)의 plugin으로 둔다. 완료 조건: workflow를 YAML 문서로 읽고, 셸 명령을 관리되는 셸 파서로 나누되 모든 recipe와 스크립트에서 지금 결과와 같으며, Go 검사는 go vet이 실행하는 go/analysis analyzer다. 각각 red case를 둔다.—Evidence: workflow는 YAML document로 읽고(scripts/repo/workflow.mts, unique key의 yaml parseDocument), shell 명령은 unbash 5.1.0으로 읽으며(scripts/repo/shell.mts), Go 규칙은 go vet -vettool이 실행하는 go/analysis analyzer 다섯이다(tests/analyzers, make go-vet-check/orm). repository의 입력 638개에서 새 segment는 631개가 이전과 같고, 다른 7개는 이전 reader가 명령으로 읽은 shell keyword와 조각이다. RED: go vet을 가린 escape된 quote, 명령으로 읽힌 주석, 놓친 flow style step, timeout-minutes로 읽힌 with: 입력, 아무것도 아닌 것으로 읽힌 깨진 YAML을 각각 고쳤다. 이전 Go 정규식은 각 analyzer testdata의 case를 놓치거나 잘못 보고했고, analyzer는 정확히 보고한다. scripts/repo와 scripts/check 143건, go-vet-check, analyzer test, layout, dependency-policy, 문서 검사가 통과한다.[o]
T137-1-4-2-1남은 run text와 Makefile 규칙을 parser로 읽는다 (T137-1-4-2에 이어). 원인: T137-1-4-2 뒤에도 ciDuplicateCommandErrors는 step run text의 줄을 feature 명령과 문자열로 비교하고, scripts/repo/toolchains.mts의 runs()와 ciServerErrors는 step의 run 값과 Makefile text에 정규식을 적용한다. 완료 조건: 각각 명령을 scripts/repo/shell.mts로, Makefile을 recipe reader로 읽고, 각각 red case가 있다.—Evidence: scripts/repo/shell.mts가 unbash tree에서 각 단순 명령(commands)과 모든 출력 redirect 대상(writtenFiles)을 준다. ciDuplicateCommandErrors는 step의 명령과 makeRecipes의 recipe를 읽고, ciServerErrors는 명령이 할당하거나 GITHUB_ENV에 쓸 때만 변수를 준 것으로 세며 env 쓰기와 symbolic link를 parse한 명령에서 찾고, toolchains.mts의 runs()는 parse한 명령을 검사한다. RED: ln -s를 적은 주석, redirect를 echo한 명령, DSN 변수를 적은 주석, 중복 명령 앞의 set -e, && 뒤의 중복, go test나 rustup을 echo한 명령, 주석의 install을 이전 reader가 각각 잘못 판단했고 지금은 맞게 판단한다. scripts/repo와 scripts/check 146건, repository, owners, lint 검사가 통과한다.[o]
T137-1-4-2-2Makefile target과 rule을 reader 하나로 읽는다 (T137-1-4-2-1에 이어). 원인: makeRecipes는 slash를 담은 target(testcase-check/go)에 이름을 주지 않아 make a/b로 실행한 중복을 검사하지 못하고 scripts/features/owners.mts가 그런 target을 찾지 못한다. scripts/repo/ci.mts의 makeRules는 아직 target 줄을 정규식으로 읽는다. 완료 조건: Makefile reader 하나가 모든 target을 prerequisite, recipe와 함께 이름 짓고 검사가 그것을 쓰며, typescriptHolderErrors는 ts-check/hold 같은 prerequisite로 build를 잡는 target을 받아들인다. 각각 red case가 있다.—Evidence: scripts/repo/testcases.mts의 makeTargets가 reader 하나다. slash와 pattern target을 포함한 모든 target을 prerequisite(continuation과 order-only), recipe와 함께 이름 짓고, makeRecipes, makeRules, independentTestErrors, recipe(), usesTestEnv, offlineMakeErrors가 그것을 쓴다. typescriptHolderErrors는 prerequisite를 따라간다. 이전 code의 RED: slash target의 usesTestEnv, make perf-check/run으로 실행한 중복, continuation 줄의 prerequisite, hold 없이 build하는 slash target, fetch/a와 fetch/b의 download를 각각 놓쳤고 지금 모두 통과한다. scripts/repo와 scripts/check, repository, feature, owners, lint 검사가 통과한다.[o]
T137-1-4-3모든 compare runner가 dbspec vector를 공유 JSON Schema로 읽는다 (T137-1-4-1에 이어). 원인: TypeScript runner는 tests/dbspec/*.json을 tests/dbspec/schema/로 읽지만 Go, PHP, PHP 확장, Python, Rust runner는 자기 field 검사를 두어 형식이 여섯 번 적힌다. 완료 조건: 각 runner가 자기 생태계의 JSON Schema validator로 같은 schema를 검증하고 거부를 tests/dbspec/compare/runners.test.mts의 공유 형식으로 보고하며, 자기 field 검사를 지운다.—Evidence: Go(santhosh-tekuri/jsonschema 6.0.3), PHP와 PHP extension(opis/json-schema 2.6.0), Python(jsonschema-rs 0.58.6), Rust(jsonschema 0.58.6) runner가 tests/dbspec/schema/로 검증하고 공유 형식으로 쓰며, 자기 field 검사는 지웠다. RED: runners.test.mts의 새 mutation 10개 중 8개를 TypeScript가 아닌 runner 다섯이 모두 받아들였고(실패 40건, 모두 exit 0), 변경 뒤 통과한다. dbspec-compare-check(runner 379/379, 12 실행 일치)와 Go, TypeScript, PHP, PHP extension, Rust, Python의 dbspec 검사가 통과한다.[o]
T137-1-4-3-1PHP runner가 vector file의 schema 거부를 모두 보고한다 (T137-1-4-3에 이어). 원인: opis/json-schema 2.6.0은 모든 오류를 모으는 mode에서 첫 거부 property에서 properties keyword를 멈춰 검사한 property를 기록하지 않으므로, additionalProperties가 그 뒤 property를 모두 unknown으로 보고한다. 그래서 PHP와 PHP extension runner는 기본 mode로 file의 첫 거부만 보고하고, 다른 runner는 모두 보고한다. 완료 조건: orm이 의존하는 기반인 fork min-0-max/json-schema가 모든 오류를 모으는 mode에서 실패한 property도 검사한 것으로 기록하고 모든 keyword group을 실행하는 release를 내고 orm이 그것을 pin하며, PHP와 PHP extension runner가 모든 거부를 공유 형식으로 보고한다. 어느 단계도 upstream을 기다리지 않는다.—Evidence: fork(v2.6.0-p1)는 실패한 property를 검사한 것으로 두고, 실패한 group 뒤에도 unevaluatedProperties를 실행한다. library의 새 case 4개 중 3개가 red였고 library suite 6281건이 모두 통과한다. orm은 fork의 vcs repository(no-api)에서 opis/json-schema 2.6.0-p1을 요구하고, tests/dbspec/vectors.php는 stop_at_first_error false로 검증한다. runners.test.mts의 새 refusals case가 php와 php-extension에서 실패했고(한 줄) 지금 모든 runner에서 통과한다. dbspec-php-check, dbspec-php-extension-check, dbspec-compare-check, dependency-policy-check가 통과한다.[o]
T137-1-4-4S9 예외를 이유와 함께 선언한다 (T137-1-4-1에 이어). 원인: make layout-check가 언어별 harness(tests/conformance, tests/dbspec, tests/interfaces의 언어별 runner), engine dialect 규칙의 Go test(tests/dialects), Node에 없는 운영체제 호출이 필요한 Go program(tests/lease, tests/new-session, tests/stop-process), codec vector의 PHP 생성기를 보고하는데, S9에서 각각은 자기 생태계의 test나 도구다. 완료 조건: config/layout.yml이 각각을 그 위 주석의 이유와 함께 예외로 선언하고, layout-check는 T137-1-4-5와 T137-1-4-6이 옮길 스크립트만 보고한다.—Evidence: make layout-check는 S9을 scripts/의 스크립트 19개와 두 orchestrator tests/conformance/check, tests/interfaces/check에만 보고한다. AES vector가 어디 있는지만 출력하고 아무도 실행하지 않던 tests/codec/gen-aes.sh는 예외로 선언하는 대신 지웠다.[o]
T137-1-4-5scripts/의 셸과 PHP 스크립트를 TypeScript로 옮긴다 (T137-1-4-1에 이어). 원인: scripts/의 셸 스크립트 18개와 PHP 스크립트 2개는 다른 interpreter의 기반 스크립트다(S9). 완료 조건: 각각이 같은 동작의 .mts 스크립트이고, 호출하는 곳이 그것을 실행하며, layout-check가 그 무엇도 보고하지 않는다.—Evidence: layout-check가 scripts/ 아래 file을 보고하지 않는다. make test-servers가 scripts/test-servers.mts로 server 여섯을 시작하고, 두 번째 start는 실행 중이라고 보고하며, test-servers-tls가 TLS를 다시 읽는다. make databases-create가 실행의 bench와 decimal database를 만들어 seed하고 databases.mts drop이 그것을 지우며 binary log를 purge한다. test-servers-check 14 case(실제 server의 migration 포함), scripts/repo와 scripts/check 143건이 통과하고, shell reader로는 로컬에서 실패하던 startLogged case가 통과한다.[o]
T137-1-4-6여러 언어를 다루는 Go orchestrator를 TypeScript로 옮긴다 (T137-1-4-1에 이어). 원인: tests/conformance/check와 tests/interfaces/check는 모든 언어를 실행하고 비교하므로 S9에서 TypeScript 기반 스크립트인데 Go program이다. 완료 조건: 각각이 같은 출력과 기록의 .mts program이고, layout-check가 둘 다 보고하지 않는다.—Evidence: tests/conformance/check와 tests/interfaces/check는 .mts program이고 make layout-check가 둘 다 보고하지 않는다. Go program을 지우기 전, TypeScript interface checker는 모든 언어, --self-test, --results에서 Go와 같은 출력, 종료 상태, byte 단위로 같은 symbol record를 냈다. make run-databases TARGETS=conformance-check가 MySQL, PostgreSQL, SQLite에서 27 vector를 5 언어로 같게 실행해 통과했다. 둘의 unit test 46건, scripts/repo와 scripts/check 143건, make interface-check가 통과한다. 둘 다 더는 읽지 않는 internal/stmtdiff는 지웠고 그 case는 stmtdiff.test.mts에 있다. contracts/validate.go의 contract 검증은 그 Go test에서 계속 실행된다.[o]
T137-1-4-7기반 스크립트가 kit 0.0.46의 TypeScript lint 규칙을 통과한다 (T137-1-4-1에 이어). 원인: kit 0.0.46은 no-explicit-any, no-unsafe 규칙, no-non-null-assertion, type assertion 규칙을 오류로 만드는데, test는 아직 JSON.parse 값과 any 매개변수 두 개를 쓴다. 완료 조건: make kit-sync KIT_TAG=v0.0.46과 scripts/, tests/의 lint가 통과한다.—Evidence: make kit-sync로 kit 0.0.47을 받았다(kit-check 통과. config/release.json은 buildBackend를 빼고, config/layout.yml은 K145대로 vars.root_files로 root file을 적는다). make lint-ts의 oxlint가 scripts/와 tests/에서 154건에서 0건이 됐고, 각각 disable이나 cast 없이 code에서 고쳤다. root project의 tsc, scripts/repo와 scripts/check(143)가 통과한다.[o]
T137-1-4-7-1package와 site가 kit 0.0.47의 TypeScript lint를 통과한다 (T137-1-4-7에 이어). 원인: T137-1-4-7 뒤 make lint-ts가 packages/와 docs/.vitepress에서 약 1,270건(non-null assertion, 불필요하거나 안전하지 않은 type assertion, 안전하지 않은 any)과 knip finding을 보고했고, lint는 package를 root의 엄격함(noUncheckedIndexedAccess, exactOptionalPropertyTypes) 없이, dbspec-language CLI와 site는 추론한 project로 판단했다. 완료 조건: lint하는 모든 file을 그 build가 쓰는 project로 root의 엄격함에서 판단하고, make lint-ts, build, client test가 통과하며, package entry point의 export signature는 바뀌지 않는다.—Evidence: make lint-ts가 0으로 끝난다(oxlint 0건, knip 통과. 남은 hint는 kit이 K176으로 지우는 복사한 knip.json의 것이다). package가 noUncheckedIndexedAccess와 exactOptionalPropertyTypes를 받고, dbspec-language CLI와 docs/.vitepress가 자기 project를 가지며, packages/ts가 project reference로 build해 knip이 dist를 src로 대응시킨다. typescript-build, editor-build, ts-check/types와 ts-check/test, dbspec-language, lsp, codemirror, vscode 검사, docs-dbs-check, docs-build, scripts-typecheck, interface-check, client-db-check의 TypeScript lane이 통과한다. 바뀐 symbol key 81개는 export하지 않은 helper와 어느 entry point도 다시 export하지 않는 export다.[o]
T137-1-4-8TypeScript 이전 뒤 main의 CI를 통과시킨다 (T137-1-4-1에 이어). 원인: 297b6afd에서 CI가 T137-1-4-1이 남긴 네 가지로 실패했다: documents 밖의 main document를 다루는 packages/ts/tests/dbspec.mjs의 변형이 이전 초안의 pointer를 기대했고(ts-check, ts-min-check, schema-typescript), make scripts-typecheck가 docs/.vitepress/dbs.ts가 import하는 packages/dbspec-language의 build 전에 실행됐으며(conformance), MySQL migration case가 scripts/test-servers-mysql.mts를 그 import가 풀리지 않는 임시 root로 복사했다(static). 완료 조건: 변형은 cases.mts가 주는 거부를 기대하고, scripts-typecheck는 dbspec-language를 먼저 build하며, migration case는 checkout의 스크립트를 임시 root의 경로로 실행하고, 세 검사가 통과한다.—Evidence: packages/ts/tests/dbspec.mjs가 310 case를 통과하고, packages/dbspec-language/dist가 없는 checkout에서 make scripts-typecheck가 통과하며, LEASE를 준 node --test scripts/test-servers-mysql.test.mts가 실제 MySQL server로 5 case를 통과한다.[o]
T137-1-4-9bench/install과 bench/seedaes를 TypeScript로 이전한다 (T137-1-4-5에 이어). 원인: scripts/bench-db.mts는 Go가 유일한 executor이던 때(2a640e7c) 쓴 Go program 둘로 bench database를 설치하고 seed하며, S9 예외는 없다. 이제 TypeScript client가 schema를 설치하고 driver를 가진다. 완료 조건: scripts/bench-db.mts가 TypeScript bench model로 schema/bench.dbs를 설치하고 TypeScript codec으로 AES와 blind-index column을 채운다. bench/install과 bench/seedaes를 지우고, setup 단계 databases/create에 Go가 필요 없다.—Evidence: 실제 server에서 make databases-create가 scripts/bench-db.mts로 MySQL, PostgreSQL, SQLite bench database를 설치하고 seed한다. TypeScript client가 aes_hex_email에서 user4242@example.com을 decode하고 blind index가 맞으며, AES 값이 빠진 author가 없다. make decimal-bench-sqlite가 같은 seed로 database를 만들고, scripts/repo와 scripts/check 143건이 통과한다.[o]
T137-1-4-10server socket folder를 node:crypto hash로 이름 짓고, worktree를 지우기 전에 그 server를 멈춘다 (T137-1-4-5에 이어). 원인: scripts/test-servers.mts는 shell script의 socket folder 이름을 지키려고만 POSIX cksum의 TypeScript 사본을 가졌는데, 그 algorithm은 Node의 crypto가 대신한다. migration case는 fixture의 socket folder를 논리 경로 /tmp로 지웠지만 script는 실제 경로로 이름 지어, 실행마다 folder 하나가 남았다. 지운 worktree orm-phpunit-T148-1의 server는 지우기 전에 멈추는 단계가 없어 8시간 동안 실행됐다(kit이 보고). 완료 조건: paths()가 SHA-256으로 folder 이름을 짓고, migration case가 socket folder를 남기지 않으며, AGENTS.md가 worktree를 지우기 전 make test-servers-stop을 적는다.—Evidence: orm-phpunit-T148-1에 남은 server를 멈췄고(data directory가 없어진 replica는 KILL로), checkout이 없어진 socket folder 넷을 지웠다. 변경 후 make test-servers-check가 14 case 통과하고 /tmp/orm-servers-* folder를 남기지 않는다. test-servers.test 9건, scripts/repo check.test 91건이 통과한다. 이 변경 전에 시작한 server는 이전 socket folder 이름을 쓰므로, checkout은 이 변경을 받기 전에 그 server를 멈춘다.[o]
T137-2kit 전환의 lock을 검토한다 (T137에 이어). 원인: T137이 package-lock.json과 composer.lock을 바꾸고 tests/interfaces/typescript/package-lock.json을 검토 기록 없이 더했으며, composer.lock을 composer.json보다 뒤에 두었고, interface reader의 TypeScript 6 예외를 지웠다. 또 kit의 @microsoft/api-extractor 의존성이 sprintf-js advisory 1241202를 orm lock에 들인다. make dependency-policy-check가 실패했다. 완료 조건: 검토가 모든 lock을 기록하고, 예외는 kit처럼 근거를 적으며, dependency-policy-check가 통과한다.——[o]
T137-3T137 이후 orm main CI를 다시 green으로 만든다 (T137에 이어). 원인: run 38517144969가 실패했다. CI가 tests/interfaces/typescript를 설치하지 않아 interface reader가 TypeScript 7로 돌았고(ts.NewLineKind undefined), packages/php-ext/src/orm_dbspec_arginfo.h가 옛 경로의 stub hash를 유지했다. 완료 조건: CI step이 interface TypeScript를 설치하고 arginfo 검사가 통과하며, 먼저 red를 확인한다.——[o]
T137-4interface TypeScript 설치를 별도 make로 실행한다 (T137-3에 이어). 원인: run 38519188361의 모든 job이 Node step에서 멈췄다. make install-packages install-interfaces-typescript는 install-packages가 kit.mk를 설치하기 전에 그것을 읽는다(kit.mk is missing). 완료 조건: step이 make를 두 번 실행한다. node_modules가 없을 때 합친 형태는 실패하고 나눈 형태는 통과한다.——[o]
T138mermaid를 최신 release로 고정한다. 원인: 2026-10-13 의존성 review가 root package가 12.1.0을 고정한 사이 mermaid 12.2.0이 나왔음을 찾았고, 의존성 정책은 이를 거부한다. 인수 조건: mermaid를 12.2.0에 고정하고 review가 이를 기록한다.—증거: 변경 전 make dependency-review RECORD=1이 이 finding을 보고했고 변경 후에는 보고하지 않는다; npm run docs:check가 Mermaid source 20개와 함께 통과한다.[o]
T139YAML codec을 pure-Rust YAML library 하나로 읽고 쓴다. 원인: 2026-10-13 review가 packages/orm-rust와 bench/rust에서 RUSTSEC-2024-0449(unsafe-libyaml 유지보수 중단)를 찾았다. 이는 Rust codec이 decode와 encode에 쓰는 serde_yaml_ng를 통해 들어오며, 같은 text는 이미 yaml-rust2가 검증한다; fork인 serde_norway는 2024-12 이후 release가 없다. 인수 조건: yaml 단계가 docs/codec.md의 규칙(공통 값 모델, CODEC_DECODE로 거부하는 형태, encode 실패 시 CODEC_ENCODE)대로 yaml-rust2만으로 decode와 encode를 한다; serde_yaml_ng와 unsafe-libyaml이 두 lock에서 빠진다; 동작이 바뀌는 codec vector에서 red를 먼저 본다; Rust codec test와 client 간 codec 검사가 통과한다; make dependency-policy-check가 통과한다.—증거: 변경 전 새 Rust codec test가 옛 decoder가 YAML 1.2 core schema와 다르게 읽은 입력 11개에서 실패했다(07을 문자열로, 0b101을 5로, u64 정수 허용 등); 변경 후 cargo test --lib codec::tests가 test 11개를 통과하고, make codec-check가 Go, Rust, TypeScript, PHP vector를 통과하며, make dependency-policy-check가 advisory 없음을 보고하고, serde_yaml_ng와 unsafe-libyaml이 두 lock에서 빠졌다.[o]
T139-1Rust YAML codec으로 모든 문자열을 쓴다(T139에 이어). 원인: yaml-rust2의 YamlEmitter는 0o17과 일부 제어 문자를 따옴표 없이 두고 C1 문자, U+FEFF, U+FFFE, U+FFFF를 escape하지 않으므로, Rust encoder의 되읽기 검사가 그 문자열을 CODEC_ENCODE로 거부한다. 다른 client는 쓸 수 있는 기능이다. 인수 조건: Rust codec이 yaml-rust2를 통해(upstream 수정이나 문서화된 option) 공통 값 모델의 모든 문자열을 쓰며, 거부되던 문자열마다 red를 먼저 본다. 조사(2026-10-14): yaml-rust2 0.13.0이 최신 release이고, 후속인 saphyr 0.2의 emitter도 같다. need_quotes는 0o, U+0007, U+0008, U+000B, U+000C, U+001B, U+007F, C1, U+FEFF, U+FFFE, U+FFFF를 빠뜨리고 escape_str은 U+0020 미만과 U+007F byte만 escape하므로, 어느 쪽의 option으로도 충족되지 않는다. 길은 need_quotes와 escape_str의 upstream 수정이다.——[o]
T139-2saphyr YAML codec의 Rust symbol을 기록한다 (T139-1에 이어). 원인: run 38504701356이 missing ...YamlReader as MarkedEventReceiver::on_event로 실패했다. T139-1이 reader의 trait을 바꾸고 기록하지 않았다. 완료 조건: Rust 기록이 새 symbol을 담고 interface-check가 통과한다.——[o]
T140restore가 update의 request hook 규칙을 따른다. 원인: restore는 request hook 조건을 실을 수 없다: restore key는 key 등호 predicate만 받으므로 hook이 더한 조건은 IR_INVALID이고, model은 hook이 돌기 전에 set 값에서 key를 고르므로 scope column을 더는 쓰지 않는 module은 그 column을 담은 unique key를 고를 수 없다. 인수 조건: planner가 hook 뒤에 set 대입과 and로 이은 등호 조건에서 restore key를 고른다(덮이면 primary key, 아니면 이름순 첫 unique key); Model::restore는 set만 전달한다; restore의 where는 key 밖의 and 조건을 더 실을 수 있고, 여전히 많아야 한 row임이 보장된다; 여섯 client, protocol(+ko), conformance에서, 각 client가 red를 먼저 본다.—증거: hyper session이 각 client에서 red를 먼저 보며 만들고 그곳에서 review했다(hook이 바꾼 key의 되읽기, Rust SetValue match, eq 문구, 공유 case 넷을 review 뒤에 고침); client마다 함수 하나가 규칙을 담는다; make interface-check가 모든 언어에서 통과하고, go test ./engine/ir/... ./engine/planner/...가 통과하며, make markdown-conformance-check가 규칙을 따르는 flowmark commit(flowmark T14)에 대해 restore case를 통과한다.[o]
T140-1새 고정에서 flowmark 표를 옮겨 둔다(T140에 이어). 원인: T140이 flowmark commit 5e065f4를 고정하면서 고정한 revision의 flowmark docs/spec/60-dialect.md를 옮겨 둔 docs/spec/dialects.md(+ko)의 표 "Markdown IR executor"를 다시 쓰지 않아, CI run 38444557164의 markdown-table-check가 실패했다. 인수 조건: 표가 고정한 flowmark를 옮겨 둔다.—증거: node scripts/docs/markdown-table.mjs --write가 표를 다시 썼고, 검사가 두 표가 v0.0.0-20261013153052-5e065f4d1b58의 flowmark spec 60을 옮겨 둔다고 보고한다.[o]
T141docs/spec/dbspec.md "Files"에 Python byte 검사와 파일 reader를 적는다. 원인: 이 절은 Go, PHP, TypeScript, Rust의 byte 검사와 파일 reader를 적는데 Python에도 read_dbspec_bytes(packages/orm-python/src/polyspec/orm/dbspec/file.py)가 있다. 인수 조건: 이 절(+ko)이 Python의 함수를 적는다.—증거: 이 절(+ko)이 packages/orm-python/src/polyspec/orm/dbspec/file.py의 함수인 read_dbspec_bytes와 read_dbspec_file을 적는다; make documents-check가 통과한다.[o]
T142Go와 PHP가 YAML을 core schema로 읽는다. 원인: T139 조사에서 Go yaml.v3가 0b101을 5로, +0x1을 1로, 1_000을 1000으로, U+0085를 줄바꿈으로 읽고 2014-12-31을 거부하며, PHP도 1_000, timestamp, U+0085를 같이 읽음을 찾았다. docs/codec.md는 YAML 1.2 문서를 요구하므로, Rust나 TypeScript client가 따옴표 없이 쓴 문자열을 다른 값으로 되읽는다. 인수 조건: Go와 PHP가 YAML 1.2 core schema로 decode하며, 형태마다 공유 codec vector를 두고 red를 먼저 본다.——[o]
T142-1PHP가 YAML을 core schema로 읽는다 (T142에 이어). 원인: symfony/yaml 7.4에는 schema flag도 scalar hook도 없고(Inline::evaluateScalar는 private), Yaml::parse가 1_000을 1000으로, 2014-12-31을 1419984000으로, 2001-12-14t21:59:43.10-05:00을 1008385183.1로(PARSE_DATETIME은 DateTime을 줄 뿐 text가 아니다), 0777을 문자열 0777로(core: 777) 읽는다. tests/codec/check.php는 vector yaml_core/underscore_int_like, date_like, timestamp_like, leading_zero_int, map을 명시해 건너뛰고 각각 출력한다. 재시도: ecosystem에서 PHP YAML 1.2 core reader를 고르거나(ext-yaml callback과 다른 package 조사), parser event API로 scalar를 resolve하는 판정을 기록한다. 그 뒤 제외 목록을 지운다.—Evidence: codec-check, client-unit-check, php-min-check, interface-check, dependency-policy-check가 통과한다.[o]
T142-1-1yaml_core/signed_hex_like와 yaml_core/leading_zero_int의 PHP 제외를 없앤다 (T142-1에 이어). 원인: purrphp/yaml 1.0.0 CoreSchema가 YAML 1.2.2 10.3.2절과 달리 +0x1을 1로, -0o7을 -7로, 0777을 float 777.0으로, nULL과 tRUE를 null과 true로 읽는다. 완료 조건: orm이 의존하는 기반인 fork min-0-max/yaml이 YAML 1.2.2 표로 core 해석을 고친 release를 내고 orm이 그것을 pin하며, codec vector의 PHP 제외 목록이 비어 있다. 어느 단계도 upstream을 기다리지 않는다.—Evidence: fork의 CoreSchema가 10.3.2 절대로 scalar를 해석한다(v1.0.0-p1, library의 새 case 17개 중 12개가 1.0.0에서 red, library suite 406 green). orm은 fork의 vcs repository에서 purrphp/yaml 1.0.0-p1을 요구한다(root composer.json, consumer manifest, docs/operations/packaging.md. Composer는 root manifest의 repositories만 읽는다). CodecVectorsTest와 HostcodecTest의 제외를 지우면 signed_hex_like, leading_zero_int(int 777로 검사), 대소문자 섞인 key가 1.0.0에서 실패하고 fork에서 통과한다. codec-check/compare, client-unit-check/hostcodec, dependency-policy-check가 통과한다.[o]
T142-2Go와 Python이 U+0085를 내용으로 읽는다 (T142에 이어). 원인: yaml.v3와 PyYAML의 scanner가 U+0085를 줄바꿈으로 읽어(a<U+0085>b가 a b로 decode된다) Node API나 resolver hook보다 아래에 있다. packages/orm-go/orm/codec_test.go(goCodecExclusions), packages/orm-python/tests/codec_vector.py(PYTHON_EXCLUSIONS), tests/codec/check.php가 yaml_core/next_line을 명시해 건너뛰고 출력한다. 재시도: U+0085를 지키는 각 ecosystem의 scanner option이나 reader, 또는 기록한 판정; 그 뒤 제외를 지운다.—Evidence: codec-check, python-unit-check, interface-check, dependency-policy-check가 통과하고 Go codec test가 통과한다.[o]
T142-9docs/codec.md의 YAML 거부 규칙이 모든 client에서 성립하게 한다 (T142-2에 이어). 원인: docs/codec.md는 anchor와 explicit tag를 거부하지만, Go와 Python은 alias 없는 anchor와 core tag(!!str, !!int)를 받고 PHP는 거부한다. 빈 stream은 Go에서 오류, Python과 PHP에서 null이다. 완료 조건: docs/codec.md에 형태마다 규칙 하나, 그 각각의 공유 vector, 모든 client의 통과.—Evidence: tests/codec/decode-vectors.json의 공유 거부 vector 여섯 개가 수정 전 실패하고 다섯 client 모두에서 통과한다. codec-check, python-unit-check, interface-check가 통과한다.[o]
T142-10TypeScript에서 문자열이 아닌 YAML map key를 parse된 node로 거부한다 (T142-9에 이어). 원인: packages/ts/src/codec.ts decodeYaml이 plain boolean, null, float key를 key의 source text에 regex를 적용해 거부하는데, 이는 yaml library 옆에서 core schema를 다시 읽는 것이다. 완료 조건: key의 type을 library가 해석한 scalar에서 얻고, regex를 없애며, key 거부 vector가 통과한다.—Evidence: TS case tRUE, nULL, .InF가 수정 전 실패한다. 공유 key 거부 vector 네 개가 다섯 client 모두에서 통과한다. codec-check가 통과한다.[o]
T142-11공유 decode vector에서 문자열이 아닌 값을 비교하고, 받아들이는 YAML key를 공유한다 (T142-10에 이어). 원인: decode-vector runner가 value를 문자열로 비교해, 받아들이는 key tRUE, nULL, .InF, 1, "true"가 TS 전용 case다. Go, Python, Rust, PHP가 이를 core schema대로 읽는지 확인되지 않는다. 완료 조건: 모든 runner가 JSON 값을 비교하고, 받아들이는 key vector를 공유하며, 다섯 client가 모두 통과한다.—Evidence: Go, Python, Rust, TypeScript가 모든 공유 decode vector를 통과한다. PHP는 60개를 통과하고 대소문자가 섞인 key 세 개를 vector 이름을 지정해 건너뛰며, T142-1-1이 추적한다. codec-check가 통과한다.[o]
T142-3codec-check가 Python codec vector를 실행한다 (T142에 이어). 원인: packages/orm-python/tests/codec_vector.py를 실행하는 make target이 없어(python-unit-check는 test_*.py만 맞춘다) Python의 YAML 결함이 드러나지 않았다. 완료 조건: codec-check가 이를 실행하고, 깨진 vector로 먼저 red를 확인한다.——[o]
T142-4codec-check가 lease tool을 준비한다 (T142에 이어). 원인: 새 checkout에서 make lease-tool 전에는 make codec-check가 .runtime/bin/lease ENOENT로 실패한다. codec-check/rust에 선행 조건이 없다. 완료 조건: codec-check가 lease-tool에 의존하고, 새 worktree에서 먼저 red를 확인한다.——[o]
T142-5codec-check가 실행하는 PHP 의존성을 설치한다 (T142-4에 이어). 원인: 새 checkout에서 make install-php 전에는 codec-check/compare가 packages/orm-php/tests/autoload.php의 PHP fatal error로 멈춘다. 완료 조건: target에 선행 조건이 있고, 새 worktree에서 먼저 red를 확인한다.——[o]
T142-7모든 PHP target이 PHP vendor를 확인한다 (T142-5에 이어). 원인: testcase-check/php, dbspec-*-php 검사, client-unit-check가 php-vendor-check 없이 vendor-php를 읽어, 새 checkout에서 이름 있는 단계 대신 PHP fatal error로 끝난다. 완료 조건: 각각 선행 조건을 갖고, 새 worktree에서 먼저 red를 확인한다.——[o]
T142-8PHP를 실행하는 node script의 PHP vendor를 확인한다 (T142-7에 이어). 원인: scripts/case-database-check.mjs(model_test.php), scripts/unselected-column-physical-check.mjs, scripts/decimal-physical-check.mjs가 vendor-php를 읽는 PHP를 실행하는데, 그 make target에 php-vendor-check가 없다. 완료 조건: 각 target이 선행 조건을 갖고, 새 worktree에서 먼저 red를 확인한다.——[o]
T143TypeScript와 PHP set reader가 Windows의 모든 name surrogate를 감지한다(T128-2a-5에 이어). 원인: docs/spec/dbspec.md 단계 3은 tag가 name surrogate인 모든 reparse point를 symbolic link로 보지만, Node와 PHP의 lstat은 symbolic link와 junction만 보고하므로 다른 name-surrogate tag(WSL symlink tag)를 따른다. Go, Rust, PHP extension은 IsReparseTagNameSurrogate를 검사한다. 인수 조건: TypeScript와 PHP reader가 선언된 방식으로 reparse tag에 닿거나, 명세가 근거와 함께 지원 tag를 적는다; 조용한 차이는 없다.——[o]
T144checkout에서 packages/orm-php/bin/orm-gen을 실행한다. 원인: php packages/orm-php/bin/orm-gen이 autoload 조회에서 vendor-php를 찾지 못해 Class "Polyspec\Orm\OrmGen" not found로 멈추므로 README의 명령이 실패한다; vendor-php/bin/orm-gen은 동작한다. 인수 조건: bin이 저장소가 설치하는 autoloader를 찾으며, red를 먼저 본다.—증거: Composer proxy 없이 실행한 bin은 autoloader를 찾지 못해 Class "Polyspec\Orm\OrmGen" not found로 죽었다; 이제 Composer의 문서화된 조회($_composer_autoload_path, composer-runtime-api ^2.2 요구)로 autoloader를 얻고, 경로를 추측하지 않고 없는 autoloader를 적어 상태 2로 끝나며, 문서는 Composer가 설치하는 bin인 vendor-php/bin/orm-gen을 실행한다; make client-unit-check/orm-gen이 새 case 둘을 통과한다.[o]
T145mysql2를 최신 release로 고정한다. 원인: 2026-10-13 의존성 review가 packages/orm-npm이 3.24.5를 고정한 사이 mysql2 3.24.6이 나왔음을 찾았고, 의존성 정책은 이를 거부한다. 인수 조건: mysql2를 3.24.6에 고정하고 review가 이를 기록한다.—증거: 변경 전 make dependency-review RECORD=1이 이 finding을 보고했고 변경 후에는 없다; make dependency-policy-check와 make typescript-build가 통과한다.[o]
T146orm-schema의 module 목록을 format한다. 원인: main에서 cargo fmt --all --check가 packages/orm-rust/orm-schema/src/dbspec/mod.rs(mod names;와 mod plan_diff 순서)에서 실패한다. 인수 조건: crate에서 rustfmt가 통과하며, red를 먼저 본다.—증거: hyper session의 agent가 69f7748d에서 red를 재현했다(orm-schema/src/dbspec/mod.rs:20의 mod names와 pub mod model 순서); c0e189e9(T128-2a-6)가 module 순서를 바로잡았고, make rust-fmt-check가 세 part 모두 통과한다.[o]
T147interface rule이 선언하지 않은 component method를 거부한다. 원인: rule 없는 Dbspec component method가 있어도 make interface-check가 통과하므로(T128-2a-7이 찾음) 빠진 rule이 red로 드러나지 않는다. 인수 조건: manifest 검사가 rule 없는 component method를 거부하며, red를 먼저 본다. 원인(우회): fixture에서 red를 먼저 본 거부 검사가 실제 manifest에서 rule 없는 component method 25개를 찾으므로, main에 commit하면 make interface-check가 red가 된다. 재시도: T147-1부터 T147-4가 끝나면 branch fix/rules-T147(검사의 commit, 그때까지 두며 merge할 때 worktree orm-rules-T147과 함께 지운다)을 merge한다.—증거: 재시도 조건(T147-1부터 T147-5 완료)이 성립해 b817ba9e의 검사를 merge했다: 모든 공개 component method가 선언된 채 make interface-check가 다섯 언어와 extension에서 통과하고, branch fix/rules-T147과 그 worktree를 지웠다.[o]
T147-1공개 component method 아홉의 rule을 선언한다(T147에 이어). 원인: Db.close, Collection.get, first, keys, connect, delete, toArray, Utils.setLocal, Utils.local은 다섯 언어 모두에 있지만 rule이 없다. 인수 조건: 각각 언어별 정확한 symbol과 input·output type을 가진 rule이 있고 기록된다.—증거: 233fc7b4가 contracts/symbols에서 읽은 symbol과 contracts/validate.go의 type으로 rule 아홉을 선언한다; make interface-check가 모든 언어에서 통과한다.[o]
T147-2Python에 없는 공개 method 여섯을 준다(T147에 이어). 원인: Db.stats, Utils.privileges, Utils.stats, SchemaUtils.exists, SchemaUtils.installed, SchemaUtils.empty는 docs/interfaces.md에 문서화되어 있지만 Python에 없다. 인수 조건: Python이 공유 test와 함께 이를 구현하고(red 먼저), 각각 모든 언어에서 rule을 가진다.—증거: 변경 전 Python test가 여섯 method에서 AttributeError와 ImportError로, PHP model_test가 정의되지 않은 Db::stats로 실패했다; Python이 이를 구현하고(fe7070a5), PHP가 Db::stats를 얻으며(7b770346), rule 여섯이 선언되고(a4aa5769), make python-unit-check, make client-unit-check, make interface-check가 통과한다.[o]
T147-3manifest의 내부 component를 판정한다(T147에 이어). 원인: Core.build, Request.shape, Planner.validate, Planner.plan, TransactionFlow.begin, savepoint, commit, rollback, Generator.generate는 공통 signature 없는 private이나 internal이다. 인수 조건: manifest가 근거와 함께 component를 internal로 표시하고 검사는 공개 component만 보거나, method가 diagram을 떠난다; docs/spec/interfaces-model.md(+ko)에 적는다.—증거: 207b98ed가 Core, Request, Planner, TransactionFlow, Generator를 근거(internal, internal_ko)와 함께 internal로 표시하고, 검사는 그 method를 건너뛰며 그 위의 rule을 거부하고(Planner.plan에서 red 먼저), docs/spec/interfaces-model.md(+ko)가 이를 <<internal>>로 보인다.[o]
T147-4모든 rule의 method를 그 component에 둔다(T147에 이어). 원인: rule SchemaUtils.addTablesAndColumns는 있지만 그 component가 그런 method를 적지 않아 docs/interfaces-model.md가 이를 빠뜨린다. 인수 조건: manifest 검사가 component에 없는 method의 rule을 거부하며(red 먼저), 빈틈을 메운다.—증거: 변경 전 fixture test가 component method 없는 rule SchemaUtils.addTablesAndColumns를 받아들였다; cd6d4d84가 이를 거부하고 SchemaUtils에 그 method를 적는다.[o]
T147-5Rust AesKeyring에 versions를 주고 그 rule을 선언한다(T147에 이어). 원인: component 검사가 rule 없는 AESKeyring.versions를 찾는다; Go(AESVersions), PHP, TypeScript, Python에는 있고 Rust AesKeyring에는 new, current, key만 있다. 인수 조건: Rust가 다른 client처럼 이를 구현하고(red 먼저), rule이 모든 언어에서 이를 선언한다.—증거: 변경 전 keyring_tests cargo test가 no method named versions로 실패했다; 576087a9가 오름차순 AesKeyring::versions를 더하고 모든 언어에서 rule을 선언한다; test와 make rust-check/clippy가 통과한다.[o]
T148parity binding 실행 규칙(R3)이 dbspec unit에서 성립하게 한다. 원인: T128-3과 T128-2a-7이 dbspec unit의 binding이 다른 case 이름을 보고하거나(TypeScript, Python), case를 보고하지 않거나(Rust), 실행되지 않음(PHP는 없는 phpunit이 필요하고, extension은 어느 target도 build하지 않는 library가 필요)을 찾았다. 인수 조건: 모든 dbspec unit의 binding 실행이 모든 runtime에서 case id를 보고하거나, kit의 규칙을 근거와 함께 바로잡는다(kit K42가 Go / 규칙을 바꾼다); red를 먼저 본다. 우회. Cause: R3는 각 case가 그 생태계의 표준 runner에서 case id로 실행되기를 요구한다. orm은 PHP test를 자체 script runner(packages/php/tests/*.php)로 실행하는데, 이것은 다시 만든 test runner다. 또 Go, TypeScript, Python, Rust의 공유 vector test는 test 하나 안에서 vector를 순회해 어떤 case id도 runner에 닿지 않는다. Retry: kit 0.0.43(K136, K137)이 나오고 T148-1부터 T148-3까지 끝났을 때.——[!] cause: T128-3과 T128-2a-7이 dbspec unit의 binding이 다른 case 이름을 보고하거나(TypeScript, Python), case를 보고하지 않거나(Rust), 실행되지 않음(PHP는 없는 phpunit이 필요하고, extension은 어느 target도 build하지 않는 library가 필요)을 찾았다. 인수 조건: 모든 dbspec unit의 binding 실행이 모든 runtime에서 case id를 보고하거나, kit의 규칙을 근거와 함께 바로잡는다(kit K42가 Go / 규칙을 바꾼다); red를 먼저 본다. 우회.; retry: kit 0.0.43(K136, K137)이 나오고 T148-1부터 T148-3까지 끝났을 때
T148-1PHP client와 extension의 test를 PHPUnit으로 실행한다 (T148에 이어). 원인: packages/php/tests와 packages/php-ext/tests가 PHP 생태계의 표준 runner인 PHPUnit이 아니라 orm 자체 script runner로 실행된다. 완료 조건: test가 PHPUnit test case이고, 공유 vector는 data set 이름이 case id인 data provider로 실행되며, make target이 PHPUnit을 실행하고, 손으로 쓴 runner를 없앤다.—Evidence: client-unit-check, codec-check, dbspec-php-check, dbspec-php-extension-check, php-min-check, interface-check, dependency-policy-check가 통과한다. PHPUnit이 각 공유 vector case를 그 id로 이름 붙인다.[o]
T148-1-1PHP 개발 도구를 저장소 root에 설치한다 (T148-1에 이어). 원인: T148-1이 PHPUnit을 위해 packages/php와 packages/php-ext에 각자 Composer project를 주어, 릴리스되는 composer.json이 repositories(ordered-json package와 packages/php path), require-dev, platform 설정을 선언했고, v0.0.16의 make release-assets가 이를 거부했다: 릴리스 zip은 repository를 선언하지 않는다. 완료 조건: 개발 root(vendor-php)가 PHPUnit과 path repository의 packages/php를 설치하고, 두 package의 test는 자기 phpunit.xml.dist로 vendor-php/bin/phpunit을 실행하며, 두 package lock을 지우고, 릴리스 manifest는 소비자가 설치하는 것만 선언하며, make release-assets가 archive를 만든다.—Evidence: client-unit-check와 dbspec-php-extension-check가 vendor-php/bin/phpunit으로 통과하고, commit의 로컬 tag에서 make release-assets TAG=v0.0.16이 모든 archive를 쓴다.[o]
T148-2Go, TypeScript, Python, Rust에서 모든 공유 vector case를 그 id로 보고한다 (T148에 이어). 원인: 각각 vector 파일을 test 하나 안에서 순회한다. 완료 조건: Go t.Run(<id>), TypeScript는 id마다 top-level test, Python은 unittest load_tests로 case마다 id가 case id인 TestCase, Rust는 id로 이름 지은 libtest-mimic trial이다. method 이름을 생성하지 않는다. 우회. Cause: Go, TypeScript, Python, Rust가 Rust show-create case를 빼고 모든 공유 vector case를 그 id로 보고한다. Rust show-create case는 orm 설계 결정(T148-2-1)을 기다린다. Retry: T148-2-1이 결정될 때.—Evidence: retry 조건이 충족됐다. T148-2-1로 Go(t.Run), TypeScript(id마다 test 하나), Python(load_tests), Rust(libtest-mimic trial)가 모든 공유 vector case를 id로 보고하고, T148-2-2가 옮긴 Rust vector를 실행한다.[o]
T148-2-1Rust show-create case를 그 id로 보고한다 (T148-2에 이어). 원인: Rust는 tests/dbspec/show-create.json을 crate 밖에서 보이지 않는 shown_checks로 읽고 lib unit test 하나로 확인한다. libtest는 unit test 이름에 하이픈 id를 쓸 수 없고, 공개 경로 read_catalog는 읽은 본문을 dbspec 문서로 바꾸므로 vector를 표현하지 못한다(kit 판정: spec 단위가 공개 경로보다 넓으며 orm의 설계 문제다). 완료 조건: orm이 SHOW CREATE reader가 API의 어디에 속하는지 정하고, Rust binding이 각 case를 그 id로 보고한다.—Evidence: SHOW CREATE reader는 Go, TypeScript, Python처럼 MySQL introspection의 private으로 둔다. tests/dbspec/show-create.json은 각 case를 공개 경로(catalog_queries, catalog_followups, read_catalog)로 기술하고, 새 libtest-mimic target packages/rust/schema/tests/dbspec_show_create.rs가 그것을 trial show_create_vectors::<id>로 실행한다. 이전 binding은 9 case 모두 R3 "not executed"였고(0 passed, 6 filtered out), 새 binding에서 kit의 cargo 명령은 9건 통과한다. dbspec-rust-check/show-create, repository, feature, owners, layout 검사가 통과한다.[o]
T148-2-2옮긴 Rust vector를 workspace에서 실행하고 측정 시간은 경고한다 (T148-2에 이어). 원인: main CI가 실패했다: T148-2의 검증 명령이 ORM_RUST_TEST_FEATURES의 workspace feature와 함께 -p polyspec-orm을 썼고, packages/rust/schema/tests/dbspec_plan.rs가 CPU 시간으로 case를 실패시켰다.—Evidence: planner-rust-validate, codec-vectors-rust, encryption-codec-rust가 통과하고 저장소 검사가 통과한다.[o]
T148-3모든 parity unit을 bind하고 parity R2와 R3를 통과한다 (T148-1, T148-2에 이어). 원인: parity 검사가 모든 unit에서 R2 binding 누락 15줄과 R3 196줄을 보고한다. 완료 조건: make parity-check가 통과하며, 끝에 한 번 실행한다. 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[!] 원인: 이 sandbox가 제공할 수 없는 CI의 database와 Rust 실행이 필요하다; 재시도: CI에서 parity bindings 실행이 통과한 뒤
T148-3-1PHP와 PHP extension parity binding을 root Composer project의 PHPUnit으로 실행한다 (T148-3에 이어). 원인: kit의 PHP runner는 package마다 vendor/bin/phpunit을 찾는데 orm은 root Composer project(vendor-php, T148-1-1)에 PHPUnit을 한 번 설치하므로 R3 binding 59개가 시작하지 못한다. 완료 조건: 가장 가까운 Composer project에서 PHPUnit을 찾는 kit 0.0.50(K178) 뒤 parity가 그것을 실행한다. 우회 원인: kit 도구가 정한다. Retry: kit 0.0.50이 release될 때(T181).—Evidence: kit 0.0.50(K178, K184)로 parity가 root Composer project(vendor-php)에서 PHPUnit을 찾는다. PHP와 PHP extension binding 62개가 모두 vendor-php/bin/phpunit으로 시작하고 phpunit이 없다고 보고하는 것은 없다. 남은 R3 finding은 test 실패와 case 이름이며 T148-3-8이 맡는다.[o]
T148-3-2parity R2가 binding이 없다고 보고하는 Python feature를 bind한다 (T148-3에 이어). 원인: aggregate-numeric, authenticated-encryption, decimal-model, planner, styled-column-states, install-changed-database, sqlite-paths, statement-events에 Python binding이 없고 대부분 Python test도 없다. 완료 조건: 각각 config/parity.json에 bind된 Python test가 있거나, 먼저 Python에 feature를 구현한다.—Evidence: T77-3이 Python feature 8개를 각각 fixture id로 case를 실행하는 test에 bind했다. make parity-declarations-check가 python R2 줄을 보고하지 않는다.[o]
T148-3-3parity R2가 binding이 없다고 보고하는 Go, PHP, PHP extension, Rust feature를 bind한다 (T148-3에 이어). 원인: go의 codec, codec-aes, dbspec-syntax, php와 rust의 codec-aes, php-extension의 schema-definition과 dbspec-sets에 binding이 없다. 완료 조건: 각각 그것을 실행하는 test에 bind한다.—Evidence: T148-3-3-1 뒤 parity R2가 0을 보고한다. 이 항목이 적은 Go, PHP, PHP extension, Rust unit이 묶였다.[o]
T148-3-3-1공유 vector case 이름에서 slash를 뺀다 (T148-3-3에 이어). 원인: tests/codec/vectors.json의 case 91개(yaml_core/...)와 tests/dbspec/syntax.json의 304개(canonical/..., invalid/...)가 slash를 담아 Go가 subtest로 중첩하므로, kit이 codec과 dbspec-syntax unit의 Go binding을 거부한다. T148-3-3의 나머지 unit 5개는 bind했다(Go, PHP, Rust의 codec-aes, PHP extension의 dbspec-sets와 schema-definition). 완료 조건: 모든 vector file과 runner에서 공유 case id에 slash가 없고, Go codec과 dbspec-syntax unit이 bind되며, parity가 그것에 R2를 보고하지 않는다.—Evidence: 91개 codec case id와 304개 syntax case id가 두 generator와 모든 runner에서 부분을 "."로 잇고, Go codec과 dbspec-syntax unit이 symbol/id subtest로 묶이며, parity R2가 2에서 0이 되고 make codec-check와 dbspec-compare-check가 통과한다.[o]
T148-3-4여러 suite를 실행하는 binding file을 나눈다 (T148-3에 이어). 원인: R3 63줄이 suite에 없는 case를 보고한다. 모든 언어의 dbspec cases, mermaid, plans, codec file이 한 file에서 다른 suite의 case나 추가 test를 실행한다. 완료 조건: 각 binding file이 suite 하나를 그 case id로 실행한다.—Evidence: suite마다 Go function, Rust trial module, PHP data-provider method, node file, Python module이 하나이고, 다른 test는 그 옆으로 옮겨 make target에서 계속 실행된다. 공유 file의 R3 줄이 모두 사라졌다(다음 항목 전 R3 161에서 38).[o]
T148-3-5feature-coverage binding의 case에 이름을 준다 (T148-3에 이어). 원인: Go, TypeScript, Rust의 coverage_* binding이 case 이름을 보고하지 않고(R3 not executed), Rust codec과 styled-state case는 id를 가질 수 없는 lib test다. 완료 조건: 각 case가 id로 실행된다.—Evidence: Go, TypeScript, Rust, Python schema-definition binding이 모든 fixture case를 id로 실행하고, conformance는 dialect마다 binding 하나가 각 vector를 기록된 기대값과 비교하며, Go 구현 인자에 featurecoverage tag가 붙는다. 건너뛰던 Go styled-state fixture의 unselected와 nonnull_sql_null case도 이제 실행된다.[o]
T148-3-6Python conformance runner를 test로 실행한다 (T148-3에 이어). 원인: tests/conformance/runner_python.py가 parity에서 5(실행한 test 없음)로 끝나고 Python root 밖에 있다(R6). 완료 조건: Python conformance case가 Python package 안에서 id로 실행된다.—Evidence: packages/python/tests/conformance_mysql.py, conformance_postgres.py, conformance_sqlite.py가 새 conformance-check/python target으로 Python conformance runner를 test로 실행한다.[o]
T148-3-6-1Python conformance target을 harness와 run database로 실행한다 (T148-3-6에 이어). 원인: conformance-check/python이 $(WITH_TEST_ENV) 없이 python -m unittest …를 실행해, python-unit-check가 그 줄을 거부했고 make run-databases에서 target이 "BENCH_MYSQL_DSN is required"로 실패했다. 완료 조건: target이 run 환경으로 packages.python.tests.testcase를 실행하고, python-unit-check와 전체 conformance-check가 통과한다.—Evidence: python-unit-check가 그 줄에서 실패했고 target은 BENCH_* 없이 실패했다. 이제 target은 lease-tool과 conformance-python-models 뒤에 $(WITH_TEST_ENV) … -m packages.python.tests.testcase를 실행하며, python-unit-check가 통과하고, make run-databases TARGETS=conformance-check가 Python 부분(test 135개)과 함께 통과하고 세 vector file은 바뀌지 않는다.[o]
T148-3-7parity R4, R5, R6, R7, R9, R10을 통과한다 (T148-3에 이어). 원인: parity 검사가 R5 396건(bind되지 않은 test file), R10 120건(client를 넷으로 세는 문서), R6 78건(root 밖 test), R4 4건(선언되지 않은 suite), R9 4건(오래된 client 목록), R7 2건(오류 code를 선언하지 않는 TypeScript와 Python)을 보고한다. 완료 조건: 각 규칙이 통과하고, 큰 묶음은 더 나눈다.—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-1parity R5를 통과한다: 모든 test file이 묶인다 (T148-3-7에 이어). 원인: R5가 binding이 없는 test file 420개를 보고한다. 대부분은 어느 parity unit도 적지 않은 한 언어 test다. 완료 조건: 각 test가 다섯 구현에 묶인 unit에 속하거나 unit의 기존 binding으로 옮겨지고, R5가 0을 보고한다.—근거: commit 39276d1a(main; config/parity.json, unit 10개): T77-6의 collection identity test 다섯 개를 각 언어의 internal profile unit으로 선언했고, 바인딩은 하나씩이며 native id를 쓴다. kit checkout에 링크된 worktree에서 make -s parity-declarations-check가 exit 0이다(every declaration holds, R5 0). 앞선 R5 1은 링크 전 실행의 결과다.[o]
T148-3-7-2parity R6을 통과한다: 선언된 root 밖의 test (T148-3-7에 이어). 원인: R6가 scripts/, tests/dialects, tests/dbspec, tests/conformance runner, examples/, benchmarks/rust, packages/dbspec-* 아래 test 76개를 보고한다. 완료 조건: 각 test가 root 아래로 옮겨지거나 이유와 함께 tools root에 선언되고, R6가 0을 보고한다.—근거: commit 1f7e8a53(main): make -s parity-declarations-check가 R6 misplaced-test 0건을 보고한다(이 줄을 여기에 기록한다. 같은 실행의 R5 5건은 T169-3의 것으로 T169-4에서 추적하며 R6이 아니다). main에 남은 R6 finding은 없다.[o]
T148-3-7-3TypeScript와 Python에 생성된 code catalog를 둔다 (T148-3-7에 이어). 원인: R7이 TypeScript와 Python에 생성된 error code catalog가 없다고 보고한다. Go, PHP, Rust는 모든 error 지점에서 자기 catalog를 쓴다. 완료 조건: orm-gen이 두 언어에 catalog를 생성하고, 두 client의 모든 error 지점이 그것을 쓰며, 오래된 catalog에서 drift test가 실패하고, R7이 0을 보고한다.—근거: TypeScript와 Python 소스의 OrmError, CodecError와 오류 code literal은 모두 생성된 catalog의 codes.<NAME> 상수가 되었고, 소스는 main과 그 상수만 다르다(quote를 맞춘 뒤 기계적으로 확인). T77-4의 drift test가 catalog를 errors.yaml과 비교한다.[o]
T148-3-7-4parity R9와 R10의 Python 기능 차이를 없앤다 (T148-3-7에 이어). 원인: scripts/features/check.mts에 Python을 넣으면 구현됨으로 표시된 기능 15개가 Python에서 통과하지 않고 2개는 Python test가 없다. Python에는 decimal physical test, pool, timeout, VERIFY_IDENTITY 설정, point codec type, typed model field, client-db-test의 database test가 없다. 완료 조건: 각 기능을 다른 client처럼 공유 vector와 함께 Python에 구현하고 test하며, R9와 R10이 Python finding을 보고하지 않는다.——[o]
T148-3-7-4-1TLS 설정, pool, timeout 설정 점검 (follows T148-3-7-4).—Evidence: commit e41a6450 on main: Python은 알 수 없는 ssl-mode를 거부하고, VERIFY_IDENTITY를 SSLContext로 적용하며, pool 옵션을 PgBouncer나 ProxySQL 이름과 함께 CONFIG로 거부하고, statementTimeoutMs를 적용하며, MySQL socket 인자를 존중한다. RED: VERIFY_CA가 받아들여졌고 틀린 CA로도 연결되었다. GREEN: mysql_tls.py, connection_config.py, replica.py가 통과한다.[o]
T148-3-7-4-2codec에서 옛 point type 제거 (follows T148-3-7-4).—Evidence: commit 6ad53395 on main: codec.md에서 옛 point 문단과 그 case를 지웠다. dbspec.md에는 point type이 없다. main에서 검사가 통과한다.[o]
T148-3-7-4-3생성된 model field에 type을 붙임 (follows T148-3-7-4).—Evidence: commit 26e129a4 on main: getter와 setter에 type을 붙였고, 값의 type이 다르면 getter가 TypeError를 던진다. RED: test_model_fields.py가 14회 실패했다. GREEN: 통과하고, Python physical conformance runner가 세 database에서 통과한다.[o]
T148-3-7-4-4decimal physical check에 Python 추가 (follows T148-3-7-4).—Evidence: commit e5b1ac66 on main: make decimal-physical-check가 database마다 Python decimal_model_db.py를 실행한다. RED: parity R9가 decimal-physical-check.mts에 python이 없다고 보고했다. GREEN: database마다 두 번 통과한다.[o]
T148-3-7-4-5구현된 모든 기능을 Python에서 통과 (follows T148-3-7-4).—Evidence: commit 3b525263 on main: Python이 scripts/features/check.mts의 client이고, owner가 있는 coverage module 15개를 더했다. RED: 오류 17건. GREEN: Python lane이 직접 실행과 pooler를 거친 실행 모두 통과한다.[o]
T148-3-7-4-6다른 client처럼 connection pool 유지 (follows T148-3-7-4).—Evidence: commit e28c1499 on main: Db가 docs/config.md의 poolSize, poolIdleSize, poolLifetimeMs를 따르는 thread-safe pool을 유지한다. RED: 기본 pool 크기가 1이었고 poolSize가 거부되었다. GREEN: connection_pool.py가 PgBouncer와 ProxySQL을 거쳐서도 통과한다.[o]
T148-3-7-4-7옛 point 함수 제거 (follows T148-3-7-4).—Evidence: commit 417b1d10 on main: distance, pointX, pointY를 Go generator의 scan 목록과 docs/dsl.md에서 지웠다. 어느 client도 구현하지 않으며, 예제는 year()를 쓴다.[o]
T148-3-7-5남은 R10 차이를 없앤다 (T148-3-7에 이어). 원인: Rust에 generator --check가 없고, examples/thin-slice는 Go, PHP, Rust만 다루며, styled value type 문서에 TypeScript와 Python이 없다. 완료 조건: 각각을 모든 client에 더하고 R10이 0을 보고한다.—근거: red: packages/rust/build에서 cargo test --test generate check_가 no method named check found for struct polyspec_orm_build::Builder로 compile되지 않았다(tests/generate.rs:449와 :476). node --test tests/docs/styled-value-sections.test.mts가 docs/spec/codec.md lacks the TypeScript StyledValue section과 the Fields bullet lacks the TypeScript styled value type으로 실패했다. go test -run TestExamplesRequireBenchDSN ./examples가 ts와 python thin-slice subtest를 실패시켰다(stderr에 DSN 요구 문구가 없고 can't open file .../examples/thin-slice/python/main.py). green: cargo test --test generate가 26건 통과했다(Builder::check와 그 test 두 건 포함). node --test tests/docs/styled-value-sections.test.mts가 2건 통과했다. go test -run TestExamplesRequireBenchDSN ./examples가 subtest 12건 모두 통과했다(thin-slice의 go, php, ts, python과 complex의 go, php). make ts-model-check가 통과했고, scan 목록에 examples/thin-slice/ts/main.mts가 들어가 models script가 그것을 scan한다. make python-model-check가 통과했다. examples/thin-slice/ts/main.mts의 tsc --noEmit과 go vet -tags examples ./examples가 오류를 보고하지 않았다. make parity-declarations-check는 R10 finding을 보고하지 않는다(남은 R5 finding 여섯 건은 main의 plan-cache test file의 것이다). Python scanner는 이름 있는 receiver만 따라가므로 Python 예제의 연쇄 호출은 scan되지 않고, 재생성된 model은 바뀌지 않았다. 실행하지 않음: bench database의 출력 비교(TestExampleOutputsAreIdentical, make example-check)와 전체 suite.[o]
T148-3-7-6set manifest를 vector suite에서 뺀다 (T148-3-7에 이어). 원인: R4가 set manifest인 contracts/fixtures/dbspec-set.json과 contracts/fixtures/bench/dbspec-set.json을 vector suite로 다룬다. parity suite glob에는 제외 형식이 없다. 완료 조건: suite 선언이 vector file만 고르고(형식이 없으면 kit에 보고), R4가 0을 보고한다.—근거: commit e8b0e079(main; suites glob이 두 set manifest를 제외한다. contracts/fixtures/**/!(dbspec-set).json이며 kit의 glob reader가 이미 받아들인다. set manifest는 set-consumers unit으로 그대로 선언된다): make -s parity-declarations-check에서 R4 undeclared-suite가 0이다. 새 test는 0개다(config/parity.json만 바뀌었다). documents-check, docs:rules-check, layout-check가 통과한다. 선언 검사는 R5(T169-4)로만 exit 2다.[o]
T148-3-8PHP와 PHP extension parity binding을 통과시킨다 (T148-3-1에 이어). 원인: kit 0.0.50에서 PHP binding 62개가 시작하고 php R3 finding 36건이 남는다(database가 필요한 identity-load와 SchemaSet case, CodecVectors go_output, case id와 맞지 않는 data set 이름). config/parity.json은 어느 make target도 build하지 않는 .runtime/parity/php-extension/orm_dbspec.so를 읽는다. 완료 조건: make target이 parity 전에 그 extension을 build하고, 모든 PHP와 PHP extension binding이 case를 id로 통과한다.—Evidence: parity-check가 새 parity-prepare target에 의존한다. 이 target은 PHP extension과 Rust conformance runner를 build하고 ORM_PARITY_PHP_EXTENSION과 ORM_PROGRAM_CONFORMANCE를 export한다(config/parity.json이 extension을 그것에서 읽음). PHP conformance와 statement event가 case마다 이름을 가지며 모든 PHP와 PHP extension binding이 통과한다. packages/php-ext/scripts/build.sh는 이제 상대 출력 경로를 절대 경로로 바꾼다. 전에는 publish copy가 실패했다.[o]
T148-3-tsTypeScript parity bindings 실행 (T148-3-ts-1부터 T148-3-ts-6까지의 그룹)——[o]
T148-3-ts-5TypeScript code-catalog 파일이 case를 id로 보고하게 한다 (T148-3에 이어). 원인: code-catalog.mjs가 cases().run으로 case를 실행해 node parity reporter가 읽지 않는 평문 TAP 줄을 썼고, R3는 code-catalog/matches-errors-yaml을 not executed로 보고했다. 완료 조건: 파일의 각 case가 id 이름의 node:test test이고, make run-databases TARGETS=parity-check UNITS=ts-code-catalog가 exit 0이며 id가 통과하고 unit의 R3 줄이 없다.—근거: RED: 여섯 unit의 parity 실행이 R3 not-executed ts-code-catalog typescript ...: not executed: code-catalog/matches-errors-yaml을 보고했다(exit 2). GREEN: ts-code-catalog 실행이 exit 0이고 ts-code-catalog typescript: exit 0, 1 of 1 ids passed를 보고하며 R3 줄이 없다.[o]
T148-3-ts-6TypeScript collection-key 파일이 두 case를 id로 보고하게 한다 (T148-3에 이어). 원인: collection_identity.mjs가 cases().run으로 case를 실행해 node parity reporter가 읽지 않는 평문 TAP 줄을 썼고, R3는 collection-key/identity-from-fixture와 collection-key/null-is-codec-decode를 not executed로 보고했다. 완료 조건: 각 case가 id 이름의 node:test test이고, make run-databases TARGETS=parity-check UNITS="ts-internal-collection-identity-from-fixture ts-internal-collection-null-is-codec-decode"가 exit 0이며 두 id가 통과하고 두 unit 모두 R3 줄이 없다.—근거: RED: 두 unit에 대해 parity 실행이 not executed: collection-key/identity-from-fixture와 not executed: collection-key/null-is-codec-decode를 보고했다. GREEN: 같은 실행이 exit 0이고 ts-internal-collection-identity-from-fixture, ts-internal-collection-null-is-codec-decode typescript: exit 0, 2 of 2 ids passed를 보고하며 R3 줄이 없다.[o]
T148-3-ts-4TypeScript statement-fault entry point 검사를 자기 test로 실행한다 (T148-3에 이어). 원인: statement_fault.mjs가 statement-fault-entry/entry_point를 네 fixture case 옆에 보고해, R3는 그 id를 suite에 없다고 보고했다. 완료 조건: entry point 검사가 자기 file에 있고 feature unit ts-statement-fault-entry로 묶이며, make run-databases TARGETS=parity-check UNITS="statement-fault ts-statement-fault-entry"가 exit 0이고 R3 줄이 없다.—근거: RED: 여섯 unit의 parity 실행이 R3 not-executed statement-fault typescript ...: not in the suite: statement-fault-entry/entry_point를 보고했다. GREEN: 실행이 exit 0이고 R3 줄이 0개이며, statement-fault typescript: exit 0, 4 of 4 cases passed(두 번 실행)와 ts-statement-fault-entry typescript: exit 0, 1 of 1 ids passed를 보고한다.[o]
T148-3-ts-1TypeScript install-changed-database case를 자기 file에 묶는다 (T148-3에 이어). 원인: binding이 schema-set.mjs를 실행했고, 이 file은 install case 옆에 several-schemas, unregistered-schema, edited-manifest, external-document, register case를 보고해 R3가 그 열다섯 id를 suite에 없다고 보고했다. 완료 조건: install fixture가 packages/ts/tests/install_changed_database.mjs에서 실행되어 unit에 묶이고, make run-databases TARGETS=parity-check UNITS=install-changed-database가 exit 0이며 R3 줄이 없다.—근거: RED: 여섯 unit의 parity 실행이 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: install-changed-database 실행이 exit 0이고 install-changed-database typescript (1 of 2): exit 0, 1 of 1 cases passed와 (2 of 2)의 같은 줄을 보고하며 R3 줄이 없다. 열다섯 schema-set case는 schema-set.mjs에 남으며 T148-3-ts-2의 대상이다.[o]
T148-3-ts-2TypeScript dbspec-set-decimal case를 decimal document를 보고하는 file에 묶고, schema-set case는 feature unit으로 묶는다 (T148-3에 이어). 원인: binding이 schema-set.mjs를 실행했다. 이 file은 decimal_schema.dbs를 보고하지 않고, suite가 가지지 않은 schema-set case 열다섯 개를 보고한다. 결정: decimal document case는 TypeScript file에서 실행되어야 한다. set-consumers profile이 TypeScript test가 decimal set 위에서 실행된다고 밝히며, suite file은 자기 suite의 이름만 보고하므로, document case는 dbspec-set-decimal.mjs로 옮기고 schema-set case는 feature unit ts-schema-set-*가 된다. 완료 조건: dbspec-set-decimal.mjs가 SQLite, MySQL, PostgreSQL에서 decimal_schema.dbs를 보고하고, unit의 TypeScript binding에 R3 줄이 없다.—근거: dbspec-set-decimal의 TypeScript binding이 decimal_schema.dbs를 보고하며(1 of 1, exit 0, 두 번), 열다섯 ts-schema-set-* unit은 T148-3-php-1의 실행에서 15 of 15로 통과하고 TypeScript R3 줄이 없다. unit을 bypass로 남겼던 PHP binding은 T148-3-php-1이 닫는다.[o]
T148-3-phpPHP parity bindings 실행 (T148-3-php-1의 그룹)——[o]
T148-3-php-1PHP dbspec-set-decimal case를 자기 data set으로 묶는다 (T148-3-ts-2에 이어). 원인: dbspec-set-decimal의 PHP binding이 symbol 없이 packages/php/tests/SchemaSetTest.php 전체를 실행해, data set 열다섯 개가 suite에 없고 decimal_schema.dbs는 실행되지 않았다(R3, exit 2). 완료 조건: binding이 SchemaSetTest::test_decimal_document method를 명칭으로 가리키고, 그 data set은 decimal_schema.dbs 하나이며, SQLite, MySQL, PostgreSQL에 decimal set을 설치하고 두 amount를 정확히 다시 읽는다. make run-databases TARGETS=parity-check UNITS="dbspec-set-decimal ts-schema-set-*"가 exit 0이고 R3 줄이 없다. RED: 실행이 exit 2로 끝나며 R3 not-executed dbspec-set-decimal php ...: not executed: decimal_schema.dbs; not in the suite: several_schemas with data set "sqlite", ...를 보고했다. GREEN: dbspec-set-decimal과 열다섯 ts-schema-set-* unit의 실행이 exit 0이며, dbspec-set-decimal php (1 of 2)와 (2 of 2)가 exit 0, 1 of 1 cases passed, TypeScript binding이 두 번 1 of 1 cases passed, 3 bindings ran twice, 17 cases passed이고 R3 줄이 없다. suite binding은 id가 아니라 symbol로 method를 묶는다.—근거: RED: 여섯 unit의 parity 실행이 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). TypeScript GREEN: dbspec-set-decimal ts-schema-set-* 실행이 dbspec-set-decimal typescript (1 of 2): exit 0, 1 of 1 cases passed(2 of 2 동일)와 ts-schema-set-* typescript: exit 0, 15 of 15 ids passed를 보고하며 TypeScript R3 줄이 없다. 같은 unit의 PHP R3 줄 때문에 실행은 여전히 exit 2다.[o]
T148-3-ts-3TypeScript IR answer case를 자기 file에서 fixture id로 실행한다 (T148-3에 이어). 원인: ir-answers binding이 markdown_conformance.mjs를 flowmark mode로 실행해, R3는 그 id를 not executed로, flowmark case id를 not in the suite로 보고했다. 이 file의 --ir-answers mode는 node binding이 인자를 넘길 수 없으며 runner.run(평문 TAP 줄)으로 ir-answers/<id> 이름의 case를 실행했다. 완료 조건: IR answer가 packages/ts/tests/ir_answers.mjs에서 fixture case id 이름의 node:test test로 실행되고, flowmark case는 markdown_conformance.mjs에 남으며, make run-databases TARGETS=parity-check UNITS="ir-answers markdown-conformance"가 exit 0이고 R3 줄이 없다.—근거: RED: parity 실행이 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: 같은 실행이 exit 0이고 ir-answers typescript (1 of 2): exit 0, 16 of 16 cases passed(2 of 2 동일), markdown-conformance typescript (1 of 2): exit 0, 35 of 35 cases passed(2 of 2 동일)를 보고하며 R3 줄이 없다. case 함수는 markdown_cases.mjs로 옮겨졌고, flowmark setup의 orm-gen scan이 그것을 읽으며, orm-gen gen --check는 commit된 model과 여전히 일치한다.[o]
T148-4전체 parity 실행이 exit 2로 끝나는 원인 가운데 Rust가 아닌 네 가지를 통과시킨다 (T148에 이어). 원인: 전체 parity 실행이 Rust 밖의 네 원인으로 exit 2다. py-decimal-model-db는 test 실패 3건으로 실패하고, PHP 성능 gate 둘과 PHP extension 선언 unit 다섯은 parity-check가 내보내지 않은 환경 때문에 exit 2이며(T148-5), TypeScript AesKeyring의 public symbol은 선언되어 있지 않다. 인수 조건: 아래 자식 항목이 모두 끝나고, 전체 parity 실행이 이 네 원인을 보고하지 않는다.—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-1parity 실행에서 py-decimal-model-db를 통과시킨다 (T148-4에 이어). 원인: python unit이 packages/python/tests/decimal_model_db.py에 python -m unittest를 실행하며, DECIMAL_MYSQL_DSN, DECIMAL_POSTGRES_DSN, DECIMAL_SQLITE_DSN이 없으면 Ran 2 tests … FAILED (failures=3)를 보고한다. 각 round-trip subtest가 "is required"를 던지기 때문이다. 인수 조건: make run-databases TARGETS=parity-check UNITS="py-decimal-model-db"가 R3 줄 없이 exit 0이며, red를 먼저 재현하고 고침은 잘못된 쪽에 가한다.—근거: RED: var/proof-red.log가 py-decimal-model-db python: exit 1과 R3 not-executed … FAILED (failures=3)를 보고한다. DSN 변수를 비운 채 같은 세 실패가 var/py-decimal-noenv.log에서 재현된다. GREEN: var/py-decimal-red.log는 cebf00eb에서 make 명령이 exit 0이고 1 of 1 ids passed이며 R3 줄이 없다. 이 tip에서는 make 명령으로 red가 재현되지 않는데, T148-5가 이미 DECIMAL_ENV로 DECIMAL_*_DSN 이름을 내보내기 때문이며, 이 항목에서 바뀐 코드는 없다.[o]
T148-4-2두 PHP 성능 gate를 통과시킨다 (T148-4에 이어). 원인: CoveragePerformanceGateTest::test_hot_path_gate와 PerfGateTest::test_perf_gate가 0 of 1 id 통과로 exit 2인데, 가드가 요구하는 ORM_BENCH_MYSQL_DSN을 parity-check가 내보내지 않았기 때문이다(T148-5가 제공한다). 인수 조건: 두 PHP binding이 parity 실행에서 1 of 1 id 통과로 exit 0이고, 그 binding에 R3 줄이 없다.—근거: var/t148-4-23.log가 php-internal-coverage-performance-gate-hot-path-gate php: exit 0, 1 of 1 ids passed와 php-internal-perf-gate-perf-gate php: exit 0, 1 of 1 ids passed를 보고하며, 실행은 check: result passed와 exit 0으로 끝난다.[o]
T148-4-3PHP extension 선언 unit 다섯을 통과시킨다 (T148-4에 이어). 원인: packages/php-ext/tests/DeclarationsTest.php의 unit 다섯(required-modules, declarations, client, package, mirrors)이 0 of 5 id 통과로 exit 2인데, binding이 php -d extension=${ORM_PARITY_PHP_EXTENSION}으로 extension을 불러오고 실행이 build된 library를 찾지 못했기 때문이다(T148-5가 extension 경로를 제공한다). 인수 조건: 다섯 binding이 parity 실행에서 5 of 5 id 통과로 exit 0이고, 그 binding에 R3 줄이 없다.—근거: var/t148-4-23.log가 php-extension-internal-declarations-required-modules, …-declarations, …-client, …-package, …-mirrors php-extension: exit 0, 5 of 5 ids passed를 보고하며, 실행은 exit 0으로 끝난다.[o]
T148-4-4TypeScript AesKeyring public symbol을 선언한다 (T148-4에 이어). 원인: 전체 parity 실행이 TypeScript AesKeyring public symbol에서 exit 2인데, contracts/interfaces.json은 AesKeyring.versions를 명칭으로 든다. contracts/symbols/typescript.json은 이미 AesKeyring, currentVersion, key, rotateRow, versions를 exported로 선언하고 있으며, 빠진 symbol을 지목하는 parity 줄은 var/t148-4-23.log에 없으므로 고치기 전에 red 실행으로 정확한 symbol을 확인한다. 인수 조건: TypeScript symbol 선언이 contracts/interfaces.json이 명칭으로 든 packages/ts/src/aes.ts의 public member와 일치하고, 그것을 보고하는 parity 검사가 exit 0이다. 증거(2026-10-18): make interface-typescript-check가 exit 0이고 contracts/symbols/typescript.json이 AesKeyring, currentVersion, keys, key, rotateRow, versions를 선언한다. R11은 전체 parity-check만 판정하므로 이 행은 마지막 한 번의 실행(T77-8-1-6)에서 검증한다.—증거 (2026-10-19): 이 원인의 전체 실행 실패는 unit 이전에 interface-check가 실패한 것이었다. T211이 PHP와 Rust의 private symbol 다섯 개를 선언하지 않았고, a06f6e30에서 기록한 뒤 make interface-check/self-test는 열 개 언어·extension 모두 통과한다 (var/interface-symbols-green.log). RED: contracts/symbols/typescript.json에서 AesKeyring.versions를 빼면 같은 검사가 not ok - interfaces/typescript, unexpected packages/ts/src/aes.ts::AesKeyring.versions로 실패한다 (var/t148-4-4-red.log). GREEN: 선언이 있으면 UNITS="aes-keyring aes-keyring-versions" make run-databases TARGETS=parity-check가 exit 0, parity-check PASS (var/t148-4-4.log).[o]
T148-4-5TestApplyStressPlan이 가장 작은 크기로 자기 성질을 증명하고, 진행과 자체 timeout을 갖게 한다 (T148-4에 이어). 판정: 규모에서의 정확성 test이므로 parity unit에 남기고, 빼는 대신 다시 설계했다. 원인: go-internal-dialects-apply-stress-physical이 dbspec-apply-stress-bench의 2000-table benchmark인 TestApplyStressPlan(MySQL 22000 step, step당 250 ms 예산)을 feature unit으로 묶는다. parity는 go test 기본 10분 timeout으로 실행하므로 600초 동안 아무것도 출력하지 않다가 timeout에 goroutine dump로 실패했다(bccfc382 전체 parity 실행). 몇 분씩 돌며 시작과 끝만 알리는 test는 진행을 보여 주지 않는다. 완료 기준: benchmark는 parity unit이 아니라 bench로 선언되고 BENCH_TARGETS에 남는다. apply 경로는 자체 timeout을 가진 unit test가 덮는다. 오래 걸리는 test는 각 step, 결과, 경과 시간을 출력한다.—증거: RED: unit이 600초 동안 step을 하나도 출력하지 않고 go test 기본 timeout으로 실패했다(bccfc382 전체 parity 실행). GREEN: make dbspec-apply-stress-bench, table 100개(max_locks_per_transaction 64보다 많음), 매 step 뒤 두 번째 PostgreSQL session이 apply session의 relation lock 0개를 요구, 200 step마다 경과 시간과 진행 줄, 30초 stall guard: mysql 1100 step 52.87초, postgres 1100 step 2.56초, sqlite 600 step 0.35초, 모두 verified와 history applied <steps>, 56.8초에 PASS.[o]
T148-5parity 바인딩이 읽는 bench와 decimal DSN을 제공한다 (T148-3에 이어). 원인: parity-check가 BENCH_* DSN만 내보내고 ORM_BENCH_MYSQL_DSN, DECIMAL_*_DSN 이름, ORM_DBSPEC_EXTENSION은 내보내지 않아서, PerfGateTest와 CoveragePerformanceGateTest가 "ORM_BENCH_MYSQL_DSN is required"로 거부했고, decimal unit에는 DECIMAL_<DIALECT>_DSN이 없었으며, DeclarationsTest는 build된 library를 찾지 못했다. 인수 조건: parity-check가 그 이름들을 실행의 TEST_ENV와 DECIMAL_ENV 파일, PARITY_RUN_DIR에서 내보내고 가드는 그대로 둔다.—근거: var/proof-red.log(변경 전)는 exit 2이며 decimal unit과 모든 PHP binding이 실패한다. var/proof-green.log(변경 후): py-decimal-model-db가 exit 0이고 1 of 1 id 통과, PHP perf-gate binding 두 개가 exit 0, DeclarationsTest binding이 exit 0이다. 실행은 PHP id의 R3 "not executed"(kit 0.0.53의 id 매칭 결함, kit에 보고하며 여기서는 고치지 않는다)로 여전히 exit 2다. 원인: parity-check가 BENCH_* DSN만 내보내고 ORM_BENCH_MYSQL_DSN, DECIMAL_*_DSN 이름, ORM_DBSPEC_EXTENSION은 내보내지 않아서, PerfGateTest와 CoveragePerformanceGateTest가 "ORM_BENCH_MYSQL_DSN is required"로 거부했고, decimal unit에는 DECIMAL_<DIALECT>_DSN이 없었으며, DeclarationsTest는 build된 library를 찾지 못했다. 인수 조건: parity-check가 그 이름들을 실행의 TEST_ENV와 DECIMAL_ENV 파일, PARITY_RUN_DIR에서 내보내고 가드는 그대로 둔다. 근거: var/proof-red.log(변경 전)는 exit 2이며 decimal unit과 모든 PHP binding이 실패한다. var/proof-green.log(변경 후): py-decimal-model-db가 exit 0이고 1 of 1 id 통과, PHP perf-gate binding 두 개가 exit 0, DeclarationsTest binding이 exit 0이다. 실행은 PHP id의 R3 "not executed"(kit 0.0.53의 id 매칭 결함, kit에 보고하며 여기서는 고치지 않는다)로 여전히 exit 2다.[o]
T148-3-9raw cargo test 명령으로 실행되는 Rust binding을 tests/cargo-test.mts로 실행한다 (T148-3에 이어). 원인: Rust unit 12개(client bench, example, benchmark, seeded test)가 raw cargo test --manifest-path 실행에 묶여 있어 test에 복사된 program이 없고, 그래서 polyspec_orm_testcase::program이 "ORM_PROGRAM_<NAME> is unset"으로 panic한다. 인수 조건: 각 binding이 tests/cargo-test.mts를 실행하는 make target으로 실행되고, 그 unit들이 id로 통과한다.—Evidence: RED(cf01f139, 전체 parity log): 12개 unit이 "ORM_PROGRAM_CLIENT_BENCH is unset"(그리고 ORM_PROGRAM_COMPLEX, ORM_PROGRAM_NATIVE, ORM_PROGRAM_DRIVER_COMPARE)으로 panic하며 12개 id 중 0개가 통과한다. GREEN: make run-databases TARGETS=parity-check UNITS="<12 ids>"가 통과한다(parity-check PASS, 12/12 id). binding 다섯 개는 이제 make feature-rust-unit을 실행하고, 이 target은 tests/cargo-test.mts로 test target을 실행해 복사된 program이 test에 닿게 한다.[o]
T148-3-10parity에서 sqlite send-savepoint test의 DSN이 존재하는 directory를 가리키게 한다 (T148-3에 이어). 원인: parity-check가 ORM_SEND_SQLITE_DSN을 .runtime/run/parity-check-<pid>에 export하는데, 이 directory는 아무도 만들지 않는다(rust-send-savepoint-check/test만 자기 directory를 mkdir -p 한다). 그래서 tx::send_tests::sqlite_send_savepoint가 "unable to open database file"로 실패하고, --lib Rust unit의 id 39개가 그 때문에 실패한다. 인수 조건: DSN이 parity-prepare가 만드는 run directory를 가리키고, 그 unit들이 parity에서 통과한다.—Evidence: RED(cf01f139): 같은 39개 id로 make run-databases TARGETS=parity-check UNITS="<39 ids>"를 실행하면 "failed outside the ids: tx::send_tests::sqlite_send_savepoint"로 실패한다. test는 "declared database must connect: ... unable to open database file"(SQLite 14)로 panic한다. 같은 test binary는 DSN이 존재하는 directory를 가리키면 통과하고 directory가 없으면 실패한다. GREEN: 같은 실행이 통과한다(parity-check PASS, 모든 unit exit 0, 39/39 id). ORM_SEND_SQLITE_DSN은 parity-prepare가 만드는 단계의 run directory를 참조한다.[o]
T148-3-11dbspec runtime model의 table 목록을 test와 맞춘다 (T148-3에 이어). 원인: schema/tests/dbspec_runtime.rs:65(runtime_model_of_bench)가 bench model의 table 목록을 단정하는데, model은 assertion에 없는 day_cell을 가진다. 인수 조건: 목록과 model이 test가 말하는 이유로 일치하고, test는 수정 전에 red, 수정 후에 green이다.—Evidence: RED: cargo test -p polyspec-orm-schema --test dbspec_runtime가 schema/tests/dbspec_runtime.rs:65에서 실패한다. bench model(day_cell 포함)과 기대값(day_cell 없음)이 다르다. bench.dbs는 185행에서 day_cell을 선언하고, commit 9f27a968이 그것을 모든 client에 더했지만 Rust 기대값은 빠져 있었다. GREEN: 같은 test가 4/4 통과하고, make run-databases TARGETS=parity-check UNITS="<group의 4 ids>"가 통과한다(parity-check PASS, 4/4 id).[o]
T148-3-12zone test의 MySQL DateTime parameter를 encode한다 (T148-3에 이어). 원인: orm/tests/zone.rs:329와 :374가 mysql에서 CODEC_ENCODE "parameter 0 (DateTime(2026-01-02T00:00:00)) is not a datetime value"로 실패하고, 같은 binary의 다른 zone case도 같은 실행에서 실패한다. 인수 조건: zone test가 test가 말하는 이유로 parity에서 통과하고, test는 수정 전에 red, 수정 후에 green이다.—Evidence: RED(cf01f139, 전체 parity log): zone unit이 CODEC_ENCODE "parameter 0 (DateTime(2026-01-02T00:00:00)) is not a datetime value"(zone.rs:329, :374)로 실패하고 11개 id 중 6개만 통과한다. 원인은 test다. #T89-3(14f1796a) 이후 모든 client가 date와 datetime slot을 canonical text로 받고 Rust bind check는 native chrono 값을 거부하므로, zone test가 설계상 거부되는 값을 bind하고 있었다. GREEN: zone test가 canonical text(YYYY-MM-DD HH:MM:SS.ffffff, UTC, docs/spec/protocol.md)를 bind하고, make run-databases TARGETS=parity-check UNITS="<11 ids>"가 통과한다(parity-check PASS, 11/11 id).[o]
T148-3-13value types test의 SQLite Date parameter를 encode한다 (T148-3에 이어). 원인: orm/tests/value_types.rs:117이 sqlite에서 CODEC_ENCODE "parameter 3 (Date(2026-01-02)) is not a date value"로 실패한다. 인수 조건: value types test가 test가 말하는 이유로 parity에서 통과하고, test는 수정 전에 red, 수정 후에 green이다.—Evidence: RED(cf01f139, 전체 parity log): rust-runtime-value-types가 value_types.rs:117에서 sqlite CODEC_ENCODE "parameter 3 (Date(2026-01-02)) is not a date value"로 실패하며 1개 id 중 0개가 통과한다. T148-3-12와 같은 원인이다. test가 native date를 bind했고, bind check는 #T89-3 이후 그것을 거부한다. GREEN: test가 canonical text YYYY-MM-DD를 bind하고, make run-databases TARGETS=parity-check UNITS="rust-runtime-value-types"가 통과한다(parity-check PASS, 1/1 id).[o]
T148-3-14Rust integration, conformance와 benchmark test의 binding을 실행할 수 있는 target에 묶는다 (T148-3에 이어). 원인: unit 18개가 target의 module이거나 library가 없는 package의 src 파일을 가리켜, kit의 cargo runner가 cargo test --lib를 실행하고 cargo가 "no library targets found in package polyspec-orm-tests"(또는 polyspec-orm-bench)를 보고한다. 인수 조건: 각 unit이 자기 test가 든 target을 실행하고, id로 통과한다.—Evidence: RED(cf01f139, 전체 parity log): 18개 unit이 "no library targets found in package polyspec-orm-tests"(또는 polyspec-orm-bench)로 실패하며 18개 id 중 0개가 통과한다. GREEN: make run-databases TARGETS=parity-check UNITS="<18 ids>"가 통과한다(parity-check PASS, 18/18 id). 일반 test는 자기 target(--test integration, --bin integration, --bin conformance, --bin native)에서 feature-rust-unit으로 실행한다. coverage test는 ignore되어 있고 database가 필요하므로, scripts/features/coverage.mts가 database마다 실행하는 것처럼 ORM_FEATURE_DATABASE=mysql, BENCH_MYSQL_DSN, 정확한 이름과 --include-ignored로 feature-rust-database에서 실행한다. parity id는 database를 적지 않으므로 그 unit들은 mysql에서 실행되고, contracts/features.json은 같은 일곱 test file을 postgres와 sqlite에서 실행한다.[o]
T148-3-14-1integration coverage test의 postgres와 sqlite unit을 bind한다 (T148-3-14에 이어). 원인: T148-3-14는 parity id가 database를 적지 않는다는 이유로 일곱 integration coverage test를 MySQL에만 묶었다. 같은 기능의 Go unit은 database마다 unit이 있고 features.json은 이 파일들을 세 database 모두에서 실행한다. 인수 조건: coverage unit마다 같은 test를 그 database에서 실행하는 postgres와 sqlite unit이 있고, parity가 통과시킨다.—근거: RED: 새 22개 id(MySQL unit 11개의 postgres와 sqlite)는 unit을 적지 않아 parity-check가 22개 finding을 보고한다("names no unit of config/parity.json"). GREEN: config/parity.json이 FEATURE_DATABASE=postgres와 BENCH_POSTGRES_DSN, sqlite와 BENCH_SQLITE_DSN으로 선언하고, make run-databases TARGETS=parity-check UNITS="<22 ids>"가 통과한다(parity-check PASS, 22/22 id, R3 not-executed 줄 없음).[o]
T149schema_definition에서 Python을 덮는다. 원인: schema_definition은 Python이 planned이고 검증 명령이 없어, Python set reader와 분석 test는 make python-check로만 소유된다. 인수 조건: Python의 schema_definition coverage가 검증 명령과 함께 실행된다(파일 소유 부분은 T151로 옮겼다).—증거: 변경 전 검사기가 Python runner를 몰라 node scripts/features/coverage.mjs --feature schema_definition --part owner/python이 undeclared owning client로 실패했다; 이제 검사기가 Python owner를 ORM_PYTHON으로 실행하고(없으면 실패하며 PATH의 python을 쓰지 않는다), schema_definition이 schema-python(make dbspec-python-check)과 fixture case 일곱을 실행하는 Python owner를 선언하며 두 번 모두 통과하고, make feature-unit-check와 node scripts/features/build.mjs --check가 통과한다.[o]
T150낡은 generator 문장 둘을 고친다. 원인: docs/usage.md는 --check가 Go, PHP, TypeScript orm-gen에 있다고 하지만 Python에도 있고, docs/packaging.md의 configuration 행은 schema 경로를 Go, PHP, TypeScript만의 connection option이라 한다. 인수 조건: 두 문장(+ko)이 code와 대조해 다섯 client와 맞는다.—증거: bin_orm_gen.py에는 하나뿐인 models.py에 missing이나 differs를 출력하는 --check가 있고, Db.connect_schema(dsn, schema)는 생성된 스키마를 받는다; docs/usage.md와 docs/operations/packaging.md(+ko)가 이제 그렇게 적는다; make documents-check와 npm run docs:rules-check가 통과한다.[o]
T151CI static group이 거부한 파일을 분류하고 소유한다. 원인: run 38413640279가 release-coverage(config/release.json 어디에도 분류되지 않은 editor package)와 feature-unit-check/owners(행동 test를 고르지 않는 파일 일곱: set 파일들, browser-entry.mjs, dbspec_names_test.php, syntax-vectors.mjs, install-cli.mjs)에서 실패했다. 인수 조건: editor package는 그 이유와 함께 T128-R까지 notReleased다; 각 파일은 그것을 읽는 검사의 input이다; generator에는 다시 생성하면 commit된 byte가 나온다는 test를, install-cli에는 임시 prefix에 설치하는 검사를 둔다.—증거: node scripts/kit/release.mjs coverage가 모든 package 파일을 분류한다; make feature-unit-check/owners가 19 case를 통과한다; packages/orm-npm/tests/syntax-vectors.mjs가 고친 syntax.json에서 실패하고 commit된 것에서 통과한다; make install-cli-check가 symbolic link 없이 설치하고 fixture를 format하고 검사한 뒤 제거한다.[o]
T1520.0.10을 release한다. 원인: hyper와 consumer session이 기다리는 request hook(T125), renderer 이름(T129, T129-2), audit된 immutable table(T130), generator scan(T132), 모든 runtime의 구문 분석·format·선언 위치(T128-3, T128-3p), 문서 집합 set 파일(T128-2a), 지금까지의 editor package가 green CI로 main에 있다. 인수 조건: AGENTS.md "Releases"의 version-bump commit이 모든 manifest를 0.0.10으로 두고 Unreleased를 0.0.10으로 옮기며, 그 commit에서 consumer lock을 v0.0.10으로 쓰고, 그 CI가 성공한 뒤 maintainer가 tag v0.0.10을 push한다.—증거: make version-check가 통과한다; tag의 release workflow가 release를 게시한다.[o]
T153Python Collection이 쓰는 key_text를 import한다. 원인: packages/orm-python/src/polyspec/orm/model.py의 Collection.get과 Collection.fetched가 put과 has만 import하는 key_text를 불러 NameError로 실패한다(T147-1이 찾음). 인수 조건: 둘을 부르는 unit test가 red를 먼저 보이고, module이 key_text를 한 번 import한다.—증거: 변경 전 test_collection.py가 Collection.get에서 NameError: name 'key_text' is not defined로 실패했다; f55fa60a가 key_text와 scalar_key를 polyspec/orm/keys.py로 옮기고 model.py가 순환 없이 맨 위에서 이를 import하며, make python-unit-check가 167 test를 통과한다.[o]
T154Python Mermaid test의 import fallback을 지운다. 원인: packages/orm-python/tests/dbspec_mermaid.py는 import에 실패하면 하드코딩한 형제 ordered-json 경로로 물러나는데, 이는 잘못된 환경을 그럴듯한 것으로 바꾸는 fallback이다(T149가 찾음). 인수 조건: test는 선언된 PYTHONPATH로만 import하고 없는 package는 그 이름과 함께 실패하며, red를 먼저 본다.—증거: 변경 전 test는 PYTHONPATH에 ordered-json이 없어도 추측한 형제 경로로 통과했다; 변경 후 같은 실행이 ModuleNotFoundError: No module named 'polyspec.ordered_json'으로 실패하며, 선언된 PYTHONPATH로는 13 case를 실행한다.[o]
T1550.0.11을 release한다. 원인: hyper의 S-285가 기다리는 request hook 아래 restore(T140), component rule 검사와 그 rule(T147), 엄격한 vector reader(T133), Python schema coverage(T149), 0.0.10 이후의 수정이 main에 있다. 인수 조건: version-bump commit이 모든 manifest를 0.0.11로 두고 Unreleased를 0.0.11로 옮기며, 바뀐 lock의 의존성 review와 v0.0.11의 consumer lock을 기록한다; 그 CI가 성공한다; annotated tag v0.0.11을 push하고 release workflow가 게시한다.—증거: 그 commit에서 make version-check와 make dependency-policy-check가 통과한다.[o]
T157markdown conformance에서 merge된 flowmark를 고정한다. 원인: flowmark T14가 열려 있는 동안 T140은 branch의 flowmark commit 5e065f4를 고정했다; flowmark는 orm v0.0.11과 함께 T14를 main의 8b5d16a로 merge했다. 인수 조건: tests/markdown/flowmark.version이 flowmark main을 고정하고 dialects 표가 그것을 옮겨 둔다.—증거: 고정은 v0.0.0-20261013184618-8b5d16a81141이고, node scripts/docs/markdown-table.mjs가 두 표가 그것을 옮겨 둔다고 보고하며, make markdown-conformance-check가 모든 client에서 restore case를 통과한다.[o]
T158T134, T135, T142 뒤 orm main CI를 green으로 되돌린다. 원인: 85d84f2c의 CI run 38462697441에서 feature_coverage, static, feature_verify_rust, conformance가 실패했다. 완료 기준: 각 sub-item의 check가 local에서 통과한다.——[o]
T158-1T142의 Go codec symbol을 기록한다 (T158 후속). 원인: interface check가 feature_coverage(interface_contract/owner/go), conformance(interfaces/go), feature_verify_rust(interface_contract/interface-self-test)에서 go: unexpected packages/orm-go/orm/codec.go::yamlCoreScalar와 ::yamlPlainUntagged로 실패한다. 완료 기준: Go record가 두 symbol을 담고 go run ./tests/interfaces/check -language go가 통과하며, red를 먼저 확인한다.——[o]
T142-6interface-check가 실행하는 PHP 의존성을 설치한다 (T142-5에 이어). 원인: 새 checkout에서 make install-php 전에는 interfaces/php와 interfaces/php-extension이 php declarations: exit status 255로 실패한다. 완료 조건: target에 선행 조건이 있고, 새 worktree에서 먼저 red를 확인한다.——[o]
T158-2socket 길이 test가 T135의 socket folder를 따른다 (T158 후속). 원인: static repo-check의 "test-servers.sh refuses a socket path longer than the platform limit"가 Cannot find module '/tmp/orm-sock-.../d/.../scripts/free-ports.mjs'로 실패한다. T135가 socket을 checkout 밖으로 옮겼는데 test는 여전히 checkout 경로를 늘린다. 완료 기준: test가 T135의 선언된 socket 위치로 한도를 넘기고 make repo-check가 통과하며, red를 먼저 확인한다.——[o]
T158-3codec-check의 need에 python을 선언한다 (T158에 이어). 원인: T142-3로 codec-check가 Python vector를 실행하는데 contracts/check-inputs.json의 codec-check에 python need가 없어 static job이 python-venv-check: .../.venv/bin/python is missing로 실패한다(run 38465371334). 완료 조건: need를 선언하고 static job의 codec-check가 통과한다.——[o]
T158-4dbspec-vscode integration의 인자 오류에 해결 방법을 적는다 (T158에 이어). 원인: repo-check가 packages/dbspec-vscode/tests/integration/run.mjs:22를 거부한다. --vscode가 없을 때의 문구가 해결 방법을 적지 않는다(run 38465371334). 완료 조건: 문구가 make dbspec-vscode-integration을 적고 repo-check가 통과한다.——[o]
T159request hook에 요청의 parameter를 읽기 전용으로 모든 client에서 넘긴다. 원인: hyper scope hook은 묶인 값을 검사하고 실패 시 닫혀야 하는데, hook에 parameter를 주는 것은 PHP($r->params)뿐이다. 완료 조건: Node와 Python은 hook을 (request, addParam, params)로 부르고, Go와 Rust는 params 타입으로 읽으며, 그것으로 parameter를 바꿀 수 없다. client마다 사례가 있고 먼저 red를 확인하며, interfaces 기록과 문서를 갱신한다.——[o]
T1600.0.12를 릴리스한다. 원인: hyper의 S-285가 기다리는 request hook parameter(T159), key-prefix 메서드(T156), Rust의 saphyr YAML codec(T139-1)과 0.0.11 이후의 수정이 main에 있다. 완료 조건: version-bump commit이 모든 manifest를 0.0.12로 두고 Unreleased를 0.0.12로 옮기며, 바뀐 lock의 dependency review와 v0.0.12의 consumer lock을 기록한다; 그 CI가 성공한다; annotated tag v0.0.12를 push하고 release workflow가 게시한다.—증거: 그 commit에서 make version-check와 make dependency-policy-check가 통과한다.[o]
T161PHP 생성 model의 query 메서드를 선언한다. 원인: PHP는 column filter, order, key-prefix chain을 Model::__call로 해석하므로 아무것도 그것을 선언하지 않아, PHPStan level 9가 hyper module에서 method.notFound 372건을 보고한다(hyper S-286-7-1). 반면 Go, Rust, TypeScript는 schema에서 메서드를 선언한다(T156). 완료 조건: PHP 생성기가 schema가 model에 정의하는 모든 query 메서드(모든 key prefix의 메서드, column filter, docs/usage.md가 정하는 order)를 유형 있는 실제 메서드로 선언한다; fixture model의 query 호출에서 PHPStan이 method.notFound를 보고하지 않으며 먼저 red를 확인한다; interface 기록, model 검사, docs/operations/usage.md(+ko)를 갱신한다; 다른 client의 같은 빈틈을 확인하고 빈틈은 별도 항목이 된다.——[o]
T161-1Python 생성 model의 query 메서드를 선언한다 (T161에 이어). 원인: Python의 Model.__getattr__(model.py)가 모든 chain을 실행 시점에 해석하고 아무것도 메서드를 선언하지 않아, type checker가 유형 없는 속성만 본다. T161이 PHP에서 닫은 빈틈이다. 완료 조건: Python 생성기가 schema가 정의하는 query 메서드(모든 column의 조건 메서드, 모든 key prefix의 tuple 메서드, column 메서드)를 model을 반환하는 유형으로 선언하고 fixture model에서 typing.get_type_hints로 읽으며, 각각이 그 체인 이름의 질의를 만들고, 먼저 red를 확인한다. __getattr__이 모든 이름을 받으므로 type checker는 원래도 Python model에서 없는 메서드를 보고하지 않는다. 빈틈은 유형 없는 결과였다.——[o]
T161-20.0.13 release commit의 CI를 다시 green으로 만든다 (T161에 이어). 원인: run 38509680967이 실패했다. scripts/check/run.test.mjs가 새 client-unit-check/model-methods part를 적지 않았고, PHPStan이 시스템 임시 폴더에 phpstan을 남겼다(feature-verify-other: 임시 항목 1개). 완료 조건: parts 표가 그 part를 적고, 사례가 PHPStan cache를 자기 폴더에 둔다.——[o]
T1620.0.13을 릴리스한다. 원인: hyper가 T156, T159와 함께 한 번에 올릴 PHP query 메서드(T161)가 main에 있다. 완료 조건: version-bump commit이 모든 manifest를 0.0.13으로 두고 Unreleased를 0.0.13으로 옮기며, 바뀐 lock의 dependency review와 v0.0.13의 consumer lock을 기록한다; 그 CI가 성공한다; annotated tag v0.0.13을 push하고 release workflow가 게시한다.—증거: 그 commit에서 make version-check와 make dependency-policy-check가 통과한다.[o]
T163Python과 TypeScript에서 generated schema 값의 문서를 읽는다. 원인: PHP(RuntimeModel::loadSet), Go(runtimemodel.LoadSet), Rust(Manifest::load_set)는 schema 값의 parse된 문서를 돌려주는데 Python과 TypeScript는 그 단계를 공개하지 않아, consumer의 Python scope(S-285)가 text를 직접 나눠야 했다. 완료 조건: Python RuntimeModel.load_set과 TypeScript loadSet이 manifest text의 문서들, 그다음 external로 표시한 문서들을 돌려주고 header 없는 text에는 SCHEMA_INVALID로 실패하며, 먼저 red를 확인하고 symbol 기록과 docs/spec/schema.md(+ko)를 갱신한다.——[o]
T1640.0.14를 릴리스한다. 원인: consumer S-285의 Python scope가 기다리는 Python과 TypeScript의 schema 값 문서 읽기(T163)가 main에 있다. 완료 조건: version-bump commit이 모든 manifest를 0.0.14로 두고 Unreleased를 0.0.14로 옮기며, 바뀐 lock의 dependency review와 v0.0.14의 consumer lock을 기록한다; 그 CI가 성공한다; annotated tag v0.0.14를 push하고 release workflow가 게시한다.—증거: 그 commit에서 make version-check와 make dependency-policy-check가 통과한다.[o]
T165kit 0.0.39, 그다음 0.0.40을 받는다 (T137에 이어). 0.0.40에 첫 mise.lock의 수정(kit 5aee092)이 있다. 원인: T128-8-3이 kit이 0.0.39부터 export하는 @polyspec/kit/install-vscode를 import한다. 0.0.39는 toolchain을 mise로 옮겨(K127) config/toolchain.json의 go, python, pythonBuild, uv, composer, cargoAudit, govulncheck, alint key가 kit의 schema에서 실패하고 install-tools가 도구 이름을 받지 않는다. 완료 조건: package.json이 kit 0.0.39를 고정하고, make kit-sync와 make kit-check가 통과하며, toolchain을 kit 0.0.39가 요구하는 대로 선언하고, 새 checkout이 설치되어 interface-check, model 검사, codec-check를 통과한다.——[o]
T165-1ARM runner가 도구를 설치하도록 kit 0.0.41을 받는다 (T165에 이어). 원인: run 38521800267의 모든 job이 install-tools에서 멈췄다. kit 0.0.40의 mise.lock은 linux-arm64를 pin하지 않는데 orm CI는 ubuntu-26.04-arm에서 실행된다(No lockfile URL found ... on platform linux-arm64). 완료 조건: orm이 kit 0.0.41을 pin하고, mise.lock이 linux-arm64를 pin하며, main의 CI가 통과한다.——[o]
T165-2mise toolchain 뒤 orm main CI를 다시 green으로 만든다 (T165-1에 이어). 원인: run 38526082365가 실패했다. 이제 PATH 맨 앞의 npm 12가 npm pack --json을 객체로 쓰고(package-declarations와 package-check는 배열을 읽는다) install-cli의 URL 의존성을 거부한다(EALLOWREMOTE). scripts/repo/toolchains.mjs는 mise.toml의 Go가 대신하는 actions/setup-go를 요구했고, editor job은 make install-go를 실행하지 않았다. 완료 조건: 각각이 npm 12와 mise가 요구하는 대로 읽거나 설치하고 검사가 통과한다. mise가 module root 안에 설치하는 Go 배포본이 go vet과 go test를 깨는 문제는 kit에 보고했다.——[o]
T165-3Go module root를 위해 kit 0.0.42를 받는다 (T165-2에 이어). 원인: run 38526082365가 go-vet-check와 go-test-check에서 실패했다. mise가 module root 안의 var/tools/mise/installs/go에 Go를 설치해 ./...가 그 test source를 지났다. 완료 조건: orm이 bootstrap이 var/tools/go.mod를 쓰는 kit 0.0.42를 pin하고 go vet이 통과한다.——[o]
T165-4mise 파일을 검사 입력으로 선언한다 (T165-3에 이어). 원인: run 38532926973가 feature-unit-check에서 실패했다. mise.lock이 어떤 검사 target의 입력도 아니었다. 완료 조건: kit-check가 mise.toml과 mise.lock을 입력으로 받고 선택 test가 통과한다.——[o]
T166모든 runtime에서 문서 집합을 parse하고 문서마다 결과를 돌려준다. 원인: flowmark catalog(그 T12-3-1)와 orm 자신의 runtimemodel이 parse 전에 dbspec 1 <document> header를 문자열 비교로 다시 읽어 set의 문서를 key로 묶는다. 어떤 runtime도 문서마다 document, position, diagnostic을 돌려주는 set parse를 공개하지 않기 때문이다. 완료 조건: 모든 runtime에 표준 set parse가 있고 vector가 있으며 먼저 red를 확인하고, runtimemodel이 그것을 쓴다. flowmark T12-3-1은 그 릴리스 뒤 그것을 받는다.——[o]
T167PHPStan을 위해 PHP client의 generic type을 선언한다. 원인: Db::transaction이 callback 결과의 type을 선언하지 않고 model row에 generic type이 없어, PHPStan level 9가 hyper module에서 method.nonObject와 mixed를 보고한다(hyper S-286-7-1, T161 뒤). 완료 조건: PHP client가 transaction 결과와 model row의 @template generic을 선언하고, PHPStan level 9 fixture가 그런 결과를 보고하지 않으며, 먼저 red를 확인하고 interface 기록을 갱신한다.——[o]
T168mise.toml의 Python을 mise에게 묻는다. 원인: python-venv-check가 mise.toml에서 release를 sed 식으로 읽었다. mise 옆의 두 번째 TOML reader다(다시 만든 바퀴 검토에서 찾았다). 완료 조건: 검사가 mise current python을 쓰고, python-venv test와 venv 검사가 통과한다.——[o]
T169PHP plan cache가 IN-list와 insert 길이마다 커지지 않게 한다. 원인: hyper product import에서 Engine::$plans(manifest마다 256개)가 8.6-35 MiB를 잡는 것을 측정했다. IN-list 길이와 여러 행 insert 길이마다 plan 모양이 따로 있기 때문이다. 완료 조건: 그 길이에 기대지 않는 plan이나, 근거를 적은 cache memory 한도를 두고, 먼저 측정으로 red를 확인하며, 모든 client의 plan cache에 같은 판정을 한다.—Evidence: contracts/fixtures/insert_parts.json가 다섯 client 모두에서 통과한다. conformance vector creates_and_save가 part를 기록한다(T169-1).[o]
T169-1conformance vector에 insert part를 기록한다 (T169에 이어). 원인: T169가 creates를 2 + 1 part로 바꿨지만 tests/conformance는 3행 INSERT 하나를 유지해, main CI가 다섯 client 모두에서 같이 실패했다.—Evidence: check compare -vector creates_and_save가 mysql, postgres, sqlite에서 vector 1개 × 5개 언어 동일을 보고한다.[o]
T169-2선택한 conformance vector를 make로 실행하고 다섯 client를 기술한다 (T169-1에 이어). 원인: tests/conformance/README.md가 아직 client 네 개와 출력 파일 네 개를 말하고, -vector를 check run에 넘기는 make target이 없어 vector 하나에도 손으로 만든 database와 make 환경이 필요하다. 완료 조건: make target이 test server로 지정한 vector를 실행, 비교, 기록하고, README가 그것과 다섯 client를 기술한다.—Evidence: VECTOR=creates_and_save가 세 database에서 vector 1개 × 5개 언어 동일을 보고한다. VECTOR와 RECORD=1은 이유를 출력하고 2로 끝난다.[o]
T169-3IN-list 길이마다 plan cache가 커지지 않게 한다 (T169에 이어). 원인: T169의 완료 근거는 insert parts만 다룬다. Planner는 IN-list의 원소마다 placeholder를 하나씩 만들므로(packages/php/src/Planner.php:525, 다른 planner도 같다), 길이마다 별도 plan과 cache 항목이 생긴다. RED: 같은 IN-list 요청을 길이 1부터 300까지 실행하고 plan cache 항목 수를 세는 test. 현재 코드에서는 길이마다 하나씩 늘어난다. 완료 조건: plan이 목록 길이에 의존하지 않거나(정해진 placeholder 묶음과 채우기 방식), cache의 메모리 상한을 밝혀 강제한다. 모든 client의 plan cache를 같은 기준으로 판단한다.—근거: commit e2bcacbf(Go)와 9767c513(PHP, Python, TypeScript, Rust)가 main에 있다. plan key는 IN list를 2의 거듭제곱 bucket으로 묶는다(마지막 값을 반복해 채우고 parameter를 다시 번호 붙인다). 그래서 각 client의 plan cache는 list 길이마다가 아니라 bucket마다 항목 하나를 갖는다. client별 red 후 green: Go TestPlanCacheBoundedByInListShape('length 11: plan cache holds 11 entries, want at most 10' 후 PASS), PHP PlanCacheBoundTest(같은 red 후 OK 1 test 600 assertions), Python test_plan_cache_bound(red 후 OK), TypeScript plan-cache-in('11 plan cache keys, want at most 10' 후 3건 중 3 통과), Rust는 이미 bucket(pad_in)을 쓰고 회귀 test in_list_keys_are_bounded_by_bucket이 통과한다. documents-check, docs:rules-check, layout-check가 통과한다. interface-check와 rust-fmt-check는 main에서도 exit 2다(이 행의 것이 아님).[o]
T169-4다섯 client의 plan cache test가 선언된다. 원인: T169-3이 client마다 test를 선언 없이 land해서 make -s parity-declarations-check가 R5 undeclared-test 5건을 보고한다: 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(T169-3 이후 발견). 수용: 각 파일은 해당 언어의 internal profile 단위가 되고(사유와 spec anchor docs/spec/parity.md#implementation-internal-tests), 바인딩은 하나씩이다. R5가 0이다. red: R5 5건. green: R5 0건.—근거: commit 07ebb236, b4d24f9e(main; 866e074a 위로 rebase, 선언 후 python test 진입점을 testcase로): 다섯 plan cache 파일이 각 언어의 internal profile unit으로 선언되어 R5 finding이 사라졌다. make -s parity-declarations-check는 더 이상 plan cache 파일을 보고하지 않는다. 남은 R5는 다른 행의 collection identity 파일(T77-6)과 rust testcase lib.rs(K204 false positive)다. python plan cache test는 testcase.main으로 실행되고 test_testcase와 함께 통과한다.[o]
T170TypeScript model을 쓰는 것만 담아 생성한다 (T137-1-3-1에 이어). 원인: kit 0.0.43의 fsm lint-ts가 orm-gen이 쓴 models.ts에서 쓰지 않는 import, 같은 이름의 class와 interface(declaration merging), 쓰지 않는 MANIFEST_* export를 찾는다. 완료 조건: TS emitter가 model이 쓰는 import와 export만 쓰고 type마다 한 번만 선언하며, generator test가 이를 확인하고, 생성 파일의 lint가 통과한다.—Evidence: 쓰지 않는 import와 interface가 합쳐진 class를 거부하는 models-scan test가 수정 전 실패하고 수정 후 통과한다. ts-check/types와 ts-model-check가 통과한다. MANIFEST_TEXT와 MANIFEST_HASH는 docs/usage.md가 정한 대로 export를 유지한다.[o]
T170-1생성 model의 TypeScript symbol을 기록한다 (T170에 이어). 원인: T170이 contracts/symbols/typescript.json을 기록하지 않고 model을 다시 생성해 main에서 make interface-check가 실패했다.—Evidence: 기록은 생성 model과 declareMember만 다르다. interface-check 8개가 통과한다.[o]
T170-2TypeScript generator test가 선언된 chain method를 읽는다 (T170에 이어). 원인: generate.mjs와 generate-scan.mjs가 T170이 없앤 합쳐진 interface를 읽어, main에서 conformance와 feature_verify_other group이 실패했다.—Evidence: 두 test가 각 model class의 declare field를 읽고 통과한다.[o]
T1710.0.15를 릴리스한다. 원인: flowmark, fsm, hyper가 기다리는 문서별 set parse(T166), 모든 client의 YAML core schema(T142-1, T142-2, T142-9부터 T142-11까지), insert part(T169), allow와 쓰지 않는 import가 없는 생성 model(T137-1-3-1, T170)이 main에 있다. 완료 조건: version-bump commit이 모든 manifest를 0.0.15로 두고 Unreleased를 0.0.15로 옮기며, 바뀐 lock의 dependency review와 v0.0.15의 consumer lock을 기록한다; 그 CI가 성공한다; annotated tag v0.0.15를 push하고 release workflow가 게시한다.—증거: 그 commit에서 make version-check와 make dependency-policy-check가 통과한다.[o]
T171-1CI cache 복원 뒤에 Rust crate를 받는다 (T171에 이어). 원인: 0.0.15 bump의 CI가 모든 Rust group에서 실패했다: Swatinem/rust-cache가 make install-rust가 받은 crate 위에 오래된 registry를 복원해 rustls 0.23.46이 없었다.—Evidence: install-rust가 복원 전의 install-rust-toolchain과 복원 뒤의 install-rust-crates로 나뉜다. scripts/check와 scripts/repo test가 통과한다.[o]
T172생성된 PHP model에 scan한 chain method를 선언한다. 원인: PHP orm-gen에 --scan이 없어(gen --set --out --namespace [--check]) module code가 부르는 chain method(ltSeq, andSlotNumber)가 어디에도 선언되지 않으며, 실행은 맞는 호출에 PHPStan이 method.notFound를 보고한다. Go, TypeScript, Rust는 scan이 찾은 method를 생성하고 Python은 맞지 않는 method를 보고하는데, docs/spec/dsl.md 2.2는 PHP만 scan 없이 둔다(hyper가 0.0.15에서 보고, 호출 8개). 완료 조건: PHP orm-gen이 --scan을 받아 source를 nikic/php-parser로 읽고, 찾은 chain method마다 정확한 parameter와 return type의 @method tag를 model class에 선언하며, 다른 generator처럼 모르는 column, operator, 인자 수를 생성 중에 거부한다. docs/spec/dsl.md(+ko)가 이를 적는다. 받는 case와 거부하는 case의 generator vector, parity unit, fixture에 대한 PHPStan 실행에서 method.notFound 0건.—Evidence: OrmGenScanTest(23)와 OrmGenScanParityTest(14)가 먼저 "flag provided but not defined: -scan"으로 실패했고 지금 통과한다. level 9 PHPStan fixture는 method.notFound를 보고하지 않고, scan하지 않은 대조 호출은 보고한다. symlink case는 RecursiveDirectoryIterator로 바꾸기 전에 exit 0으로 실패했다.[o]
T172-1nikic/php-parser의 review를 기록한다 (T172에 이어). 원인: T172가 nikic/php-parser를 dependency review 없이 packages/php/composer.json의 require로 옮겨, 65c758b5에서 CI group static의 dependency-policy-check가 실패했다(review 뒤에 lock이 바뀌었고 nikic/php-parser의 항목이 없다). 완료 조건: make dependency-review RECORD=1이 registry dependency 47개와 lock 9개를 기록하고 make dependency-policy-check가 통과한다.—Evidence: 2026-10-14T18:06:24Z에 기록한 review로 dependency-policy-check가 통과한다.[o]
T172-2PHP scan의 PHPStan case가 CI에서 통과하게 한다 (T172에 이어). 원인: 65c758b5에서 CI group feature_verify_other가 OrmGenScanTest::test_phpstan_finds_every_scanned_chain_method를 "the control call is declared"에서 실패시켰고, 로컬에서는 통과한다. 이 case는 PHPStan의 종료 상태를 읽지 않아 분석하지 않은 실행이 finding 없이 첫 assertion을 통과했다. 완료 조건: case는 0이나 1이 아닌 상태에서 PHPStan의 출력과 함께 실패하고 control에는 상태 1을 요구한다. CI의 원인을 그 출력에서 찾아 고치고 feature_verify_other가 통과한다.—Evidence: 종료 상태를 읽자 CI run 38615714975가 원인을 보였다: runner에서 PHPStan의 raw format은 control.php:6:Call to an undefined method ...::ltReadCount().를 case가 맞추던 [identifier=...] 꼬리 없이 써서 control이 method.notFound를 찾지 못했다. case는 --error-format=json을 읽고 messages[].identifier로 맞춘다. 로컬에서 통과하고, 이 항목을 닫는 commit의 CI가 static과 feature_verify_other에서 실행한다.[o]
T172-3root tests layout에 tests/generator를 선언한다 (T172에 이어). 원인: T172가 PHP 생성기와 node 생성기를 비교하는 vector tests/generator/php-scan-vectors.json을 layout-root-tests(S7)가 선언하지 않은 폴더에 더해 make layout-check가 그것을 보고한다. 완료 조건: config/layout.yml이 tests/generator를 공유 vector로 이유와 함께 선언하고, layout-check가 더는 보고하지 않는다.—Evidence: make layout-check가 tests/generator에 S7 finding을 보고하지 않는다.[o]
T1730.0.16을 릴리스한다. 원인: hyper가 dynamic chain method의 PHPStan finding을 없애려고 기다리는 PHP orm-gen scan(T172, T172-1), 개발 도구를 root에 둔 PHP client의 PHPUnit runner(T148-1, T148-1-1), 공유 vector의 case id(T148-2, T148-2-2)가 main에 있다. 완료 조건: version-bump commit이 모든 manifest를 0.0.16으로 바꾸고 Unreleased를 0.0.16으로 옮기며, 바뀐 lock의 dependency review와 v0.0.16의 consumer lock을 기록한다. 그 CI가 성공하고 annotated tag v0.0.16을 push하며 release workflow가 게시한다.—Evidence: 그 commit에서 make version-check와 make dependency-policy-check가 통과한다.[o]
T173-10.0.16의 릴리스 commit이 CI를 통과하게 한다 (T173에 이어). 원인: ae23528a의 CI run 38615714975가 실패했다: T172가 model_generation의 설명을 바꾼 뒤 docs/features.md를 다시 생성하지 않았고, CoveragePerformanceGateTest는 package directory로 경로를 만들어 T148-1-1이 지운 packages/php/vendor/bin/phpunit을 계속 실행했으며, T172-2의 PHPStan case가 실패했다. 완료 조건: 생성 문서가 최신이고, case가 vendor-php/bin/phpunit을 실행하며, static, feature_coverage, feature_verify_other가 통과한다.——[o]
T174TypeScript scan에서 not, andNot, orNot callback 안에서 부르는 chain method를 선언한다. 원인: TypeScript scan은 and, or, on, fetchKey, fetchValue에 준 callback의 첫 매개변수를 receiver class의 model로 읽었지만, PHP scan(T172)과 chain 문법(docs/spec/dsl.md)이 같게 다루는 not, andNot, orNot은 읽지 않았다. 그래서 tests/conformance/runner_typescript가 부르는 orGtReadCount와 ltReadCount를 생성된 model이 선언하지 않아 tsc가 거부한다. 완료 조건: scan이 not, andNot, orNot의 callback을 and, or처럼 다루고, 수정 전에 실패하는 generator scan case가 있으며, commit된 model이 runner가 부르는 method를 선언한다.—Evidence: generate-scan model_calls가 수정 전에 "Author lacks gtServiceSeq, lbName, neIsDisplay"로 실패했고 지금 통과한다. npm run models가 Author에 ltReadCount와 orGtReadCount를 더하고 make ts-model-check가 통과한다.[o]
T174-1scan한 chain method의 TypeScript symbol을 기록한다 (T174에 이어). 원인: T174가 Author.ltReadCount와 Author.orGtReadCount로 packages/ts/src/models/models.ts를 다시 생성했지만 TypeScript symbol을 기록하지 않아, 297b6afd에서 interface-check(feature_verify_rust, interface_contract/interface-self-test)가 그것을 예상하지 못한 symbol로 보고했다. 완료 조건: contracts/symbols/typescript.json이 두 symbol을, contracts/interfaces.json이 새 hash를 가지고 go run ./tests/interfaces/check -language typescript가 통과한다.—Evidence: -record가 정확히 두 symbol을 더하고, 계산한 hash로 검사가 통과한다.[o]
T175PHP scan에서 invocable model을 해석한다 (T172에 이어). 원인: PHP orm-gen scan은 $m = new C; $m->chain()은 해석하지만, hyper module이 각각 약 130번 쓰는 invocable model 관용구 (new C)($db)->chain()은 해석하지 않는다. ScanVisitor에 new C 식이나 model을 돌려주는 __invoke 결과에 대한 호출 case가 없어, --scan이 member, page, board, canister, product, ordersheet에서 chain method를 하나도 선언하지 않았다(hyper가 0.0.16에서 보고). 완료 조건: (new C), (new C)(...)와 그 chain에 대한 호출이 C로 해석되고, tests/generator/php-scan-vectors.json의 scan vector와 PHPStan fixture가 그 관용구를 쓰며, PHP scan과 node scan과의 parity가 통과한다.—Evidence: 변경 전 orm-gen-scan이 3 case에서 실패했고(invoked vector 둘, PHPStan method.notFound) 변경 후 27건 통과한다. orm-gen-scan-parity 16건 통과, interface-check -language php는 symbol 변경이 없다.[o]
T176Python client에서 3.46보다 오래된 SQLite를 거부한다. 원인: Go(db.go), Rust(db.rs), PHP(Orm.php), TypeScript(driver.ts) client는 docs/dialects.md대로 연결할 때 3.46보다 오래된 SQLite를 CAPABILITY_UNSUPPORTED로 거부하지만, Python client에는 그 검사가 없고 Python은 system SQLite를 link한다. 완료 조건: RED case가 3.46보다 오래되었다고 보고하는 SQLite에 Python client가 연결하는 것을 보이고, 이후 client가 다른 client와 같은 message의 CAPABILITY_UNSUPPORTED로 거부하고 3.46 이상은 받아들인다. 공유 vector는 만들지 않는다: 각 client는 자기가 link한 library의 version을 읽고, 어느 case도 다른 client의 그것을 바꿀 수 없으므로 각 검사는 자기 비교다.—Evidence: 변경 전 test_sqlite_version이 3.45.3에서 "OrmError not raised"로 실패했다. 변경 후 3.45.3, 3.9.0, 2.8.17을 거부하고 3.46.0, 3.50.4, 4.0.0을 받아들이며, python-unit-check가 통과한다.[o]
T1770.0.17을 release한다. 원인: hyper module이 기다리는 호출한 model의 PHP orm-gen scan(T175), Python client의 SQLite version 검사(T176), consumer가 기다리는 모든 client의 ordered-json 0.0.9(T178), TypeScript infrastructure script(T137-1-4-5, T137-1-4-9)가 main에 있다. 완료 조건: version-bump commit이 모든 manifest를 0.0.17로 하고 Unreleased를 0.0.17로 옮기며, 바뀐 lock의 dependency review와 v0.0.17의 consumer lock을 기록한다. 그 commit에서 CI가 통과하고, annotated tag v0.0.17이 release asset을 게시한다.—Evidence: T177-1, T177-2, T177-3 뒤 release commit bcba7ddc의 CI가 통과했다(run 38648667367). annotated tag v0.0.17이 release workflow(run 38651315793)를 실행해 asset 일곱 개로 v0.0.17을 게시했고, Python wheel은 sha256으로 polyspec-ordered-json 0.0.9를 요구한다.[o]
T177-10.0.17 release commit의 package check를 통과시킨다 (T177에 이어). 원인: 739f8709의 CI가 package-check에서 실패했다. Rust client를 path dependency로 검사하는 외부 crate에 lock이 없어, offline cargo가 git dependency polyspec-ordered-json을 tag v0.0.8로 해석하는데 runner가 되살린 git database에 그 tag가 없었다. 이전 실행은 cache에 tag가 있을 때만 통과했다. 완료 조건: 외부 crate가 packages/rust/Cargo.lock을 쓰고, client의 cargo fetch --locked가 받은 것만 있는 cargo home으로 검사가 통과한다.—Evidence: cargo fetch --locked 뒤의 새 CARGO_HOME에서 변경 전 외부 crate의 cargo check가 "failed to find tag v0.0.8"로 실패했다. lock을 쓰면 통과하고, 그 cargo home으로 make package-check가 통과한다.[o]
T177-2Linux에서 시작한 server의 첫 줄을 읽는다 (T177에 이어). 원인: a7d4dac1의 CI가 repo-check에서 실패했다. startLogged case가 Linux에서 stderr에 아무것도 없이 멈췄다. startLogged는 server를 spawn한 뒤에 log 크기를 읽었는데, Linux에서는 그때 이미 server가 줄을 써서 읽기가 ready 줄 뒤에서 시작했고 이후 쓰기가 없어 깨어나지 않았다. macOS는 spawn이 느려 통과했을 뿐이다. 완료 조건: 읽기가 spawn 전의 log 크기에서 시작한다.—Evidence: CI log에서 case가 20초 제한에서 stderr가 빈 채로 끝났다. 즉 한 줄도 읽지 못했다. spawn 전에 크기를 읽으면 로컬에서 8회 중 8회 통과한다.[o]
T177-3server test에서 멈춘 process를 상태로 판정한다 (T177에 이어). 원인: e8dac3c8의 CI가 test-servers-check에서 실패했다. Linux에서 stopPid는 종료 event에 돌아왔지만 끝난 가짜 server는 새 parent process가 아직 reap하지 않은 zombie였고, case는 zombie에도 성공하는 kill(pid, 0)을 실행 중으로 보았다. macOS는 이전 shell 단계가 reaper에게 시간을 주어 통과했을 뿐이다. 완료 조건: stopPid case와 binary log fixture가 끝난 process를 ps로 판정한다: 없거나 상태 Z.—Evidence: CI record가 종료 event 17 ms 뒤 "quick server still runs after stopPid"를 적는다. 상태 판정으로 test-servers.test가 로컬에서 9건 통과하고, stopPid case는 3회 중 3회 통과한다.[o]
T178모든 client에서 ordered-json 0.0.9를 쓴다. 원인: ordered-json 0.0.9에는 consumer가 import 속도에 필요한 Python parser(T1.41)가 있는데, orm은 PHP(zip URL), TypeScript, Python wheel의 Requires-Dist에서 ordered-json 0.0.8을 정확히 고정해 consumer가 바꿀 수 없다(kit과 consumer가 보고). 완료 조건: PHP, TypeScript, Python, Rust, Go client가 0.0.9를 요구하고(Go는 새 module 경로 github.com/polyspec/ordered-json/packages/go로, Python은 sha256을 붙인 wheel URL로), lock과 dependency review를 기록하며, 각 client의 codec과 JSON 검사가 통과한다.—Evidence: 내려받은 wheel의 sha256이 bc4d26a9…f01d다. test server에서 JSON, codec, AES case의 go test가 통과하고, PHP hostcodec unit 61건, Rust codec test 8건, python-unit-check, TypeScript codec vector가 통과한다. review 뒤 dependency-policy-check가 통과한다.[o]
T179TypeScript orm-gen backend가 검사한 읽기를 생성한다 (fsm을 위해 kit이 보고). 원인: 생성된 TypeScript가 entity를 entities.get(..)!로, column을 column(..) as T로 읽어, kit 0.0.47의 lint가 모든 consumer의 생성 model을 거부한다. 완료 조건: 생성 code가 entity를 이름을 대며 throw하는 guard로 찾고, column 값을 typeof/instanceof로 검사해 column 이름을 댄 TypeError를 던지며, ordered_json column은 TypeScript model runtime의 검사한 accessor Core.jsonColumn으로 읽는다(contracts/interfaces.json에서 Core는 internal이다). 생성 signature는 바뀌지 않고, test가 생성 출력의 검사하지 않은 읽기를 거부한다.—Evidence: packages/ts/tests/generate.mjs의 새 uncheckedReads 검사가 이전 generator에서 검사하지 않은 읽기 90개로 실패했고 지금 통과한다. styled-value/json-column이 ordered-json 값과 SQL NULL을 받아들이고 plain object를 author.json_setting을 댄 TypeError로 거부한다. 다시 생성한 model이 oxlint와 models-scan을 통과하고, contracts/symbols/typescript.json의 export된 model class key는 바뀌지 않았다.[o]
T1800.0.18을 release한다. 원인: fsm이 kit 0.0.47의 lint를 통과하려고 기다리는 orm-gen TypeScript 출력의 검사한 읽기(T179), packages/go의 Go module(T137-1-5, T137-1-6), kit의 layout(T137-1-7, T137-1-8, T137-1-9), TypeScript infrastructure reader와 checker(T137-1-4-2, T137-1-4-3, T137-1-4-6), kit 0.0.47과 그 lint(T137-1-4-7, T137-1-4-7-1)가 main에 있다. 완료 조건: version-bump commit이 모든 manifest를 0.0.18로 하고 Unreleased를 0.0.18로 옮기며, dependency review와 v0.0.18의 consumer lock을 기록한다. 그 commit에서 CI가 통과하고, annotated tag v0.0.18과 packages/go/v0.0.18이 release를 게시한다.—Evidence: T180-1과 T180-2 뒤 release commit 4e304aac의 CI가 통과했다(run 38665460029). annotated tag v0.0.18과 packages/go/v0.0.18이 release workflow(run 38670294931, 38670294798)를 실행해 asset 일곱 개로 v0.0.18과 Go module github.com/polyspec/orm/packages/go v0.0.18을 게시했다.[o]
T180-1release에서 test 도구와 build 기록을 뺀다 (T180에 이어). 원인: make release-assets가 Python wheel을 거부했다. wheel이 test validator jsonschema-rs를 release가 받지 않고 consumer도 필요 없는 extra marker로 선언했다. TypeScript client의 composite build가 증분 기록 packages/ts/tsconfig.build.tsbuildinfo를 commit했고, go.work.sum에 tests module의 sum이 없었다. 완료 조건: test validator는 make install-python이 설치하는 PEP 735 dependency group이고, TypeScript build는 기록을 자기 출력 옆에 두고 지우며, go.work.sum이 최신이고, release-assets가 통과한다.—Evidence: 변경 전 release-assets가 "Requires-Dist jsonschema-rs==0.58.6; extra == "test" is a marker"로 실패했고 지금 통과한다. make install-python이 group에서 jsonschema-rs를 설치하고, typescript-build가 tree에 tsbuildinfo를 남기지 않으며, dependency-policy-check가 통과한다. kit이 dependency group review를 K177로 맡았다.[o]
T180-2Unicode 표의 검사한 읽기를 생성한다 (T180에 이어). 원인: release commit의 CI가 go-test-check와 feature-verify-other에서 실패했다. T137-1-4-7-1의 lint 작업이 생성 파일 packages/ts/src/dbspec/unicode_word.ts를 손으로 고쳐, go run ./tests/dbspec/unicode -write의 출력과 같지 않았다. 완료 조건: 생성기가 present로 검사한 읽기를 내보내고, file을 다시 생성하며, models.ts의 생성 TypeScript가 생성기와 같다(ts-model-check).—Evidence: CI record가 TestCommittedTablesAreGenerated(unicode_word.ts가 Unicode 17.0.0 표와 다름)를 적는다. 변경 뒤 go test ./tests/dbspec/unicode, typescript-build, ts-model-check가 통과하고 4e304aac의 CI가 통과한다.[o]
T181kit 0.0.48부터 0.0.50까지 받는다. 원인: kit 0.0.48(K169, K171: node는 mise.toml로, recipe의 모든 도구는 kit 변수나 절대 경로로), 0.0.49(K172부터 K177), 0.0.50(K178부터 K185)이 copy, Makefile 규칙, dependency review를 바꾸고, kit은 0.0.50과 함께 한 번에 받기를 권한다. 완료 조건: make kit-sync가 그중 최신을 받고, 모든 recipe가 K171대로 도구를 실행하며, kit-check, lint-ts, layout-check, dependency-policy-check가 통과한다. 우회 원인: 0.0.50이 release되지 않았다. Retry: kit 0.0.50이 release될 때.—Evidence: make kit-sync로 kit 0.0.50을 받는다(kit-check 통과). node 26.12.0은 mise.toml의 도구이고 .node-version은 지웠다. 모든 recipe가 $(NODE), $(NPM), $(GO), $(COMPOSER), $(PYTHON3)으로 도구를 실행한다(kit-check가 이름만 쓴 84곳을 찾았고 지금 0). knip이 binary를 선언하고, ciExempt가 이유와 함께 feature-owner-check를 적으며, php-ext composer.json은 자기 lock과 함께 dependency policy가 덮는다. ruff 0.17.0과 vulture 2.16이 mise.toml의 도구라 make lint-python이 실행되고, ruff 125건, format 135건, vulture 658건을 disable 없이 고쳐 통과한다. lint-ts, layout-check, dependency-policy-check, release-coverage, python-unit-check, scripts/repo와 scripts/check가 통과한다.[o]
T181-1vulture의 lock sidecar를 commit한다 (T181에 이어). 원인: mise.lock은 pypi:vulture의 uv lock을 경로 .mise/locks/pypi-vulture/2.16과 sha256으로 적는데, T181 작업이 그 folder를 지워 새 checkout에서 mise install --locked가 "dependency sidecar … .mise/locks/pypi-vulture/2.16: No such file or directory"로 실패했다(kit이 보고). 완료 조건: mise.lock이 적은 digest의 sidecar를 추적하고, 새 worktree에서 install-tools가 성공한다.—Evidence: origin/main의 새 worktree에서 make install-tools가 그 message로 실패했다. make toolchain-lock이 sidecar와 그 digest를 mise.lock에 썼고, 그 뒤 install-tools가 성공하며 kit-check, layout-check, dependency-policy-check, release-coverage가 통과한다.[o]
T181-2dbspec의 Python connection type을 공개 type으로 둔다 (T181에 이어). 원인: T181이 Python introspect_dbspec와 apply signature의 정의되지 않은 이름 Connection을 private Protocol _Connection으로 바꿔, 공개 signature가 private type을 적고 main에서 make interface-check가 실패했다(python/Dbspec.introspect, apply, recover: want Connection, got _Connection). 완료 조건: Protocol이 polyspec.orm.dbspec.introspect의 공개 Connection이고, signature가 그것을 적으며, Python symbol이 그것을 기록하고, interface-check가 통과한다.—Evidence: interface-check가 위의 want/got 줄로 실패했다. Connection을 export하고 그 symbol 둘(과 checker가 계산한 python snapshot hash)을 기록하자 interface-check, lint-python, python-unit-check, repository 검사가 통과한다.[o]
T181-3mise lock sidecar의 owner를 선언한다 (T181-1에 이어). 원인: T181-1이 .mise/locks/pypi-vulture/2.16/uv.lock을 owner 없이 추적해 main에서 feature-unit-check/owners가 실패했다("every tracked file selects a behaviour test or declares its scope"). 완료 조건: mise.lock을 검증하는 kit-check가 .mise/locks/**를 owner로 갖고 owners 검사가 통과한다.—Evidence: owners 검사가 그 경로 하나를 finding으로 적었다. contracts/check-inputs.json의 kit-check input에 .mise/locks/**를 넣자 통과하고, kit-check, layout-check, repository 검사도 통과한다.[o]
T181-4push-gate를 mise.toml을 읽는 setup-node release에 고정한다 (T181에 이어). 원인: T181이 node를 mise.toml로 옮기고 node-version-file: mise.toml을 썼는데, push-gate.yml은 actions/setup-node를 mise.toml을 읽지 못하는 이전 v7 commit 82076278에 고정해, T181 뒤 main의 모든 push-gate run이 실패했다("Unable to find Node version '[tools]'"). 완료 조건: push-gate가 kit의 release.yml과 같은 949feb24(v7.1.0)에 고정되고 main에서 push-gate가 통과한다.—Evidence: 2a934dc6부터 a2232a24까지 main의 push-gate run이 setup-node에서 그 message로 실패했고, 949feb24로 해석되는 v7 tag를 쓰는 docs-pages는 mise.toml을 읽어 통과했다. push-gate.yml은 이제 949feb24에 고정된다.[o]
T182validation, encoding, planning, execution 및 공유 runtime을 포함한 전체 PHP client protocol을 native extension으로 구현한다.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-0extension에 PHP client의 class 이름을 등록한다 (T182에 이어). 원인: extension은 Polyspec\Orm\Dbspec\Native와 Polyspec\Orm\Native를 등록하므로 PHP code가 그 이름을 써서 골라야 하지만, T182의 결정은 사용자 code가 extension 유무와 관계없이 같게 한다. 완료 기준: extension이 PHP client의 이름(Polyspec\Orm\Dbspec\Dbspec, Polyspec\Orm\Code 등)을 등록한다. extension이 있는 process는 그 class의 client PHP 파일을 하나도 불러오지 않고, 없는 process는 불러온다. 선언 test는 같은 명칭으로 extension과 client를 각자의 process에서 비교한다. Native namespace는 alias 없이 제거한다. docs/spec/dbspec.md(+ko)가 규칙을 알린다.—make dbspec-php-extension-check/declarations[o]
T182-0-1extension의 모든 검사를 PHP client test의 case로 둔다 (T182-0에 이어). 원인: extension test(packages/php-ext/tests)는 extension과 PHP client를 한 process에서 비교한다(DbspecTest: hashes, render, manifest diagnostics, bytes, arguments, objects, documents, introspect, apply. DbspecSetTest, DbspecShowCreateTest, DbspecAnalyzeTest, DbspecNamesTest, DbspecSetsTest, DbspecSetParseTest, CoverageSchemaDefinitionTest). 같은 class 이름에서는 이것이 불가능하다. 완료 기준: 각 비교가 짝이 되는 packages/php/tests 파일의 case가 되고 기대값(공유 vector, 또는 PHP client가 지금 내는 값이나 예외를 test에 적은 것)을 가진다. 그래서 같은 파일이 각 구현을 자기 process에서 검사한다. PHP client는 새 case를 모두 통과한다.—증거: 공유 vector가 이미 고정하는 비교는 extension이 실행할 client test다(DbspecTest, DbspecManifestTest, DbspecRenderTest, DbspecIntrospectTest, DbspecPlanApplyTest, DbspecApplyTest, DbspecAnalyzeTest, DbspecSetTest, DbspecSetParseTest, sets helper, CoverageSchemaDefinitionTest). 나머지는 packages/php/tests/dbspec_behaviour.php의 case 176개다(bytes, arguments, manifest, objects, documents, introspect 실패, apply 인자, SetResult, Names, SQLite apply script 112개). packages/php/testing/dbspec/behaviour.php가 그 결과를 PHP library에서 packages/php/tests/fixtures/dbspec-behaviour.json으로 기록하며, 두 번 기록해도 같고 machine 경로가 없다. DbspecBehaviourTest가 비교한다(test 177개, assertion 353개). tests/dbspec/show-create.json만으로 공개 introspect를 검사하던 extension의 show-create introspection test는 packages/php/tests/DbspecShowCreateIntrospectTest.php로 옮겼다(test 9개). 기록 중에 PHP library의 요소 type 미검사를 찾아 T182-0-1-1로 먼저 고쳤다. make -s dbspec-php-check와 make -s feature-unit-check가 통과한다.[o]
T182-0-1-1PHP library가 document set과 그 object graph의 요소 type을 검사한다 (T182-0-1에 이어). 원인: extension은 array가 다른 type을 담은 document set이나 document graph를 거부한다(src/convert.c의 %s holds %s, not %s, document set과 Table::$columns, Document::$uses, Plan::$renameTables 같은 property 24개). 그러나 PHP library는 array 요소의 type을 정하지 않는 PHP property type에 기대므로, Dbspec::manifest(['text'])가 Attempt to read property "name" on string 경고를 내고 그대로 진행한다(T182-0-1 기록 중에 발견). 완료 기준: PHP library가 document나 plan을 받는 모든 진입점에서 같은 순회 순서와 같은 message의 TypeError를 발생시킨다. 검사하는 property마다 거부 case가 있고, vector의 유효한 document는 계속 통과한다.—증거: Polyspec\Orm\Dbspec\Graph(@internal)가 extension의 순서로 검사하고, emit, render, manifest, externalDifferences, emitPlan, chain, diff, compareSchemas, installedDifferences, addTablesAndColumnsSteps, planSteps, exportMermaid와 apply 명령(extension처럼 dialect와 error mode 검사 뒤)이 부른다. RED: main의 library에서 packages/php/tests/DbspecGraphTest.php가 test 50개 중 49개 실패, 경고 12개, deprecation 2개. GREEN: test 50개, assertion 841개(진입점 10개를 거친 document property 37개, list와 plan case 11개, 첫 요소, 유효한 document). 새 graph target과 함께 make -s dbspec-php-check가 통과하고 make -s feature-unit-check가 exit 0이다.[o]
T182-0-1-2T182-0-1과 T182-0-1-1이 더한 PHP test를 bind한다 (T182-0-1에 이어). 원인: 88cdbe1a와 69115256이 DbspecGraphTest, DbspecBehaviourTest, DbspecShowCreateIntrospectTest와 그 case 파일을 parity binding 없이 더했고, 그 commit이 parity 선언 검사를 실행하지 않아 main에서 실패했다. 완료 기준: 각 test method가 php-internal unit이 되고, case 파일이 php support 파일이 되며, 검사가 성립한다.—증거: RED: main에서 make -s parity-declarations-check가 R5 undeclared-test 네 개(packages/php/tests/dbspec_behaviour.php, DbspecBehaviourTest.php, DbspecGraphTest.php, DbspecShowCreateIntrospectTest.php)로 exit 1. GREEN: php-internal unit 일곱 개와 support 항목. 검사가 "every declaration holds"를 출력한다.[o]
T182-0-1-3Graph의 PHP symbol을 기록한다 (T182-0-1-1에 이어). 원인: 88cdbe1a가 contracts/symbols/php.json을 기록하지 않고 packages/php/src/Dbspec/Graph.php를 더했고, 그 commit은 dbspec test만 실행하고 interface 검사는 실행하지 않아, main에서 make interface-check가 class와 method 열 개에 대해 php: unexpected ...Graph로 실패했다. 완료 기준: snapshot이 Graph를 기록하고, 공개 symbol 여섯 개가 이유와 함께 nativeOnly 항목이 되며(R11), 검사가 성립한다.—증거: RED: main에서 make -s interface-check가 Graph symbol 열한 개로 not ok - interfaces/php 실패. GREEN: -record -language php가 그것과 symbol hash를 더하고(44줄), make -s interface-check가 exit 0이며, nativeOnly 항목 여섯 개를 선언하고 make -s parity-declarations-check가 성립한다.[o]
T182-0-1-4plans.json을 하나뿐인 reader로 읽는다 (T182-0-1에 이어). 원인: DbspecGraphTest.php(T182-0-1-1)와 dbspec_behaviour.php(T182-0-1)가 tests/dbspec/plans.json을 직접 열었지만, DbspecPlanReaderTest는 dbspec_plan_cases.php만 그것을 열기를 요구한다. 완료 기준: 두 파일이 plan_vectors()로 읽고 reader test가 통과한다.—증거: RED: parity 실행에서 unit php-internal-dbspec-plan-reader가 plans.json must be opened by one file only: DbspecGraphTest.php, dbspec_behaviour.php, dbspec_plan_cases.php로 실패했다. GREEN: 두 파일이 plan_vectors()를 쓰고, DbspecPlanReaderTest가 통과하며, DbspecGraphTest(test 50개)와 DbspecBehaviourTest(test 177개)도 계속 통과한다.[o]
T182-0-2extension이 client의 이름을 쓰고 client의 test를 실행한다 (T182-0-1에 이어). 완료 기준: stub과 src/*.c가 Polyspec\Orm\Dbspec*와 Polyspec\Orm\Code를 등록한다. extension의 parity binding과 make target이 extension을 불러 packages/php/tests 파일을 실행한다. packages/php-ext/tests에는 extension 자신의 검사(declarations, package, required modules)만 남는다. Native namespace, mirror 검사, declarations.php의 이름 바꾸기를 제거한다. interface 기록, contracts/interfaces.json, docs/spec/dbspec.md(+ko)가 client class를 이름 붙인다.—make dbspec-php-extension-check/declarations, make dbspec-php-extension-check/behaviour[o]
T182-0-2-1column limit document를 test의 memory 안에서 거부한다 (T182-0-2에 이어). 원인: DbspecRulesTest limit/columns(column 1000개짜리 table 121개)는 120001번째 column이 limit에 걸리기 전까지 process 기준선보다 PHP library에서 98 MB, extension에서 109 MB 높은 peak가 나온다. parse된 column 하나당 약 0.9 KB다. extension을 불러 PHPUnit의 128 MB 제한 아래에서 실행하면 Allowed memory size of 134217728 bytes exhausted로 process가 끝난다(extension으로 client test를 실행하다 발견, T182-0-2). 완료 기준: 두 구현에서 parse된 column의 memory를 측정하고 가장 큰 몫을 없애 두 구현 모두 limit case의 peak가 제한보다 충분히 낮다. unit test가 limit/columns case의 peak가 밝힌 한계보다 낮음을 확인한다. 측정(2026-10-18): 가장 큰 합법 document(column 120000개)의 peak는 PHP library 100.6 MB, extension 196.6 MB이다. 120000 column은 limit가 허용하는 document이므로 limit 검사의 결함이 아니다. PHP library의 비용은 column과 그 position 하나당 약 650 byte이고, extension은 C model과 PHP object graph를 동시에 가져 두 배다. branch feat/ext-names-T182-0-2가 memory 요구(phpunit.xml.dist의 256M)와 peak를 160 MB 아래로 확인하는 DbspecRulesTest::test_largest_document를 담고 있으며, PHP library는 통과하고 extension은 실패한다.—Commands: make dbspec-php-extension-check/declarations[o]
T182-0-2-1-1PHP library의 column당 memory를 줄여 limit/columns case의 peak가 산출된 상한 아래에 든다(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-2Names::column이 자기 인자 오류를 알린다 (T182-0-2에 이어). 원인: column 목록에 문자열이 아닌 요소가 있으면 PHP library는 내부 호출의 TypeError Names::render(): Argument #2 ($suffix) must be of type string, int given을 내 호출자가 넘기지 않은 parameter를 이름 붙이지만, extension은 Names::column(): Argument #3 ($columns) must contain only strings, int given이라고 한다. 완료 기준: PHP library가 extension의 message를 내고, behaviour case names/column/non-string-element를 다시 기록하며, 두 구현이 통과한다.—증거: RED: 이름을 바꾼 extension으로 behaviour test를 실행하면 names/column/non-string-element만 실패했고, PHP library는 Names::render(): Argument #2 ($suffix) must be of type string, int given을 기록했다. GREEN: Names::column이 Polyspec\Orm\Dbspec\Names::column(): Argument #3 ($columns) must contain only strings, int given 오류를 발생시킨다. case를 다시 기록했다(한 줄 변경). DbspecBehaviourTest가 PHP library와 이름을 바꾼 extension build에서 test 177개로 통과한다. make -s dbspec-php-check가 통과한다.[o]
T182-0-3conformance check는 php-extension을 extension을 켠 PHP runner로 돌리므로 모든 conformance vector가 extension에서 실행된다 (T182-0-2 후속). Cause: config/parity.json이 php-extension을 구현으로 선언하는데 tests/conformance/check/main.mts의 runOne에 그 case가 없어, RECORD=1을 포함한 모든 conformance 실행이 unsupported language php-extension으로 멈춘다tests/conformance/check, the Makefile conformance targetsnode --test tests/conformance/check/main.test.mts[o]
T182-1extension의 error catalog (T182에 이어). 완료 기준: extension이 다른 client처럼 orm-gen이 생성한 stub에서 errors.yaml의 code를 Polyspec\Orm\Native\Code로 등록하고, 상수는 PHP client의 Code와 같다. profile은 그 구현의 모든 unit을 요구하므로, client profile의 error-code api unit은 T182-6이 extension을 그 profile에 넣을 때 이것을 집계한다.—증거: orm-gen errors --lang php-extension이 packages/php-ext/stubs/code.stub.php를 쓴다. packages/php-ext/scripts/arginfo.php가 gen_stub.php로 src/code_arginfo.h를 만든다. MINIT이 class를 등록한다. declarations.php가 native 명명 규칙(마지막 namespace 단계가 Native)으로 Polyspec\Orm\Code와 비교한다. RED: make -s dbspec-php-extension-check/declarations가 test_client에서 Polyspec\Orm\Native\Code와 상수 40개가 (not declared)로 실패했다. GREEN: arginfo 검사와 declarations target이 test 5개로 두 번 통과한다. catalog drift loop가 go, php, php-extension, rust에서 같은 파일을 생성한다. interface 검사가 class를 기록한다(contracts/symbols/php-extension.json). make -s feature-unit-check가 exit 0이다.[o]
T182-1-1interface contract test에서 extension의 모든 stub을 복사한다 (T182-1에 이어). 원인: T182-1이 packages/php-ext/stubs/code.stub.php를 더하고 그 class를 contracts/symbols/php-extension.json에 기록했지만, tests/interfaces/check/contract.test.mts의 case the extension declarations are checked against the contract는 임시 root에 orm_dbspec.stub.php만 복사하므로, 기록된 snapshot이 복사본에 없는 class를 이름 붙인다. T182-1은 이 test가 아니라 php-extension의 interface 검사를 실행했다. 완료 기준: case가 extension root의 모든 stub을 복사하고 통과한다.—증거: RED: main에서 case가 the recorded stub and snapshot are refused로 실패한다. GREEN: 나머지 stub을 그대로 복사하면 contract.test.mts가 통과한다.[o]
T182-2extension의 codec (T182-1에 이어). 완료 기준: 모든 logical type의 encode와 decode, AES와 decode vector. codec, codec-aes, codec-decode suite가 id로 실행된다.—Commands: make dbspec-php-extension-check/declarations[o]
T182-2-1extension이 모든 logical type의 codec을 구현하고 codec suite(tests/codec/vectors.json)가 그 id로 extension에서 실행된다 (T182-2 후속). Cause: packages/php-ext에 codec이 없어 extension을 켜도 PHP library test가 Codec을 PHP로 돌린다packages/php-ext/src, the stub, config/parity.jsonmake dbspec-php-extension-check/codec, make dbspec-php-extension-check/declarations[o]
T182-2-2extension이 host style을 decode하고 codec-decode suite(tests/codec/decode-vectors.json)가 그 id로 extension에서 실행된다 (T182-2-1 후속)packages/php-ext/src, the stub, config/parity.jsonmake dbspec-php-extension-check/decode[o]
T182-2-3extension이 AES와 blind index를 구현하고 codec-aes suite(tests/codec/aes-vectors.json)가 그 id로 extension에서 실행된다 (T182-2-2 후속)packages/php-ext/src, the stub, config/parity.jsonmake dbspec-php-extension-check/aes, make dbspec-php-extension-check/hostcodec[o]
T182-2-4확장이 오류를 반환하지 않는 경로에서도 명시적으로 실패하게 한다(T182-2 하위). 원인: validator.c fail은 OrmException을 찾지 못하면 예외 없이 false를 반환해 Validator::restoreKey와 conflictKey가 null을 조용히 반환한다. db.c scall은 호출할 수 없을 때 예외 없이 false를 반환하고 record_inserted가 그 결과를 무시한다.packages/php-ext/src/validator.c, packages/php-ext/src/db.c, packages/php-ext/src/config.m4, packages/php-ext/tests/silent-pathsmake dbspec-php-extension-check/silent-paths[o]
T182-3extension의 IR validator (T182-1에 이어). 완료 기준: tests/ir/validate.json이 case 명칭으로 실행되고 다른 client와 같은 code를 반환한다.—make dbspec-php-extension-check/ir, make dbspec-php-extension-check/declarations[o]
T182-4extension의 planner (T182-3에 이어). 완료 기준: planner, insert-parts, ir-answers가 id로 실행된다.—make dbspec-php-extension-check/planner, make dbspec-php-extension-check/declarations[o]
T182-5PHP 자체 database 계층 위의 extension executor (T182-2, T182-4에 이어). 완료 기준: statement는 새 driver가 아니라 PHP가 이미 제공하는 database API를 C에서 호출해 실행한다. statement-events, statement-fault, sqlite-paths, mysql-session이 실행된다.—Commands: make dbspec-php-extension-check/declarations[o]
T182-5-1extension이 Polyspec\Orm\Db의 연결과 statement 경로를 PDO 위에서 C로 구현한다: Orm::connect를 통한 연결, select, scalarOf, writeOf, paginate, args, exec, fetch, statement event이며, sqlite-paths와 mysql-session이 extension에서 실행된다 (T182-5 후속)packages/php-ext/src, the stub, config/parity.jsonmake dbspec-php-extension-check/executor[o]
T182-5-2extension이 Polyspec\Orm\Db의 transaction과 savepoint를 C로 구현한다: begin, finish, savepoint, cleanup, audit, fault injection이며, case가 transaction 안에서 도는 statement-events와 statement-fault가 extension에서 실행된다 (T182-5-1 후속)packages/php-ext/src, the stub, config/parity.jsonmake dbspec-php-extension-check/executor[o]
T182-5-3extension이 Polyspec\Orm\Db의 request hook, Utils, Stats를 C로 구현하므로 Db의 모든 public member가 extension의 것이다 (T182-5-2 후속)packages/php-ext/src, the stub, config/parity.jsonmake dbspec-php-extension-check/executor[o]
T182-5-3-1extension Db의 주석이 구현한 것을 적는다 (T182-5-3 후속). Cause: packages/php-ext/src/db.c:8-10과 polyspec_orm.stub.php:896-898이 transaction, savepoint, audit, execute, Utils, Stats method가 T182-5-2와 T182-5-3 전까지 INTERNAL을 던진다고 아직 적는데, 두 항목이 이를 구현했다packages/php-ext/src/db.c, packages/php-ext/src/polyspec_orm.stub.phpmake 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-4extension에서 unsubscribe하면 그 unsubscribe closure의 function을 해제한다 (T182-5 후속). Cause: extension의 subscribe()마다 unsubscribe closure용 persistent function struct 하나를 할당하고 해제하지 않아, subscribe와 unsubscribe를 되풀이하는 오래 사는 process가 끝없이 커진다packages/php-ext/src/db.c, a leak testmake dbspec-php-extension-check/executor[o]
T182-5-5extension에서 transaction과 statement timeout이 PHP library와 같이 동작한다 (T182-5 후속). Cause: extension을 켜면 ModelTest test_transactions가 CONFIG "row locks are allowed only inside a transaction"(packages/php-ext/src/db.c:1265)로 실패하고, test_statement_timeout이 MySQL과 PostgreSQL에서 실패하며, test_statement_timeout_through_a_pooler는 끝나지 않아 그 경로에서 lock wait나 statement timeout이 지켜지지 않는다; executor check는 ModelTest를 돌리지 않는다packages/php-ext/src/db.c, the dbspec-php-extension-check/executor target with the ModelTest casesmake dbspec-php-extension-check/executor[o]
T182-6extension의 query, model, collection client (T182-5에 이어. T199는 이 항목의 표면이다). 완료 기준: 생성된 model이 extension에서 실행된다. MySQL, PostgreSQL, SQLite conformance, identity-load, aggregate-numeric, decimal-model, styled-column-states, authenticated-encryption, install-changed-database가 실행된다. client profile이 php-extension을 나열하고 parity가 모든 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. main 완료 증거: 기존 model-native 통과 106건과 수정한 child 사례 4건(35 assertions), declaration 8건/24 assertions 및 3건/8 assertions, 남은 library 7건/47 assertions, 명명된 native fixture 39건 전체, aggregate/authentication 여섯 클라이언트 기록, audit_lazy만 재검증한 세 database의 59개 벡터 비교, interfaces/results tail 종료 코드 0, 최신 parity 선언 검사 통과. 기록: var/evidence/final-integrated-20261026/t1826-acceptance-evidence-index.json, conformance-after-fixes-20261027.log, audit-lazy-comparison/, interfaces-results-tail-20261027-final.log, parity-declarations-after-native-api-audit.log. 소스 수정: f76c5a87d, 8a91e2697, 379a177a4. 최초 전체 대상 명령은 종료 코드 2였으며, 통과한 벡터를 반복하지 않고 실패 벡터와 아직 실행되지 않았던 결과 구조 검증 단계를 각각 확인했다.[o]
T182-6-12PHP extension의 Collection을 C가 소유하고 IteratorAggregate로 native Iterator를 반환하면서 typed key identity, 순서가 있는 교체, 지연 snapshot 순회, clone 및 value callback 동작을 유지한다(T182-6에 이어).packages/php/src/Collection.php, packages/php-ext/src/collection.c, packages/php-ext/src/collection.h, packages/php-ext/tests/CollectionTest.phpmake collection-php-extension-check는 고정 PHP 8.4로 빌드하고 extension 로드 상태에서 native Collection PHPUnit case를, library mode에서 PHP source identity/coercion case를 실행한다. T182-6 전체나 database 간 parity를 완료하지 않는다.[o]
T182-6-13PHP extension이 Request, StyledValue, Page, Func, PendingTime를 PHP source API와 동작을 보존하는 native C 구현으로 소유한다(T182-6에 이어). 인수 조건: 각 class를 generated stub에서 한 번 등록하고, focused native 및 source unit test가 전체 public surface, Request canonicalization, PHP weak scalar 규칙, NULL/error 동작을 검증한다.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.jsonmake model-types-php-extension-check[o]
T182-6-14PHP extension이 전체 runtime Model API와 필수 native 보조 클래스를 C로 소유한다(T182-6-13에 이어). 인수 조건: Model 메서드, 프로퍼티, flags와 기본값이 PHP source와 일치하고, Model 및 보조 클래스가 generated arginfo를 통해 각각 한 번 등록되며, generated subclass의 late static meta() dispatch가 유지된다. focused native test가 query, read, write, rollback, Fiber isolation, GC, serialization, clone, typed key, column state와 weak null 동작을 검증한다. 커밋된 구현 24a1e7e에서 make item-test ID=T182-6-14가 PHP 8.4.26의 native test 47개(654 assertions), declaration test 3개(8 assertions), PHP source 비교 test 10개(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.jsonmake model-native-php-extension-check builds the extension and runs the focused Model and helper PHPUnit tests without a database.[o]
T182-6-15PHP extension arginfo를 temporary build copy에서 offline 생성하고 구현된 native Stats class를 선언 inventory에 반영한다(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, Makefilemake native-arginfo-php-extension-check; make dbspec-php-extension-check/declarations[o]
T182-6-16PHP extension은 native read의 LIMIT와 OFFSET 값을 MySQL에서 PHP client처럼 정수로 bind한다 (T182-6에 이어). 원인: CoverageModelQueriesExtensionTest가 MySQL에서 limit(0, 3)에 대해 ''0', '3'' 근처 SQLSTATE[42000] 1064로 실패하고, CoverageConformanceVerificationExtensionTest의 MySQL conformance child도 같은 오류로 끝나므로 native read가 limit을 따옴표 친 문자열로 전달한다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-17relations 및 명시적 root-page conformance vector가 Go, PHP, PHP extension, Rust, TypeScript, Python의 MySQL, PostgreSQL, SQLite에서 일치한다 (T182-6에 이어). 원인: 기록된 explicit root-page SQL 및 bind 기대값은 T236이 offset_p와 count_p를 bind하기 전에 만들어졌다.tests/conformance/vectors.json, tests/conformance/vectors.postgres.json, tests/conformance/vectors.sqlite.json선택 기록 및 재검증: tests/conformance/check/main.mts run에서 conditions_connectors, conditions_group, conditions_leading_group, conditions_leading_prefix, terminal_by, terminal_reuse, expression_forms, joins, relations, aggregates, empty_reads, get_query, relations_flatten을 선택하고 MySQL, PostgreSQL, SQLite에서 각각 두 번 실행했다. 13개 vector 모두 각 database에서 여섯 client 간 일치했다. 근거: var/parallel/resume-landing/relations-root-page-conformance-record.log (SHA-256 3faeba3aaa6d32cab85a743eba767240ef3fcb0cd1dc46dce4a12c87aee351ff). 이를 감싼 명령은 별도 PHP-extension interface-symbol snapshot 검사에서 나중에 exit 2였으며, 그 결과는 선택 conformance 결과에 포함하지 않는다.[o]
T182-6-18PHP extension의 OrmException::fromDriver는 Zend heap을 손상시키지 않는다 (T182-6에 이어). 원인: errorInfo에 대한 zend_read_property의 rv zval을 해제했는데, 선언된 property이면 read가 rv를 쓰지 않으므로 -O2 build가 초기화되지 않은 stack zval을 해제했다. model_writes, statement_events, audit_triggers, request_hook의 php-extension owner가 MySQL, PostgreSQL, SQLite에서 zend_mm_heap corrupted로 끝났다packages/php-ext/src/orm_exception.c, packages/php-ext/tests/OrmExceptionNativeTest.phpmake native-api-equivalence-check NATIVE_API_EQUIVALENCE_FILTER=testDriverFactoryLeavesTheHeapIntactAcrossRepeatedDriverErrors[o]
T182-6-18-1PHP extension의 Registry::model은 등록된 model class가 없는 manifest hash에서도 Zend heap을 손상시키지 않는다 (T182-6-18에 이어). 원인: hash에 등록된 class가 없으면 registry_native.c가 args[2]를 쓰지 않고 loadSet으로 model을 만든 뒤 args[0..3]을 해제하므로 초기화되지 않은 stack zval을 해제한다. contracts/fixtures의 fixture로 Registry::generated 다음 Registry::model을 부르면 process가 SIGBUS로 끝난다packages/php-ext/src/registry_native.c, packages/php-ext/tests/RegistrySchemaNativeTest.phpmake model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testModelOfGeneratedTextKeepsTheHeapIntact[o]
T182-6-19PHP extension test의 extension run은 packages/php/src에서 PHP library의 class를 하나도 load하지 않는다 (T182-6에 이어). 원인: php-ext target이 kit run-tests의 --without-library packages/php 없이 PHPUnit을 실행하므로 root Composer autoloader가 Polyspec\Orm\을 packages/php/src로 mapping하고, native model run이 Assemble, Config, DbState, Dbspec\Meet, Dialect, Engine, Generator, OrmGen, ParamCheck, TxFrame을 userland class로 load한다. S10: "A PHP extension implements every PHP library of its repository and registers their classes and functions under the same names and signatures". 이 item은 --without-library를 켜며 하위 item -1에서 -5 다음에 land한다Makefile (PHP_EXTENSION_TESTS and the php-ext PHPUnit targets), packages/php-ext/testsEvidence: main 8c28c87b3에서 PHP 8.4.26 make model-native-php-extension-check 통과: native 97 tests / 1,348 assertions, declaration 3 tests / 8 assertions, PHP library 5 tests / 48 assertions이며 --without-library packages/php guard도 통과했다. fake Flowmark protocol 경로를 검증했으며 실제 Flowmark server나 MySQL/PostgreSQL/SQLite 3 DB feature matrix는 실행하지 않았다.[o]
T182-6-19-1PHP extension은 Engine(plan cache)과 Assemble을 같은 이름과 signature로 C에 구현한다 (T182-6-19에 이어). 원인: db.c가 Polyspec\Orm\Engine::for, Engine::plan, Assemble::index를 명칭으로 부르며, 이는 packages/php/src/Engine.php와 Assemble.php로 풀린다packages/php-ext/src (engine and assemble), packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/testsmake model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testEngineAndAssembleAreNative[o]
T182-6-19-2PHP extension은 Config, Dialect, ParamCheck, TxFrame, DbState를 같은 이름과 signature로 C에 구현한다 (T182-6-19에 이어). 원인: native model run이 이들을 packages/php/src에서 load한다packages/php-ext/src, packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/testsmake model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testConfigDialectAndFramesAreNative[o]
T182-6-19-3PHP extension은 Dbspec\Meet을 같은 이름과 signature로 C에 구현한다 (T182-6-19에 이어). 원인: native model run이 packages/php/src/Dbspec/Meet.php에서 이를 load한다packages/php-ext/src, packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/testsmake model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testDbspecMeetIsNative[o]
T182-6-19-4orm-gen(bin/orm-gen과 이것만 쓰는 OrmGen, Generator, Scan, ScanVisitor, ChainDeclare, UsageError)은 library를 require하는 자체 Composer package이며, extension test harness는 Generator를 부르는 대신 이 CLI가 미리 생성한 model을 쓴다 (T182-6-19에 이어). 판정 (coordinator, 2026-10-25), 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". CLI 밖의 library API는 Generator를 부르지 않으므로 extension은 이 class들을 제공하지 않는다packages/php (bin/orm-gen and its classes moved to their own package), composer.json, config/release.json, packages/php-ext/tests, packages/php/testsmake model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testGenerationToolingIsNotLoaded[o]
T182-6-19-5PHP extension은 stub에 없는 나머지 public library class인 Bytes, Transform, Decimal, Go, GoMap, Subscription, Notification을 구현한다 (T182-6-19에 이어). 원인: 이들은 packages/php/src에서 @internal이 아니며 polyspec_orm.stub.php는 이 중 아무것도 선언하지 않는다. db.c는 Bytes를 명칭으로 쓴다packages/php-ext/src, packages/php-ext/src/polyspec_orm.stub.php, packages/php-ext/testsmake model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testPublicValueClassesAreNative[o]
T182-6-20main에서 make model-native-php-extension-check가 통과한다(T182-6 후속). 원인: 4b8db7551에서 88 passed, 1 failed, 1 timed out으로 실패한다. ModelWriteSurfaceTest::testNativeMarkdownWriteReturnsFirstKeyOrNullWhenNoKeysWereAnswered가 MarkdownModel\connect(packages/php/markdownmodel/schema_declarations.php:107)에서 "CONFIG: a markdown DSN selects the markdown IR executor and has no PDO DSN; read it with parseMarkdownDsn"를 던지고, PHPUnit process가 마지막 case RegistrySchemaNativeTest::testPerManifestRuntimeCacheAndInvalidationAfterRegister 뒤 30초 안에 끝나지 않는다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.phpmake model-native-php-extension-check MODEL_NATIVE_TEST_FILTER=testNativeMarkdownWriteReturnsFirstKeyOrNullWhenNoKeysWereAnswered[o]
T183MySQL migration fixture의 setup이 실패하면 임시 root를 지운다. 원인: scripts/test-servers-mysql.test.mts의 fixture()는 /tmp/orm-mig-XXXX를 만들고, script를 복사하고, 그 text를 검사하고, mysql-start를 실행한 뒤에야 close()를 돌려주었다. 그래서 이 단계에서 실패하면 root와 시작한 mysqld가 남았고, /private/tmp에서 그런 folder 아홉 개(약 1.7 GB)가 발견되었다. 완료 조건: setup이 실패하면 fixture가 자기 close()를 실행하고(서버를 멈추고 socket directory와 root를 지움) 원래 error를 다시 throw한다.—Evidence: case "the MySQL fixture removes its root when its setup fails"가 복사한 mysql-start를 exit 3으로 끝나게 한다. RED: 수정 전에는 existsSync(root)에서 true !== false로 실패했다. 수정 후 make test-servers-check가 15개 모두 통과하고 /tmp/orm-mig-* folder가 남지 않는다.[o]
T184kit 0.0.53을 받는다. 원인: 다른 polyspec repository가 모두 kit 0.0.53으로 옮겼다(kit K131). 0.0.53은 K188(knip hint 없음), K189(review의 allow-remote), K190과 K195(sibling wheel), K191(owner-check가 import로 test를 고름), K192(추적하는 mise sidecar), K193(PHP를 쓰면 .php-version, orm은 이미 있음)을 가져온다. 완료 조건: package.json과 package-lock.json이 공개된 integrity로 0.0.53을 받고, make kit-sync가 copy를 쓰며, 검사가 통과한다.—Evidence: lock이 sha512-b5N/8uNp…EQ==로 v0.0.53을 받는다. kit-sync가 scripts/kit/kit-bootstrap.mk, .github/workflows/release.yml, config/layout.kit.yml을 썼다. dependency-review RECORD=1이 registry dependency 65개와 lock 10개를 기록했다. kit-check, dependency-policy-check, release-coverage, toolchain-check, layout-check, interface-check, go-model-check, go-vet-check, Go engine test, lint-python, lint-ts, client-unit-check, rust-fmt-check가 통과한다.[o]
T185coverage process의 기한을 선언할 수 있는 가장 긴 test 등급으로 둔다 (T184에 이어). 원인: scripts/features/coverage.mts가 coverage run의 모든 native test process에 DATABASE 등급(120000 ms)을 주었는데, test는 자기가 선언한 등급으로 자기 작업의 기한을 지킨다. Go model_generation case는 testcase.Process(300000 ms)를 선언하고 cold build에서 orm-gen을 실행하므로, ci run 38715909724가 그 case를 자기 기한 안인 120000 ms에서 멈췄다. 완료 조건: 바깥 기한 NATIVE_DEADLINE이 PROCESS보다 짧지 않고, 짧으면 실패하는 case가 있다.—Evidence: NATIVE_DEADLINE이 DATABASE일 때 scripts/features/coverage.test.mts의 새 case가 "NATIVE_DEADLINE 120000 ms is shorter than PROCESS 300000 ms"로 실패했고, PROCESS로 바꾸자 통과한다. make feature-unit-check/coverage가 통과한다.[o]
T186개발 규칙에 0.0.x 개발의 local link 문서를 적는다. 원인: kit K198이 polyspec 버전이 0.0.x인 동안 개발 checkout이 설치한 polyspec package를 source에 link한다는 것을 kit docs/plans/local-links.md에 기록했고, AGENTS.md는 그 문서를 적지 않았다. 완료 조건: AGENTS.md와 AGENTS.ko.md가 shared tool 규칙에 그 문서를 한 문장으로 적고, make documents-check가 통과한다.—Evidence: AGENTS.md와 AGENTS.ko.md가 0.0.x local link에 대해 kit docs/plans/local-links.md로 안내한다. lock으로 설치하고 link하지 않은 worktree에서 documents 검사, docs rule, repository와 features 검사, owners, layout, go-model, interface, go-vet, Go engine test가 통과한다.[o]
T187make typescript-build이 필요한 TypeScript workspace package를 모두 build해서, 새 checkout에서 make lint-ts가 통과한다. 원인: protocol 결함이 아니라 build 결함이다. typescript:build(package.json)가 packages/ts만 build하므로, @polyspec/dbspec-language를 import하는 packages/dbspec-lsp가 나중의 workspace build 전에는 그것을 해석하지 못하고, 새 설치에서 make lint-ts가 exit 2로 실패한다(T53 worktree에서 make install-packages와 make typescript-build 뒤에 재현). 완료 조건: install-packages와 typescript-build만 한 뒤 make lint-ts가 exit 0이고, typescript:build script가 dbspec-lsp보다 먼저 dbspec-language를 build하며, 빠진 의존 build를 먼저 잡는 검사가 있다(RED 먼저).—Evidence: commit 3297593b(main, 2310bc71에서): RED: typescript-build만 한 뒤 make lint-ts가 exit 2(잘못된 type의 @polyspec/dbspec-language import에서 oxlint 300건, project reference 뒤에는 tests/interfaces/typescript.mts에서 157건). repo check는 감싸지 않은 typescript:build 줄에서 exit 1. GREEN: make typescript-build가 exit 0이고, install 선행 조건을 둔 make lint-ts가 exit 0이다. tsBuildInfoFile이 무시되는 dist에 있어 git status에 .tsbuildinfo가 없다. main에서 documents-check, docs:rules-check, repo check가 exit 0이다.[o]
T187-1lint-ts가 읽는 workspace package를 먼저 build한다 (T187에 이어). 원인: lint-ts target이 typescript-build에 의존하지 않아, 새 설치에서 build되지 않은 @polyspec/dbspec-language를 읽고 error 타입 문제를 수천 건 보고한다(새 worktree에서 발견).—근거: RED는 build 없이 설치한 새 설치에서 make -s lint-ts가 exit 2이고 error 타입 finding 2376건을 보고하는 것(packages/dbspec-lsp/src/results.ts). GREEN은 Makefile에 lint-ts: typescript-build를 둔 뒤 같은 설치에서 make -s lint-ts가 exit 0인 것이다.[o]
T188make check과 CI가 PHP extension의 dbspec suite를 실행한다. 원인: 연결 결함이다. packages/php-ext의 sets와 schema-definition suite를 실행하는 엄브렐라 target dbspec-php-extension-check가 CHECK_TARGETS에도, 어느 CI check group에도 없어서 make check와 CI가 그 suite에 도달하지 않는다(T111에서 발견). RED: make -n check와 CI workflow의 check group에 그 target이 없다(또는 kit의 모든 bound suite가 check에 도달하는지 검사가 이를 보고한다면 그 검사). 완료 조건: target이 CHECK_TARGETS와 다른 dbspec suite가 도는 CI group에 있고, make -n check가 그것을 보여준다. 연결 전에 RED를 기록한다.—Evidence: commit 511fd905(main, rebase 전 fix 68b05434): dbspec-php-extension-check가 CHECK_TARGETS와 dbspec CI group에 들어간다. RED(unlinked, 기록됨): make -n check-run에 일치하는 줄이 없고 group에 target이 없다. GREEN: make -n check-run이 그것을 한 번 보여준다. Linked: make -s dbspec-php-extension-check가 exit 0이다(sets와 schema-definition suite 통과). dbspec group에 build와 test 시간이 약 1분 늘어난다. group 시간 예산 점검은 owner의 질문이다.[o]
T189생성된 model setter가 모든 언어에서 date, time, datetime에 text 형태를 받는다. 원인: T89-3이 bind 층에서 native 시간 값을 거부하지만, 생성된 setter의 매개변수는 여전히 native 값으로 타입이 정해져 있다(PHP DateTimeInterface, Python str | datetime.date, Go 생성 코드). 그래서 호출자는 bind 시점에야 거부를 만난다. 완료 조건: 각 generator가 다섯 언어 모두에서 date, time, datetime setter에 text 타입을 내고, 하위 호환은 남기지 않는다. RED 먼저: 생성된 setter의 타입이 native 값을 받는다(native 인자를 타입 검사하는 검사가 수정 전에 실패해야 한다). bind vector는 그대로 같다.—Evidence: main의 commit a5b2edc9(0a1c643c 위로 rebase하기 전 9fc03812): 생성된 setter가 Go, PHP, Python, TypeScript, Rust에서 date, time, datetime에 text를 받는다. RED(linked): Go TestGeneratedFieldTypes(SetDay/SetStamp가 time.Time을 받음), Python test_model_fields(str | datetime.date), PHP ModelMethodsTest::test_a_time_setter_takes_the_text_form(DateTimeInterface), TypeScript typecheck(TS2578 쓰이지 않는 @ts-expect-error), Rust build/tests/generate.rs(set_start_dt가 text가 아님). GREEN: Go generator test, Python 1325 통과, PHP model method test 통과, TypeScript typecheck exit 0, Rust build test 24+3 통과. Go에서 오류는 기존 failErr 장치를 재사용한 CODEC_ENCODE로 열 이름을 밝히며, time_text_test.go가 sqlite, MySQL, PostgreSQL에서 Create, Update, GetQuery가 그 오류를 돌려주는 것을 증명한다. rebase 후 main에서 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, bind-check가 exit 0이다.[o]
T190Rust ir-answers 검사가 main에서 컴파일된다: ir-answers-check/rust target이 packages/rust/tests/tests/markdown_conformance.rs:374에서 E0308으로 실패한다. creates는 Vec<Tasks>를 받는데 호출은 Vec<Result>를 넘긴다. RED: make -s ir-answers-check/rust가 E0308으로 exit 2다. 완료 조건: 호출이 creates 시그니처와 맞고, test가 컴파일되며, Rust에서 ir-answers가 통과한다.—Evidence: commit(main, git log 참조): packages/rust/tests/tests/markdown_conformance.rs가 Result를 돌려주는 setter로 Tasks 행을 만들고 Vec<Result<Tasks>>를 creates에 넘겼다. RED: make -s ir-answers-check/rust가 374행에서 E0308(mismatched types)로 exit 2다. GREEN: 행의 마지막 setter에 ?를 붙여 creates가 Vec<Tasks>를 받고, make -s ir-answers-check/rust가 exit 0이다. rust-fmt-check와 나머지 사전 검사가 통과한다.[o]
T191schema_definition의 manifest hash가 main에서 fixture와 맞는다. 원인: packages/go/model/orm.go의 Go 상수 ManifestHash는 sha256:a26e0390c07bd97f6907d6d4588636778e3073086eceb3cd7bcf7e693f875e30을 주고, contracts/fixtures/schema_definition.json은 sha256:6b243f5910037ab61aca570f15bcb9c94461a80690ec6ed9546fecaf51e5af29을 기대한다. feature check의 schema_definition 실행(go, php, python, typescript, rust)이 같은 불일치로 실패한다(T118-5-1 기준선에서 발견, 깨끗한 main 3417edde에서 재현). 수용: 모든 구현에서 하나의 manifest hash가 상수와 fixture에서 맞고, owner 실행이 통과한다. red: node tests/go-test.mts -v -timeout 0 -count=1 -tags featurecoverage ./packages/go/orm -run '^TestCoverageDbspecManifestHash$'가 exit 1이고 packages/go/orm/coverage_schema_definition_test.go:87에서 manifest hash = sha256:a26e0390…, want sha256:6b243f59…를 보고한다. green: 같은 명령이 exit 0이다.—근거: commit 2ed87e31(main; rebase 전 8a432915): fixture가 낡은 쪽이었다. bench.dbs는 a26e0390c07bd97f6907d6d4588636778e3073086eceb3cd7bcf7e693f875e30으로 해시되고(shasum -a 256 contracts/fixtures/bench/bench.dbs), Go 상수 ManifestText도 같은 값으로 해시되며, d788f2cd(T117)가 상수, 생성 model, 다른 구현들을 이 값으로 옮기면서 fixture는 갱신하지 않았다. red: node tests/go-test.mts -count=1 -tags featurecoverage ./packages/go/orm -run '^TestCoverageDbspecManifestHash$'가 exit 1이며 manifest hash = sha256:a26e…, want sha256:6b24…를 보고한다. green: 같은 명령이 exit 0이다(ok github.com/polyspec/orm/packages/go/orm). 수정 후 go, php, python, typescript, rust의 schema_definition owner 실행이 통과한다(worktree의 feature-coverage 실행 기준).[o]
T192coverage checker의 자체 test가 main에서 통과한다. 원인: scripts/features/coverage.test.mts의 test 세 개가 깨끗한 main 8269f6f4에서 실패한다. 이는 T118-5-1 변경과 무관하다. selecting coverage parts requires only those parts(test 안의 수 15가 manifest의 12와 다르다, 낡은 기대값), coverage runs without cargo proceed while the Rust test build runs(Rust build 30초 시간 초과), Go, PHP, and Rust native cases execute through their owning files(Rust case)이며, 각각 tests/testcase.mts:231에서 … failed로 보고된다(T118-5-1에서 발견). 수용: main에서 node --test scripts/features/coverage.test.mts의 모든 test가 통과한다. red: var/tools/bin/node --test scripts/features/coverage.test.mts가 위 세 test 실패로 exit 1이다. green: exit 0이다.—근거: commit 8b8e6b84(main; fix d32e69d9를 rebase): 실패한 test 세 개는 test 쪽이 틀렸다. selecting coverage parts requires only those parts는 owner 4개를 기대했지만 statement_events는 owner 5개 곱하기 database 3개(15)이므로 개수를 5 곱하기 database로 고쳤다. Rust와 PHP case는 make가 주는 LEASE와 CARGO_LEASES 없이 돌아서, test가 전달용 stub으로 그 값을 준다. RED: var/tools/bin/node --test scripts/features/coverage.test.mts가 18 통과, 3 실패, exit 1. GREEN: 21 통과, 0 실패, exit 0(worktree에서 make install-tools와 make install-packages 후). coverage.mts는 바뀌지 않았다.[o]
T193추적되는 모든 Rust 파일이 행위 test를 선택하거나 범위를 선언한다. 원인: make -s feature-unit-check/owners가 main 93b12379와 T118-3 branch에서 실패한다. packages/rust/tests/tests/integration/main.rs가 추적되지만 feature unit이 선택하지도 않고 범위도 선언되지 않았다(T118-2 land에서 발견, main에서 재현). 수용: main에서 owners 검사가 통과한다. red: make -s feature-unit-check/owners가 exit 2이며 actual 목록에 그 경로가 있다. green: exit 0이다.—근거: contracts/check-inputs.json이 packages/rust/tests/tests/integration/main.rs를 suite 경로로 선언한다(모델을 생성하고 통합 모듈을 선언하는 crate root). 빨강: make -s feature-unit-check/owners가 그 경로를 actual 목록에 넣고 exit 2. 초록: exit 0.[o]
T194make -s rust-fmt-check가 main에서 통과한다. 원인: packages/rust/tests/tests/integration/main.rs의 mod 줄 순서가 rustfmt 순서와 다르다(T118-3 land에서 발견, main 02d36df9에서 재현). T118-2가 이 파일을 추가했다. 수용: Rust workspace의 cargo fmt --check가 main에서 통과한다. red: make -s rust-fmt-check가 exit 2이며 출력은 "Diff in packages/rust/tests/tests/integration/main.rs:4"(mod result_chains; 줄이 mod coverage_transactions; 뒤로 가야 한다)다. 등록 시점의 정확한 출력은 위와 같고, 이 파일의 mod 재정렬만 해당한다. green: exit 0이다.—근거: commit 54f850ae(main; fix 9cd4f1e4를 rebase): integration의 mod result_chains; 줄을 rustfmt 순서대로 mod coverage_transactions; 뒤로 옮겼다. RED: make -s rust-fmt-check가 exit 2이며 Diff in packages/rust/tests/tests/integration/main.rs:4와 :13을 보고한다. GREEN: exit 0이다(rust, rust/bench, rust/interfaces 통과). 순서만 바꾸므로 컴파일에 영향이 없고, documents-check와 docs:rules-check가 통과한다.[o]
T195확장의 DbspecApplyTest가 PHP client 사본과 같다. 원인: packages/php-ext/tests/DbspecApplyTest.php가 헤더 뒤에서 packages/php/tests/DbspecApplyTest.php와 다르다(거기서는 apply_dsns가 getenv를 읽고, PHP client는 requiredDsn을 쓴다). 그래서 DeclarationsTest::test_mirrors가 실패한다(T118-6에서 발견). 수용: 두 파일의 test 본문이 같고 DeclarationsTest::test_mirrors가 통과한다. red: packages/php-ext/tests/DeclarationsTest.php의 PHPUnit 실행이 test_mirrors에서 실패한다. green: 통과한다.—근거: commit fd094074(main; 확장의 apply test 사본을 PHP client 본문과 맞추고 bootstrap에 requiredDsn을 둠): DeclarationsTest::test_mirrors가 red에서 'Failures: 1', green에서 'OK (1 test, 15 assertions)'다. DeclarationsTest.php 전체는 'OK (5 tests, 27 assertions)'다. DbspecApplyTest는 server가 필요해서 최종 배치에서 검증한다. documents-check, docs:rules-check, layout-check가 통과한다.[o]
T196set의 external 경로는 디렉터리 symbolic link를 거칠 수 있다. 설치된 위치가 checkout으로 링크되듯이. 원인: docs/spec/dbspec.md step 3은 경로의 검사되는 prefix에 symbolic link를 금지하고(표의 set.symlink), 그래서 링크된 checkout을 external 문서의 위치로 쓸 수 없다. 그러나 링크 전제(개발 중 설치 위치가 checkout으로 링크된다)는 이를 요구한다. 수용: set 파일, root, 모든 document 파일은 여전히 링크가 아니다. 디렉터리 symbolic link는 root 아래 external 경로의 중간 component로만 허용된다. 마지막 component가 링크이면 여전히 거부한다. identity와 중복 규칙(step 4)은 열린 파일에 대해 판정한다. 사양 step 3과 6, set.symlink 표 문장을 EN과 KO에서 바꾼다. 공유 vector tests/dbspec/set-files.json에 허용 case(링크된 디렉터리를 거치는 external)를 더하고 거부 case는 모두 둔다. 다섯 set reader(Go, PHP, TypeScript, Python, Rust)와, set을 읽는다면 PHP 확장을 맞춘다. red: 새 vector가 모든 reader에서 실패한다. green: 각 reader의 자체 단위 test에서 set-files vector가 통과한다.—근거: commit bb93eac4(main; fix d1d9c3d0를 rebase하고 KO 원본 스탬프를 고쳐 amend): 디렉터리 symbolic link는 root 아래 external 경로의 중간 component로만 따라간다. set 파일, root, document 파일, 마지막 component, documents prefix는 여전히 거부한다. vector tests/dbspec/set-files.json에 허용 case external-through-linked-directory를 더했다. red 후 green이며 각 reader의 자체 set-files 단위 test가 통과한다: Go TestReadSetVectors ok, PHP DbspecSetTest 72 OK, PHP 확장 75 OK, TypeScript dbspec-set 94/94, dbspec-language resolve-set 55/55, Python 62 OK, Rust dbspec_set 62 passed. 선언 검사에서 R10, R6, R4가 0이다. documents-check, docs:rules-check, layout-check가 통과한다. set.c와 set_read/set_file의 Windows branch는 여기서 컴파일하지 않았다.[o]
T198언어 집합 변경을 소유한 feature check 세 개가 main에서 통과한다. 원인: 37a095d2 기준 main(T77-10-1 branch에서도 같음)에서 make -s feature-unit-check/coverage가 exit 2이며 not ok - sample/owner/php/none run 1 줄 여섯 개를 출력하고, make -s feature-unit-check/docs가 exit 2이며 features: generated document is stale: docs/features.md를 출력하고, node scripts/features/check.mts가 exit 1이며 … belongs to no feature 오류 열두 개를 출력한다. 수용: 세 검사 각각이 main에서 exit 0이다. red: 위의 세 출력. green: 각각 exit 0이다.—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-1T77-9-1이 옮긴 Go dialect 물리 test 파일 열 개(packages/go/internal/dialects/*_physical_test.go와 probe_test.go)가 feature에 묶인다(T198에 이어). 원인: main에서 node scripts/features/check.mts가 열 파일 각각에 features: packages/go/internal/dialects/<file>: go test file belongs to no feature를 보고한다. 수용: 각 파일이 feature의 owner test로 선택되고 정적 검사가 exit 0이다. red: 위의 열 메시지. green: exit 0이다.—근거: 열 파일이 feature schema_definition의 tests에 들어간다(contracts/features.json). RED: node scripts/features/check.mts가 exit 1이며 열 개의 belongs to no feature 줄을 출력한다. GREEN: exit 0이며 features: 19 feature contracts passed를 출력한다. owner 선택의 unit test는 T198-2 이후 통과한다. make documents-check가 exit 0이다.[o]
T198-2모든 추적 파일이 행동 test를 고르거나 scope를 선언한다(T198-1에 이어). 원인: main에서 node --test scripts/features/owners.test.mts가 every tracked file selects a behaviour test or declares its scope를 실패하며, 어떤 verification 명령, coverage 부분, helper, make target도 고르지 않는 열 개 file을 적는다: 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와 tests/collection/keys.json. 수용: 그 test가 통과하고 node scripts/features/check.mts가 exit 0이다. red: 이 열 개 file을 적는 실패, exit 1. green: exit 0이다.—근거: 각 file은 그것을 실행하는 verification 명령의 입력이다: model_queries의 collection-identity-go, -php, -rust, -typescript(tests/collection/keys.json은 각 명령의 입력), planner의 plan-cache-go, -php, -typescript, schema_definition의 dialects-probe-go(paths.go는 probe_test.go가 쓴다). Go coverage gate는 performance_gate의 go owner이며 parity unit go-internal-coverage-performance-gate-hot-path-gate에 묶인다. RED: node --test scripts/features/owners.test.mts scripts/features/manifest.test.mts가 exit 1이며 22 pass와 1 fail로 열 file을 적는다. GREEN: exit 0이며 23 pass와 0 fail이다. node scripts/features/check.mts가 exit 0이다(feature contract 19). make feature-docs 후 make -s feature-unit-check/docs가 exit 0이다. 새 명령은 여기서 실행하지 않았다.[o]
T198-3main에서 make -s feature-unit-check/docs가 통과한다. 원인: docs/features.md와 docs/features.ko.md가 contracts/features.json에서 다시 생성되지 않았다. 수용: make -s feature-unit-check/docs가 exit 0이다. red: features: generated document is stale: docs/features.md와 함께 exit 2. green: exit 0이다.—근거: make feature-docs가 docs/features.md(한 줄)와 docs/features.ko.md(두 줄)를 다시 생성했다. RED: make -s feature-unit-check/docs가 features: generated document is stale: docs/features.md와 make: *** [feature-unit-check/docs] Error 1을 내며 exit 2다. GREEN: exit 0이다. documents-check가 exit 0이다(문서 쌍 44, checklist 1, changelog 1).[o]
T156모든 생성기가 모든 key의 모든 prefix마다 유형 있는 메서드를 선언한다. 원인: scan한 소스가 호출하는 메서드만 생성되어, 어떤 소스도 호출하지 않는 key prefix에는 메서드가 없었다(hyper U-97). 완료 조건: Go, Rust, TypeScript model이 primary key, unique key, index의 모든 prefix마다 메서드를 선언하고, Python은 같은 이름을 실행 시점에 해석하며, symbol 기록과 docs/usage.md를 갱신한다.——[o]
T156-1T156 이후 orm main CI를 다시 green으로 만든다 (T156에 이어). 원인: run 38502539388이 실패했다. Python key-prefix 테스트가 PYTHONPATH를 바꾸고 지워진 packages/orm-python/bin/orm-gen을 실행했고, Go와 Rust key-prefix 테스트가 testcase를 시작하지 않았으며, generator/gen_go_key_prefix_test.go가 어떤 feature에도 속하지 않았고, src/models/markdown_fixture에 key-prefix 메서드가 없었다. 완료 조건: 각각을 고치고 그 소유 검사가 통과한다.——[o]
T156-2Go markdown model을 다시 생성한다 (T156에 이어). 원인: run 38504701356이 go-markdown-model-generated에서 실패했다. packages/orm-go/markdownmodel에 key-prefix 메서드가 없었다. 완료 조건: go generate가 폴더를 바꾸지 않는다.——[o]
T69Release 0.0.9. 원인: T61, T62와 markdown IR executor인 T62-1, T63부터 T67까지는 polyspec/flowmark가 요구하는 protocol section, markdown 저장 setting과 executor를 추가하며, flowmark는 pseudo-version 대신 released tag를 요구한다. 완료 기준: 0.0.9의 release 절차를 이전 release 항목(T52, T56)과 같이 밟고 tag로 끝난다. T62-1, T63, T64, T65, T66, T67이 필요하다.—Evidence: annotated tag v0.0.9가 ddcdca4f에 있고 GitHub release v0.0.9가 asset 4개를 가진다. flowmark는 그때부터 release tag를 받는다(지금은 v0.0.18에 고정). 이 항목은 release된 뒤 닫히지 않았다.[o]
T77-58parity-check 단계가 자기 실행 디렉터리를 남긴다: PARITY_RUN_DIR(.runtime/run/parity-check-$PPID)은 parity-prepare가 만들고 어떤 단계도 지우지 않으며, 종료 시 지울 수 있는 주체는 kit의 parity.mjs뿐이다. 원인: step.mts:166-168이 TMPDIR에 남은 항목을 보고하고 지우지 않으므로 TMPDIR 아래에는 둘 수 없다. 완료 기준: 깨끗한 parity-check 실행이 남은 디렉터리를 보고하지 않는다. 빨강: 단계가 left .runtime/run/parity-check-NNN을 보고. 초록: 보고하지 않는다. kit 행 필요.—증거: 깨끗한 실행 scratchpad/g4-parity.log(바인딩 84개 두 번 실행, 케이스 85개 통과, 모든 unit이 모든 구현에서 성립); 같은 단계 parity-check PASS이고 남은 디렉터리 없음. 수정 전에는 같은 단계가 left .runtime/run/parity-check-86307로 실패했다.[o]
T200restoreOrCreate: soft-delete된 행을 키로 복원하거나 생성한다 (consumer S-286-19-1). 원인: soft delete는 행의 키 값을 남기므로 같은 키의 새 create는 DUPLICATE_KEY로 실패하고, 앱이 키를 찾아 복원하거나 생성해야 한다. 기존 upsert(duplication()->create())는 충돌 키를 정확히 하나 받고, 살아 있는 행을 덮어쓰며, soft-delete된 행은 지운 채로 둔다. 완료 기준: docs/operations/usage.md(+ko)가 다섯 client(php, ts, python, go, rust)의 호출을 명세한다. soft_delete가 있는 table에서 한 transaction 안에, restore()가 고르는 방식으로 고른 키를 가진 soft-delete된 행은 set 값으로 복원되고 audit도 함께 쓰여 돌려지며, 그 키를 가진 행이 없으면 set 값으로 생성되어 돌려지고, 살아 있는 행이 그 키를 가지면 새 catalog code ROW_NOT_DELETED로 실패하고 아무것도 쓰지 않는다. identity가 아닌 unique key를 가진 bench table 위의 conformance vector restore_or_create가 MySQL, PostgreSQL, SQLite에서 세 경우의 statement와 결과를 고정한다. 구현 전 red, 구현 뒤 green. 호출의 interface rule, symbol, parity unit. 결정(사용자, 2026-10-18): 지금 만든다.—근거: 구현을 stash한 red(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. 실행이 Rust build에서 멈추므로 TS와 PHP에는 닿지 않았다. green(var/t200-green.log, make run-databases TARGETS=conformance-check VECTOR=restore_or_create): MySQL, PostgreSQL, SQLite에서 go, php, typescript, python, rust가 ok이고, 모든 vector를 실행하는 Python case가 세 데이터베이스 모두에서 통과한다. SQLite에서 그 vector만의 실행은 전체 실행이라면 앞 vector가 보내는 연결의 첫 orm__row_lock table statement도 보내며, 그 기대값은 그 statement를 뺀다(tests/conformance/README.md). 담당 검사: interface-check, parity-declarations-check, feature-unit-check, python-model-check, Go generator test, Rust build test(database case 하나는 DSN이 필요하다).[o]
T200-1bench set을 고정한 test가 T200의 새 table을 따른다. 원인: T200이 bench.dbs에 soft_link를 더해 bench의 entity 목록, 개수, manifest hash가 바뀌었는데, T200은 그것을 고정한 test를 하나도 실행하지 않았다. 완료 기준: 그것을 고정한 모든 test와 fixture가 새 값을 읽는다.—근거: PHP RuntimeModelTest와 TS engine.mjs의 entity 목록에 soft_link가 더해졌고, Python test_orm_gen이 entity 20개를 집계한다. contracts/fixtures/schema_definition.json, docs/spec/protocol.md(+ko), test_orm_gen.py, Rust soft_delete.rs와 sqlite_concurrency.rs에서 hash sha256:18b7c53c…가 sha256:a26e0390…를 대신한다. bench를 읽는 test가 통과한다: Python, TS 파일, seed된 bench database가 필요 없는 PHP 파일, Go model.[o]
T201audit trigger가 transaction의 감사 대상 table을 기록한다 (consumer S-286-19). 원인: audit 기록(seq, uuid, service_seq, account_seq, service_member_seq, action, reason, created_at, request_id)은 table을 저장하지 않고, 각 history table의 audit column만 그것을 가리키므로, 화면이 table별로 audit을 거르거나 page를 나눌 수 없다. 완료 기준: 문서(+ko)가 다섯 client에서 같은 의미로, audit into trigger가 transaction이 바꾼 감사 대상 table마다 한 행(audit seq와 감사 대상 table 이름)을 audit set의 table에 함께 쓴다고 명세한다. 그것은 audit set의 일반 생성 model에서 audit의 relation으로 읽히므로, query가 ltSeq, limit, getCount로 table별로 거르고 page를 나눈다. 맞는 행이 없으면 다른 읽기와 같다(빈 결과 또는 NO_ROWS). MySQL, PostgreSQL, SQLite에서 vector가 red 뒤 green이고, trigger의 statement, schema, 모든 runtime의 생성 model이 포함된다. 결정(사용자, 2026-10-18): 지금, T200과 병렬로 만든다.—근거: audit setting의 tables <table> (<record column>, <name column>) 절을 여섯 dbspec 구현(Go, TypeScript, PHP, PHP extension, Python, Rust)에 넣었다. insert와 update trigger가 (audit, table)마다 행 하나를 쓰고, introspection이 그 절을 되살린다. vector: tests/dbspec/cases.json의 canonical과 11개 invalid audit-tables case, 동작 단계를 가진 ddl.json의 audit-tables case, introspect.json의 세 case. 각 구현 전 red, 후 green. 각 runtime의 dbspec unit check와 introspect check가 통과한다. docs/spec/dbspec.md, docs/spec/dialects.md, docs/operations/usage.md(+ko).[o]
T201-1external text가 사용 table 사이의 foreign key를 유지한다(consumer requests.dbs). 원인: external text가 각 사용 table을 column, primary key, unique key만으로 담아, manifest와 external text를 다시 parse하는 runtime이 T201의 audit tables 절을 거부했다. 그 tables table은 audit table로 가는 foreign key가 필요하다: Python은 import에서, PHP는 Database::open에서 SCHEMA_INVALID를 냈다. 완료 기준: external text가 참조 table이 같은 external document의 사용 table인 foreign key와 그 column으로 시작하는 index도 유지해, 줄인 text가 같은 검증을 통과한다. docs/spec/dbspec.md(+ko). 모든 구현에서 전 red, 후 green인 sets case.—근거: manifest와 external text를 다시 parse하기도 하는 tests/dbspec/cases.json의 sets case external-document-used-table-keys. 구현마다 그 case 하나만 대상으로 red, 메시지 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와 PHP extension Failures: 1, Python RuntimeModel.load_set에서 SCHEMA_INVALID, Rust set_vectors::external-document-used-table-keys FAILED. green: Go ok, TypeScript pass 1, PHP와 extension OK (1 test, 22 assertions), Python ok, Rust 1 passed. contracts/fixtures/external을 읽는 test가 통과한다: PHP OrmGenTest, SchemaSetTest, AuditExternalSetsTest, TypeScript generate.mjs와 schema-set.mjs, Go orm External, Rust build generate와 orm external_documents, register. bench manifest hash는 바뀌지 않는다.[o]
T204일치하는 행이 없는 select 읽기는 빈 값을 돌려주며, 어떤 select도 NO_ROWS를 내지 않는다(사용자 판정). 원인: get()은 일치하는 행이 없으면 NO_ROWS를 내고, getSum()과 getAvg()는 행이 없을 때 SQL이 주는 null에 CODEC_DECODE를 돌려주므로, 호출자가 평범한 빈 결과에 오류를 잡는다. consumer python(262곳)과 php, node의 NO_ROWS catch가 이것을 기다린다. 완료 기준, 각 값을 Go, PHP, Python, Rust, TypeScript에서 red 뒤 green으로 실행하는 conformance vector로 고정한다: - get()과 getBy…()는 null을 돌려준다(Python None, Go nil, Rust None). get()의 NO_ROWS 경로는 지운다. - gets(), getsBy…(), getsCount()는 빈 컬렉션을 돌려준다. getsCount()는 그룹별 행이므로 빈 값이 컬렉션이다. getsPage()는 빈 items와 totalCount, totalPages 0을 돌려준다. - getCount…()는 0을 돌려준다. - getSum()은 0을 돌려준다: 빈 집합의 합은 정의상 0이고, COALESCE(SUM(x), 0)이 SQL null의 표준 관용구이며, 결과 type이 모든 언어에서 숫자로 남는다. - getAvg()는 null을 돌려준다: 빈 집합의 평균은 정의되지 않으며, 0은 실제 평균과 구분할 수 없는 지어낸 평균이다. - SQL SUM과 AVG는 null 값을 건너뛰고 남은 값이 없으면, 일치하는 행이 없든 모든 값이 null이든 null을 준다. 둘 다 위의 빈 값이다. 숫자가 아닌 값, 유한하지 않은 값, 범위를 넘는 scalar는 CODEC_DECODE로 남는다: 빈 결과가 아니라 잘못된 값이다. - restore()는 어떤 행도 갖지 않은 key에 NO_ROWS를 유지한다: 쓰기이며, 아무것도 쓰지 않은 restore가 성공처럼 보이면 안 되고, 행이 있게 만드는 호출은 restoreOrCreate()(T200)다. - local(key)는 없는 key에 NO_ROWS를 유지한다: select가 아니라 transaction 로컬 값이며, 없는 key는 호출자 오류이고, null은 저장된 빈 값과 구분되지 않는다. - docs/spec/dsl.md, docs/spec/interfaces.md, docs/operations/usage.md, README와 ko 쌍, contracts/interfaces.json, contracts/fixtures/ir_answers.json, 생성된 Get 주석, select에서 NO_ROWS를 기대한 모든 test.—근거: 구현을 뺀 red: Go와 Rust는 nullable get/getAvg에서 build가 실패하고, Python, PHP, TypeScript는 NO_ROWS: query matched no rows로 실패한다. green: vector empty_reads와 옛 get() NO_ROWS를 기대값에 가진 vector(terminal_by, write_cycle, delete_recursive)를 MySQL, PostgreSQL, SQLite에서 다섯 client가 합의한 출력으로 기록했다. restore는 NO_ROWS를 유지한다. 기록 도구가 client 사이에 다른 필드를 표현한다. 담당 검사: interface-check, Rust workspace clippy, 다섯 client의 ir-answers, 각 client의 바뀐 unit test.[o]
T204-1contract가 T204의 빈 읽기, T205의 batch, T212의 revision을 따른다. 원인: T204, T205, T212가 contract 없이 API를 바꿨다. restore(hidden)와 delete_recursive(service_left)의 state contract는 읽기가 이제 null을 주는 곳에 여전히 NO_ROWS를 기대했고, tests/interfaces/contracts/validate.go에는 get과 getAvg의 nullable output과 input 목록 Rows,Batch, ColumnRevision이 없었으며, Python test_python_natives는 이제 24개인 entity rule을 22개로 고정했다. 선택한 vector의 실행은 state contract를 검사하지도 않았다. 완료 기준: 각 contract가 API와 맞고, 선택한 vector의 실행이 그 state contract를 검사한다. 전 red, 후 green.—근거: Model.get의 output은 OptionalModel, Model.getAvg는 OptionalAggregate(validate.go, interfaces.json). validate.go가 Rows,Batch와 ColumnRevision을 선언한다. 두 state contract가 null을 기대한다. tests/interfaces/check가 --results와 함께 --vector를 받고 conformance-check가 VECTOR를 넘겨, 선택한 vector의 실행이 그 contract만 검사한다(Makefile, tests/conformance/README.md +ko). red: Go TestLogicalContractRejectsNativeDrift Model.get/php return ?static does not implement Model, Python 24 != 22 다음 KeyError: 'ColumnRevision', 옛 contract로 make run-databases TARGETS=conformance-check VECTOR="audit_lazy restore delete_recursive"가 세 dialect의 다섯 client에서 state contract restore와 state contract delete_recursive로 실패(audit_lazy는 연결의 첫 row lock vector). green: Go contract test ok, test_python_natives OK, sequence와 flag test 통과, 같은 실행이 ok - interfaces/results로 통과.[o]
T205creates(rows, batch): 호출자가 multi-row INSERT 하나의 행 수를 정한다(consumer S-286-19 상품 import, 사용자 요청). 원인: creates(rows)가 행을 자기 규칙으로 statement에 나누므로 호출자가 statement 크기를 고를 수 없다. 완료 기준, Go, PHP, Python, Rust, TypeScript에서 red 뒤 green으로 실행하는 conformance vector로: - creates(rows, batch)가 유일한 형태다: batch는 필수이며 양수이고, batch 없는 creates(rows)는 기본값도 fallback 분할도 없이 지운다. 양의 정수만 받으며, 다른 값은 어떤 statement보다 먼저 CONFIG로 실패한다. - batch 행씩의 각 묶음(마지막은 더 작을 수 있다)이 multi-row INSERT 하나이고, 모든 묶음이 한 transaction(활성 transaction 안에서는 savepoint) 안에서 실행되며, 실패하면 모든 묶음을 rollback한다. - bind 수가 dialect의 bind 한도를 넘는 묶음은 어떤 statement보다 먼저 새 오류 code BATCH_TOO_LARGE(docs/spec/errors.yaml, 모든 catalog, parity unit)로 실패하며, message가 묶음, 그 bind 수, dialect 한도를 표현한다. 나누지 않는다. - 결과는 삽입된 행 수다. - docs/spec/dsl.md, docs/spec/interfaces.md, docs/usage.md와 ko 쌍, contracts/interfaces.json, 생성된 wrapper, parity unit.—근거: merge base의 model source로 red(vector, runner, model은 유지): Go conformance runner와 Rust runner가 creates(rows, batch)에서 build에 실패한다. green: vector creates_batch와 creates_batch_too_large, 그리고 creates로 행을 넣는 contains_ascii_fold와 contains_binary_code_point를 MySQL, PostgreSQL, SQLite에서 다섯 client가 합의한 출력으로 기록했고, 모든 vector를 실행하는 Python case가 셋 모두에서 통과한다. green 실행이 Python이 연 transaction 밖에서 batch를 실행함을 찾았다(transaction null). executor를 이제 그 안에서 얻는다. SQLite에서 creates_batch만의 실행은 연결의 첫 orm__row_lock statement도 보내며, 기대값은 그것을 뺀다(tests/conformance/README.md). statement 차이가 이제 statement의 tables와 transaction을 표현한다. interface-check와 parity-declarations-check가 통과한다.[o]
T202upsert의 conflict key를 request hook 뒤에 검증한다(consumer hyper ScopeHook). 원인: request hook이 insert의 scope column을 할당하지만 duplication()->create()의 conflict key 검증이 hook 전의 request를 읽으므로, hook이 완성하는 key가 거부된다. 완료 기준: 검증 위치(Go ir.go, PHP Validator.php, Python validate.py, Rust validate.rs, TS validate.ts)가 hook이 돌려준 request를 읽는다. 언어마다 unit case, 전 red, 후 green.—증거 (2026-10-19): client마다 case upsert_scope (contracts/fixtures/request_hook.dbs의 hook_upsert table, unique (scope_col, name); hook이 insert에 scope_col을 넣고, name만 준 duplication(hits=7)->create()를 두 번 실행해 a/k/7 한 행이 남는다). RED (hook의 할당을 뺀 상태): Go, PHP (Validator.php:180), Python, Rust, TypeScript가 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 (실제 hook): 다섯 모두 통과 (var/t202-green-*.log). 모든 validator가 hook이 돌려준 request를 읽으며, 이 case가 그것을 지킨다. consumer는 더 이상 이 upsert를 쓰지 않는다 (canister는 read와 save()로 쓴다).[o]
T203relation()과 relations()가 service scope 아래에서 실행된다(consumer). 원인: service scope 아래에서 둘 다 CONFIG로 실패한다. 완료 기준: 원인을 찾아 여기에 적고, 언어마다 case, 전 red, 후 green. 재개 (2026-10-19): 재현되지 않음으로 닫았다. 다음: consumer가 ScopeHook 아래 canister relation()/relations() case를 한 번 실행해 CONFIG message, file:line, hook 본문을 기록하고, 그 case를 client마다 red 후 green으로 만든다.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-scopemake request-hook-relation-scope-check, make parity-declarations-check UNITS=coverage-request-hook-relation-scope[o]
T206orm이 행이 대기나 진행 중인 동안 make check와 parity-check를 거부하는 kit full-run guard를 쓴다(kit K233, K234). 완료 기준: orm에서 각각 한 번의 dry run이 열린 행을 말하고 거부한다.—증거 (2026-10-19): RED: var/t206-red.log, 89cf4323에서 make run-databases TARGETS=parity-check가 interface-check에서 TypeScript와 Rust interface를 빌드한 뒤(87-147줄) full-run.mjs gate가 거부한다(149줄). gate가 선행 target 뒤의 recipe 줄이었기 때문이다. 수정은 kit K236(215202a5)이다: parity-check가 UNITS 없이 goal이면 kit.mk가 makefile을 읽는 동안 gate를 실행한다. orm이 parity-prepare에 넣었던 gate 줄은 지운다. GREEN: var/t206-green.log, 이 Makefile로 make parity-check가 대기 행 23개와 kit.mk:290: *** parity-check: the gate of full-run refuses the full run으로 거부하고 아무것도 빌드하지 않는다(run-long, cargo, typescript-build, parity-bench 줄 0개). make check도 같은 행으로 거부한다(e095582f).[o]
T207database server가 그 checkout과 함께 멈춘다. 원인: 지워진 worktree의 mysqld 쌍과 같은 datadir의 이전 시작이 며칠씩 실행된다. 완료 기준: worktree를 지우거나 datadir에 server 쌍을 시작하면 먼저 그것에 속한 server를 멈춘다. 전 red, 후 green인 test. 지금 도는 소유자 없는 process는 사용자가 멈출 일이다.—근거: test server 시작은 같은 datadir에 이전 시작이 남긴 server를 .runtime/servers의 pid 파일(mysqld --pid-file, postmaster.pid, pooler의 pid 파일)로만 알아보고 먼저 멈춘 뒤 새로 시작한다. make worktree-remove WORKTREE=<path>는 git worktree remove 전에 그 worktree의 server를 멈춘다(AGENTS.md). scripts/test-servers.test.mts의 unit case는 가짜 server를 pid 파일로 멈추며 database를 시작하지 않는다. 지금 도는 소유자 없는 process는 사용자에게 남긴다.[o]
T208metadata가 manifest와 다른 generated model을 거부한다(consumer PlanBinds 경고). 원인: consumer의 PHP model이 orm-gen이 varchar length를 쓰기 전(adc4d518) 생성됐고, runtime은 그것을 거부하지 않고 PlanBinds.php:91의 PHP 경고와 null length로 읽는다. 완료 기준: runtime model을 만들 때 각 generated model의 metadata를 그 manifest가 주는 entity와 비교하고, 다르면 class, entity, 처음 다른 필드, orm-gen 명령을 말하는 CONFIG로 실패한다. 낡은 model로 red, 뒤 green. generated code가 metadata를 갖는 모든 client에서.—근거: PHP RuntimeModel::fromModels가 각 entity를 manifest에서 만들고 generated class의 meta()와 비교한다. 다르면 class, entity, 처음 다른 path(columns.name.length 등), orm-gen 명령을 말하는 CONFIG로 실패한다(RuntimeModelTest test_a_generated_model_that_its_manifest_contradicts_is_refused). Go, TypeScript, Python, Rust는 runtime model을 manifest text에서 만들고 entity를 명칭으로만 찾으므로 generated code가 그것과 어긋날 수 없다. docs/spec/dbspec.md "Runtime model"(+ko). red: 455fe6a0~1의 RuntimeModel.php로 phpunit --filter test_a_generated_model_that_its_manifest_contradicts_is_refused가 실패한다(낡은 model을 받아들인다, RuntimeModelTest.php:99). 455fe6a0에서 green: OK (1 test, 4 assertions).[o]
T209external document의 table을 읽기 전용 entity로 읽는다(consumer: set을 넘는 행의 audit). 원인: request 하나는 manifest hash 하나를 갖고 runtime model에 external table의 entity가 없어 어떤 query도 다른 set의 table을 읽지 못하며, key를 모아 IN에 넣는 것은 금지다. 완료 기준: 쓰는 set의 runtime model과 generated code가 쓰는 external table마다 select, join, subquery, relation, 조건이 되는 entity를 주고, 모든 쓰기는 CONFIG <table> is owned by another document set으로 실패한다. docs/spec/dbspec.md "External documents"(+ko). 다섯 client에서 전 red, 후 green인 conformance vector.—근거: 사용되는 external table은 쓰는 set의 entity가 되며 external 정의로만 만들어지고, 다섯 request validator에서 모든 쓰기가 CONFIG <table> is owned by another document set으로 거부된다. 생성기가 그 model과 그것으로 가는 relation getter를 선언한다. bench set에 bench_reader.dbs(소유)와 bench_shared.dbs(external, table shared_note)가 더해졌다. merge base의 구현 source로 red: Go와 Rust에서 conformance build가 실패한다. green: vector external_read(getSharedNoteModel()로 읽는 relation, set을 넘는 IN subquery, 거부되는 create)를 MySQL, PostgreSQL, SQLite에서 다섯 client가 합의한 출력으로 기록했고, 모든 vector를 실행하는 Python case가 통과한다. bench 변경은 bench를 읽는 모든 test에 반영했고 파일마다 다시 green으로 실행했다: client마다 하나인 설치 helper가 bench set 전에 bench_shared.dbs를 자기 set으로 설치하고(PHP tests/bench.php, Go internal/benchset, TypeScript tests/bench-set.mjs, Python tests/bench_set.py, Rust tests/src/bench_set.rs), introspection round trip은 각 schema document를 그것이 쓰는 document와 함께 읽고, 생성기 test는 external model을 기대하고, Rust integration program은 dbspec-set.json을 읽는다. interface-check와 parity-declarations-check가 통과한다.[o]
T209-1PHP 생성기 test가 사용되는 external table의 읽기 전용 model을 기대한다(T209에 이어). 원인: T209부터 생성기가 사용되는 external table마다 읽기 전용 model을 쓰는데, OrmGenTest test_gen_generates_only_the_owned_tables_of_a_set_with_external_documents가 여전히 소유 model만 기대해 generated files ExtAccount.php, ExtAudit.php, ExtPost.php, ExtPostHistory.php, bootstrap.php로 실패한다packages/php/tests/OrmGenTest.phpvendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/OrmGenTest.php[o]
T210native time 값을 넘기는 test가 통과한다. 원인: Go TestStatementTimeout과 TestStatementTimeoutThroughAPooler가 CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value(timezone_test.go:333, 403)로, packages/ts/tests/model.mjs의 모든 case가 Date setup(model.mjs:176-177)에서 CODEC_ENCODE: parameter 5 (object) is not a datetime value로 실패하며, T200 이전 main에서도 그렇다. native time 값은 14f1796a부터 거부된다. 완료 기준: 각 test가 정규 text 형태를 넘기고 MySQL, PostgreSQL, SQLite에서 통과한다. 증거 (2026-10-19), test server(TZ=Asia/Seoul)에 단독 실행 node tests/go-test.mts -count=1 ./packages/go/orm -run '^(TestStatementTimeout|TestStatementTimeoutThroughAPooler)$', node packages/ts/tests/model.mjs: RED (9ce82e84^의 test file, var/t210-red.log): 두 Go test가 timezone_test.go:333, :403에서 CODEC_ENCODE: parameter 0 (time.Time) is not a datetime value로, model.mjs가 parameter 5 (object) is not a datetime value로 실패한다. 9ce82e84의 첫 green은 connectionsUseUtc 9 case가 여전히 실패했다: model.mjs:522가 new Date('2026-01-02T00:00:00+09:00')을 넘겼다. 이제 canonical UTC text 2026-01-01 15:00:00을 넘기고, 어떤 client도 더 받지 않는 instant 동등 filter는 지운다. GREEN (var/t210-green.log): Go 둘 다 MySQL, PostgreSQL에서 PASS, model.mjs는 SQLite, MySQL, PostgreSQL에서 64 ok, 실패 0, connectionsUseUtc 9 case 포함.——[o]
T211TypeScript scan이 어떤 호출도 붙이지 않는 getter 호출을 거부한다(consumer: 중첩 relation alias의 getMatch). 원인: scan은 alias<Name>() 호출과 getter의 receiver를 모두 generated model로 풀 때만 get<Name>()을 선언하고, 풀지 못한 호출은 말없이 버린다. relation 안에 중첩된 relation의 alias와 getter의 generic 인자로만 type이 정해지는 receiver가 버려진다. Rust는 이미 거부한다("no call attaches", generate.rs). 완료 기준: 다섯 생성기가 중첩 relation alias를 풀고, 어떤 alias, new<Name>, addColumn도 붙이지 않는 scan된 getter 호출은 model과 method를 말하며 생성을 멈춘다. 각각 전 red, 후 green.—근거: HEAD~1의 생성기 source와 새 case로 red(생성기마다 대상 실행 한 번): Go TestGoUnattachedGetterStopsGeneration FAIL, PHP 3개 중 2개 실패, Python test_a_getter_call_that_nothing_attaches_stops_generation FAIL, Rust unattached_getter_stops_generation FAILED, TypeScript nested_alias_getter와 unattached_getter 실패. 수정 위에서 green: Go ok, PHP OK (3 tests, 9 assertions), Python OK (3 tests), Rust 3 passed, TypeScript pass 3. green 실행이 Rust scan이 Option<T>를 T로 읽어 unwrap을 model method로 거부함을 잡았다. Option binding은 이제 unwrap과 expect를 그 model로 푼다. Rust는 이미 중첩 alias를 풀었으므로 그 중첩 case는 전후 모두 통과한다.[o]
T212기대 revision을 받은 update와 delete는 행이 바뀌었으면 REVISION_CHANGED로 실패한다 (consumer S-286-19: page/board ManageViews::submit와 board comment 삭제가 각자 이 순서를 다시 만들고 make duplicates가 보고한다). 수용 기준: model이 revision column을 선언하거나 chain이 지정한다. 기대 revision을 받은 update()와 delete()는 주어진 transaction에서 행을 잠그고 revision을 비교해, 같으면 revision + 1로 update하거나 delete하고, 다르면 새 catalog code REVISION_CHANGED로 실패하며 아무것도 바꾸지 않는다. docs(+ko)와 contracts에 명시하고, conformance vector가 Go, PHP, Python, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 red 후 green으로 실행된다.—근거: expectRevision(column, revision)(Python과 Rust는 expect_revision)이 update()와 delete()의 key 조건에 revision을 더하고, update()는 그것을 1 늘린다. 어떤 행과도 맞지 않는 쓰기는 새 code REVISION_CHANGED로 실패하고 아무것도 쓰지 않는다. merge base의 구현 source로 red: Go와 Rust runner가 build에 실패하고(ExpectRevision undefined, no method named expect_revision), Python은 CONFIG: ExpectRevision is not a column of author로 실패한다. SQLite unit case(revision_test.go, RevisionTest.php, revision.mjs, test_revision.py, revision.rs)가 각 client에서 전 실패, 후 통과한다. green: vector expect_revision(author like_count 42에서 43, 두 불일치 모두 REVISION_CHANGED, 행 유지 뒤 삭제)을 MySQL, PostgreSQL, SQLite에서 다섯 client가 합의한 출력으로 기록했고, 모든 vector를 실행하는 Python case가 통과한다. SQLite에서 그 기대값은 연결의 첫 orm__row_lock statement를 뺀다(tests/conformance/README.md). interface-check, parity-declarations-check, documents-check가 통과한다.[o]
T213각 generator의 base Model method 목록은 Model 자신의 것이다 (consumer: Python orm-gen이 T200 method restore_or_create를 거부). 원인: generator마다 base method를 손으로 적은 목록을 두었고 어긋났다. Python _BASE_METHODS는 restore_or_create, alias가 없고 Model에 없는 to_json이 있어 scan된 호출이 생성을 거부했다. PHP FIXED_METHODS는 restoreOrCreate, encodeValue, jsonText가 없어 json_text column이 Model::jsonText와 충돌하는 jsonText를 만들었다. TypeScript baseMethods는 restoreOrCreate가 없었다. Rust와 Go는 이미 맞았다. 수정: Python은 Model의 public method를 읽고, PHP는 Model을 reflect(public, protected)하며, TypeScript는 Model.prototype을 읽고, Rust는 목록을 유지하되 fixed.rs.txt의 public method와 비교하는 test를 둔다. 증거 (2026-10-19), 각각 단독 실행: RED: Python test_orm_gen이 Author.restore_or_create is not a model method, Author.alias is not a model method로 실패, PHP RuntimeModelTest가 충돌하는 json_text column을 받아들임, TypeScript generate-scan base_methods_match_model이 missing ["restoreOrCreate"], Rust guard는 FIXED에서 restore를 빼면 FIXED differs from the public methods of the fixed impl로 실패 (var/scan-methods-red-.log). GREEN: Python 14 OK, PHP RuntimeModelTest 11/11, OrmGenScanTest 31/31, TypeScript generate-scan 7/7, generate 1/1, Rust fixed_method_tests와 generate test 30 통과 (var/scan-methods-green-.log). Generator::modelMethods를 기록한 뒤 make interface-check 통과 (var/scan-methods-interface.log).——[o]
T214bench 문서의 Mermaid round trip은 soft_link table의 object를 기대한다 (T200에 이어). 원인: T200이 contracts/fixtures/bench/bench.dbs에 soft_link를 넣었지만 tests/dbspec/mermaid.json의 round_trip case bench는 그 전 문서의 drop을 나열해, 모든 구현이 export dropped 22 objects, import dropped 11 objects로 실패했다. 수정: export가 버리는 soft_link의 comment, settings, unique uq_soft_link_code와 import가 버리는 unique code를 case에 넣는다. 증거 (2026-10-19), 각 round-trip test 단독 실행: RED (299f9976): Go TestMermaidRoundTripVectors/bench, Python test_dbspec_mermaid_round_trip, TypeScript dbspec-mermaid-round-trip.mjs, Rust dbspec_mermaid round_trip::bench, PHP와 PHP extension DbspecMermaidTest test_round_trip이 이 네 object로 실패 (var/mermaid-red*.log). GREEN: 여섯 모두 통과 (var/mermaid-green-*.log).——[o]
T215ClientGenericsTest의 PHPStan case가 T204 뒤에 통과한다. 원인: closure가 이제 Author|null을 반환해 ClientGenericsTest.php:100이 실패한다. T204부터 get()이 null을 반환하는데 case는 여전히 Author를 선언한다. 완료 기준: case가 nullable 결과를 type하고, 전 red, 후 green.—근거: source는 이미 nullable 읽기를 선언했다(Model::get(): ?static, packages/php/src/Model.php:1485, TModel|null을 반환하는 Collection::get). case는 fn(): Author를 선언하고 null 검사 없이 이름을 읽었다. red: calls.php:9: Anonymous function should return ...Author but returns ...Author|null [return.type], Failures: 1. case는 이제 ?Author로 type하고 $author?->getName() ?? ''로 읽는다. green: ClientGenericsTest.php만 실행해 OK (1 test, 1 assertion).[o]
T216PHP ModelTest test_insert_parts@mysql가 memory 한도 안에서 실행된다. 원인: Service object 65536개를 만들며 Model.php:403에서 128M 한도를 다 쓴다. 완료 기준: 원인(test의 크기 또는 client의 행당 memory)을 적고 case가 통과한다. 전 red, 후 green.—근거: 원인은 client가 아니라 test다. MySQL에서 case가 BATCH_TOO_LARGE에 닿으려 bind 한도 + 1 = 65536개의 서로 다른 Service model을 만들었고, 하나당 약 1846 byte(10000개에 memory_get_peak_usage 4,116,816에서 22,745,304 byte)라 creates가 실행되기 전에 PHP가 ModelTest.php:561에서 128M를 다 썼다. creates는 큰 묶음을 행 수와 column 수로 어떤 statement보다 먼저 거부하며 묶음마다 복사본을 포함하지 않는다. case는 이제 model 하나를 반복하며(array_fill) 같은 경로로 같은 거부를 증명한다. memory_limit은 그대로다. red: Allowed memory size of 134217728 bytes exhausted ... ModelTest.php on line 561. green: --filter test_insert_parts가 sqlite, mysql, postgres에서 OK (3 tests, 24 assertions), 최대 28 MB.[o]
T217Rust integration case conditions가 통과한다. 원인: integration.rs:185가 native DateTime을 넘기며, native time 값은 text만 받으므로 parameter 검사가 CODEC_ENCODE ... is not a datetime value로 거부한다. 완료 기준: 정규 text를 넘기고, 전 red, 후 green.—근거: integration.rs:185가 이제 시간 비교를 그 파일의 txt() text helper로 넘긴다. T210이 Go와 TypeScript에서 한 방식이다. 다른 Rust test는 native time 값을 parameter로 넘기지 않는다(conformance.rs는 이미 txt()를 쓰고, orm test는 시간 값을 읽거나 고정 clock을 쓴다). red: packages/rust/target/debug/integration contracts/fixtures/bench/dbspec-set.json --case conditions가 integration.rs:185에서 CODEC_ENCODE: parameter 0 (DateTime(...)) is not a datetime value로 panic. green: 같은 명령이 sqlite, mysql, postgres에서 0으로 끝난다.[o]
T218test server의 PostgreSQL replica가 동기를 유지한다. 원인: log가 requested WAL segment ... has already been removed를 반복해, synchronous replication을 기다리는 commit이 멈추고 primary_and_replica postgres case가 막힌다. 완료 기준: server 설정의 원인을 적고 고치며(WAL 보존 또는 replication slot), 긴 실행 동안 replica가 streaming을 유지하는 case를 둔다.—근거: replica는 replication slot 없이 pg_basebackup -R -X stream으로 만들어졌고 wal_keep_size는 0이어서, checkpoint가 멈췄거나 뒤처진 replica가 아직 필요한 WAL을 지웠고 모든 remote_apply commit이 끝없이 기다렸다. pg_basebackup은 이제 -C -S orm_replica로 실행된다(scripts/test-servers.mts의 postgresBaseBackupArguments): 물리 replication slot이 replica가 받지 않은 모든 segment를 보존하고, -R이 그 primary_slot_name을 쓴다. 시작은 두 data directory를 다시 만들어 slot을 지운다. docs/spec/config.md(+ko). scripts/test-servers.test.mts의 case는 빈 port에 자기 primary와 replica를 만들고, replica를 멈춘 뒤 insert, pg_switch_wal(), CHECKPOINT를 8번 쓰고, replica를 다시 시작해 20초 안의 remote_apply commit과 streaming replica를 요구한다. red: a synchronous commit did not return within 20 s ... requested WAL segment 000000010000000000000003 has already been removed. green: case가 통과한다(2431 ms).[o]
T219generator scan은 이름을 가질 수 있는 class 집합으로 판정하고, use한 문서 entity의 getter를 받아들인다 (consumer python core requests model). 원인: PHP, Python, TypeScript scan은 이름의 마지막 텍스트 binding만 기억해, 중첩 scope나 분기에서 다시 묶인 이름을 엉뚱한 class로 돌렸고 (IpBlock 행에 Ip.set_blocked is not a model method), getter 검사는 get_<entity>_model을 생성 문서의 entity에만 허용했다 (IpBlockHistory.get_audit_model is called, but no ... attaches it). 결정 (이견이 없을 때까지 적대적 감사로 정함): 이름은 가질 수 있는 class 집합을 가진다 (분기, switch, try, match, 반복을 합치고, return이나 throw로 끝나는 분기는 기여하지 않으며, 반복은 반복 전 상태를 유지하고, null과 할당되지 않은 이름은 model이 없고, 알 수 없는 값은 이름을 가린다). model method 호출은 그 집합의 어떤 class도 선언하지 않을 때만 거부한다. PHP generator는 인자 개수도 그 class들로 검사하고, TypeScript, Rust, Go는 compiler에, Python은 호출 시점 해석에 맡긴다 (docs/spec/dsl.md). T209부터 use한 table은 set의 entity이므로 그 getter는 local과 같이 붙는다. 함께 고침: Rust generate test styled_setter_result_handling_preserves_model_calls가 pub fn expect로 T212의 expect_revision에 걸렸고, 이제 pub fn expect(로 맞춘다. 증거 (2026-10-19), 각 case 단독 실행: RED (299f9976과 이 행의 각 이전 round): 외부 getter와 scope case가 Python, PHP, TypeScript, Rust, Go에서 실패하고, round 2~4 case (union, switch/try/match, 반복, 참조, exit, finally, 0회 반복, null, 인자 개수)가 그 이전 코드에서 실패 (var/scan-scope-red-.log). GREEN (0950457a에 rebase): Python test_orm_gen 26 OK, PHP OrmGenScanTest 45 OK, PHP-TS scan parity 16 OK, TypeScript generate-scan 11 pass, Rust generate 32와 integration 외부 getter 2 통과, Go scan test 11 PASS (var/scan-scope-green-round5-.log). 기록 후 make interface-check green (var/scan-scope-interface-round5.log).——[o]
T220행을 쓰지 않은 감사 unit of work는 감사 기록을 쓰지 않는다 (consumer S-286-19-11: 바꿀 것이 없는 unblock은 아무것도 바꾸지 않아야 한다, requests.md:698). 원인: executor는 감사 top-level transaction이 시작될 때 감사 기록을 insert한다 (PHP Db.php:519-526, 다른 client도 같다). 그래서 unblock의 race 경로처럼 body가 아무것도 쓰지 않는 transaction도 감사 행 하나를 남긴다. 수용 기준: 기록은 unit of work의 첫 write 직전에 insert되어 docs/spec/dbspec.md "Audit"이 요구하는 대로 여전히 변경보다 앞서고, 아무것도 쓰지 않는 unit은 insert하지 않는다. docs/spec/dbspec.md(+ko)에 명시하고, conformance vector가 Go, PHP, Python, Rust, TypeScript에서 MySQL, PostgreSQL, SQLite로 red 후 green으로 실행된다.—근거: 대기 중인 audit 기록은 audit key를 bind하는 첫 statement 바로 전에 넣는다. insert는 그것을 먼저 넣고, update, delete, restore는 SAVEPOINT orm_audit, audit insert, 쓰기를 실행하며 쓰기가 행을 바꾸면 release하고 0행이나 오류면 rollback한다. release하지 않는 중첩 savepoint는 이전 audit를 되돌린다. bench에 audited table audited_note가 audit_record, audited_note_history와 함께 더해졌다. docs/spec/dbspec.md Audit, docs/operations/usage.md, docs/spec/interfaces.md(+ko). client 변경을 거꾸로 적용한 red: vector audit_lazy가 MySQL, PostgreSQL, SQLite에서 다섯 client 모두 {before:1, read:0, unchanged:1, written:1, rolled_back:true, after:0}를 주며, 기록이 어떤 쓰기보다 먼저 있다. green: {before:0, read:0, unchanged:0, written:1, rolled_back:true, after:0}, 세 dialect에서 합의한 다섯 client로 기록했고, audit_lazy가 이제 연결의 첫 row lock을 만들므로 SQLite write_cycle을 함께 다시 기록했다. audit_transaction_test.go의 Go case가 eager code에서 실패하고 통과한다. interface-check와 documents-check가 통과한다.[o]
T221test-servers-check의 MySQL migration case가 통과한다. 원인: "a MySQL setting other than the declared one migrates the data with equal counts, then nothing"이 main에서 실패한다: 출력에 RUN mysql-migration/preflight no-deadline이 없고, 메시지는 선언된 값 1에 대한 lower_case_table_names=2를 표현한다. 완료 기준: 원인을 적고 case가 통과한다. 전 red, 후 green.—근거: migration은 맞았고 case가 낡았다. a004c343(T118-7)이 runLong이 TAP 줄(ok - <case>, not ok - <case>와 # <reason>)을 출력하게 했는데, case는 여전히 RUN ... no-deadline을 기대했다. lower_case_table_names=2는 macOS 기본값이며, case는 일부러 거기서 선언된 1로 migrate한다. case(scripts/test-servers-mysql.test.mts)는 이제 정확히 여덟 줄 ok - mysql-migration/{preflight,dump,stop,keep,start,restore,verify,record}를 순서대로 요구하고, deadline 줄이나 Bail out!을 거부한다. red: 여덟 단계가 ok를 출력했지만 정규식 /^RUN mysql-migration\/preflight no-deadline$/m이 맞지 않았다. green: case가 통과하고 파일이 6개 중 6개 통과한다. Linux 전용 collision case는 그 이유 줄과 함께 not ok - mysql-migration/preflight를 기대한다. macOS에서는 일찍 돌아가므로 Linux에서만 검증된다.[o]
T221-1MySQL migration case의 Linux lower_case_table_names 충돌 분기가 Linux에서 red 후 green으로 실행된다 (T221에 이어). 원인: 이 분기는 macOS에서 바로 return하므로 T221은 macOS 경로만 검증했다. 보류: 이 기계에는 orm용 Linux host가 없고, 유일한 Linux VM(lima soksak-linux-arm64)은 다른 project의 것이며, orm VM을 만드는 것은 사용자가 결정한다. Retry: 그 case를 Linux에서 수정 전 red, 수정 후 green으로 실행하고 log를 남긴다.—node --test --test-name-pattern='names that one lower case name would merge refuse the migration' scripts/test-servers-mysql.test.mts[-] 원인: 2026-10-25 사용자 결정(local macOS 전용): orm은 이 Mac에서 plain symlink로 local 실행만 하면 되므로 Linux 전용 검증 실행은 범위 밖이다
T222Python scan은 생성된 models module에서 가져온 class만 model로 묶고, 매개변수는 자기 binding으로 시작한다 (T219에 이어, consumer python Module.holds is not a model method). 원인: scan.py ImportCollector가 가져온 class 이름만으로 model에 묶었다. consumer contracts/src/kit/harness_calls.py는 consumer.model.models에서 ORM Module as ModuleRow를, ..calls에서 자체 non-model Module을 가져오는데, Module이 ORM model에 묶여 모든 module.holds/stock/run/products/packages/items/load/submit 호출이 그것으로 검사됐다. 같은 검토에서 바깥 binding이 새는 매개변수도 찾았다: Python *args, **kwargs, lambda 매개변수, 그리고 다른 class로 type이 지정된 TypeScript callback 매개변수가 receiver의 model을 받았다. 수정: class 이름은 models라는 module(orm-gen이 쓰는 models.py)에서 가져올 때만 alias로 model에 묶고, 다른 module에서 같은 이름을 가져오면 binding을 푼다. Python 매개변수는 annotation이 model을 가리킬 때 외에는 unknown으로 시작한다. TypeScript는 annotation이 가리키는 class를 받는다. 다른 module로 재export된 model은 묶이지 않아 그 호출은 거부되지 않고 검사되지 않는다 (T219 규칙). PHP는 새는 곳이 없었다 (guard case 추가). 증거 (2026-10-19), 각각 단독 실행: RED (033edc4d): consumer의 make python-model 명령(읽기 전용, 출력은 scratch)이 Module.holds is not a model method; Module.stock ...로 실패 (var/scan-params-consumer-red.log). 최소 case test_a_class_named_like_a_model_but_imported_from_another_module_is_not_the_model이 Author.set_ghost_column is not a model method로 실패. Python variadic, lambda case와 TypeScript callback_parameters case 실패 (var/scan-scope-red-params-*.log). GREEN: consumer model set(36 model)과 requests set(8 model) 생성 (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 (var/scan-params-interface-consumer.log).——[o]
T223한국어 page가 어느 깊이에서나 영어 page로 연결한다. 원인: docs/.vitepress/config.mts가 한국어 page의 영어 page 연결을 ../<path>.html로 써서 최상위에서만 맞으므로, docs-check의 vitepress build가 중첩된 plan의 dead link ./../plans/execution/plan과 ./../plans/kit-standard/plan으로 실패한다docs/.vitepress/config.mtsmake docs-check[o]
T224생성된 PHP model이 unique key와 index를 어떤 순서로 선언하든 manifest와 맞는다(limepie: CONFIG generated model Limepie\Model\CommandRun of entity command_run differs from its manifest at unique). 원인: orm-gen은 meta를 문서의 선언 순서로 만들고(OrmGen.php, RuntimeModel::tableEntity) manifest text는 canonical이라 unique key와 index를 명칭으로 정렬하므로(Dbspec/Emitter.php), fromModels가 한 table의 두 순서를 비교해 거부한다packages/php/src/RuntimeModel.php, packages/php/src/OrmGen.php, packages/php/tests/RuntimeModelTest.phpvendor-php/bin/phpunit --configuration packages/php/phpunit.xml.dist packages/php/tests/RuntimeModelTest.php[o]
T225TypeScript package의 빌드된 orm-gen bin이 실행 가능하다(fsm). 원인: scripts/typescript/build.mts가 tsc 출력을 그대로 게시하고 tsc는 dist/bin/orm-gen.js를 mode 644로 쓰므로, 소비자의 node_modules/.bin/orm-gen이 Permission denied로 실패한다. dbspec bin은 mode 755로 commit되어 있어 해당하지 않는다scripts/typescript/build.mts, packages/ts/tests/bin-mode.mjsnpm run -s typescript:build && node --test packages/ts/tests/bin-mode.mjs[o]
T226scan한 model 호출은 모든 generator에서 그 class를 import한 생성 module의 set에 속한다. Cause: TypeScript scan이 class 명칭으로만 호출을 귀속시켜, limepie에서 orm-gen gen --set requests가 main set의 Account.getEnabledTotps()(node/src/accounts/store.ts:79)에서 실패한다. requests set도 그 Account를 use한다docs/spec, the scanners of the five generators, their tests, config/parity.jsonnode --test packages/ts/tests/generate-scan.mjs, go test -run Scan ./packages/go/generator, the scan tests of PHP, Python and Rust[o]
T226-1TypeScript scan은 재할당된 변수에 담긴 receiver를 그 initializer와 재할당을 따라 class와 그 class의 import로 푼다 (T226 후속). Cause: T226 뒤 limepie models.ts에 ltSeq와 andLtSeq가 없다; modules/core/node/src/console/audit.ts:130이 자기 chain으로 재할당한 let page = new Audit()...에서 이들을 부른다the scanners of every generator, their testsnode --test packages/ts/tests/generate-scan.mjs[o]
T227orm-gen이 scan 중 parse할 수 없는 파일은 모든 generator에서 오류다. Cause: packages/python/src/polyspec/orm/generate/scan.py:561은 parse할 수 없는 파일을 조용히 건너뛰는데 PHP와 TypeScript scanner는 실패한다; 구현 간 차이이자 버려진 오류다docs/spec/dsl.md and its Korean twin, the scanners of the five generators, their tests, config/parity.jsonnode --test packages/ts/tests/generate-scan.mjs, go test -run Scan ./packages/go/generator, the scan tests of PHP, Python and Rust[o]
T227-1parse할 수 없는 scan의 Python test가 ruff format을 지킨다 (T227 후속). Cause: main에서 lint-python이 T227이 더한 case인 packages/python/tests/test_orm_gen.py:584에서 실패한다packages/python/tests/test_orm_gen.pymake lint-python, node --test packages/ts/tests/generate-scan.mjs[o]
T228DBS-173은 모든 generator가 set이 use line으로 쓰는 각 external table의 읽기 전용 model을 scan한 source가 부르든 안 부르든 쓴다고 규정하고, 다섯 generator에 그 규칙을 지키는 회귀 case를 둔다 (T209 후속). Cause: T209 이전의 consumer 생성 model이 낡아 generator 간 차이처럼 보였고, spec이 그 규칙을 적지 않았다docs/spec/dbspec.md and its Korean twin, the five generators, their tests, config/parity.jsonnode --test packages/ts/tests/generate-scan.mjs, go test -run External ./packages/go/generator[o]
T229PostgreSQL LISTEN과 NOTIFY가 모든 구현의 client 기능이다: notify(channel, payload)는 transaction 안에서 보내져 그 commit과 함께 전달되고, listen(channel)은 전용 connection에서 받는다; MySQL과 SQLite는 둘 다 명시된 오류로 거부하며 조용한 no-op은 없다 (소비자: limepie drama design). Cause: docs/spec이 둘 다 적지 않아 어느 client도 제공하지 않는다docs/spec/dialects.md and protocol.md with their twins, every client, a PostgreSQL conformance case, config/parity.jsonmake parity-declarations-check UNITS=postgres-listen-notify[o]
T230MySQL lock wait timeout(1205)은 CFG-6의 statement timeout처럼 모든 client에서 CANCELED이다 (소비자: limepie drama design). Cause: 어느 client도 1205를 mapping하지 않아 DRIVER가 되는데(python database.py:566-584, php OrmException.php:77, ts driver.ts:184), errors.yaml은 만료된 timeout bound를 CANCELED로 정한다docs/spec/errors.yaml, every client, a MySQL conformance case, config/parity.jsonGOPROXY=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-1T230 주석 변경 후 orm main이 빌드된다: php-ext arginfo 헤더가 stub과 일치한다packages/php-ext/src/polyspec_orm_arginfo.hmake build-php-ext[o]
T231errors.yaml이 DUPLICATE_KEY에 모든 database의 duplicate key를 적는다: 모든 client가 mapping하듯 MySQL 1062, PostgreSQL 23505, SQLite 2067과 1555. Cause: docs/spec/errors.yaml:87이 MySQL 1062 / SQLSTATE 23000만 적는다docs/spec/errors.yaml, config/parity.jsonmake text-absent FILE=docs/spec/errors.yaml TEXT="DUPLICATE_KEY # MySQL 1062 / SQLSTATE 23000"[o]
T232docs/spec/dialects.md가 고정된 PHP 8.4 pdo_sqlite의 SQLite version을 select sqlite_version()으로 측정해 적는다 (소유자 판정: PHP 8.5는 8.4로). Cause: dialects.md:76과 dialects.ko.md:71이 PHP 8.5.10의 pdo_sqlite를 적는다docs/spec/dialects.md and its twinmake text-absent FILE=docs/spec/dialects.md TEXT="PHP 8.5.10"[o]
T233docs/operations/perf.md가 PHP 8.5 비율을 인용하지 않는다 (소유자 판정: PHP 8.5는 8.4로). Cause: perf.md:42와 perf.ko.md:43이 PHP 8.5.10에서 잰 비율을 인용한다; 이를 빼거나 최종 benchmark 단계에서 8.4로 다시 잰다docs/operations/perf.md and its twinmake text-absent FILE=docs/operations/perf.md TEXT="PHP 8.5.10"[o]
T235취약한 markdown-it 잠금과 보안 검토를 갱신한다. 원인: package-lock.json이 수정 버전 14.3.3이 있는데도 high advisory 1242672가 있는 markdown-it 14.3.2를 고정한다.package-lock.json, config/dependency-review.jsonmake dependency-policy-check; make dependency-policy-mutation-check[-] 원인: 사용자 2026-10-22: 개발 중 제3자 advisory 또는 pin 작업 없음
T234PHP client 문서의 prose가 제3자 YAML 제품 이름을 적지 않는다. 원인: T142-1-1과 T142-1의 CHANGELOG 항목과 packaging 문서가 upstream YAML 제품 이름을 적는다. 사실은 upstream 1.0.0 release가 YAML 1.2.2 core schema로 plain scalar를 읽지 않으며 fork release가 이를 고친다는 것뿐이다. 인수 조건: 네 파일이 의존성 package 이름, fork URL, 수정 사실을 남기고 prose에서 upstream 제품 이름을 쓰지 않는다. red: 지금 네 파일에 그 이름이 있다.CHANGELOG.md, CHANGELOG.ko.md, docs/operations/packaging.md, docs/operations/packaging.ko.mdmake 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]
T236Root LIMIT 개수와 OFFSET 값을 parameter로 바인딩해 같은 query shape의 페이지가 여섯 구현체(Go, PHP library, native PHP extension, Python, Rust, TypeScript)에서 plan 하나를 공유하게 한다. 원인: literal page 값이 plan-cache key를 바꾸어 offset마다 새 항목이 생기며 Engine::$plans(cacheSize 256)는 약 161 페이지 뒤 가득 찬다. limepie peer session 요청(P-41)이며 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통합과 제거 전에 서버를 멈추고 data 삭제 전에 worktree stop target을 선언한다. 원인: ORM Makefile에 Makefile-origin WORKTREE_STOP이 없다. kit land는 Makefile-origin 값만 읽으므로 환경 변수나 command-line override는 stop target을 등록하지 않는다. 관련: KSTD-12.ORM Makefile에 WORKTREE_STOP = orm-test-servers-stop을 추가한다.node --test scripts/repo/worktree-stop.test.mts가 격리된 환경에서 kit이 ORM의 Makefile-origin stop target을 읽는지 검증한다.[o]
T238완료된 worktree-stop test의 폐기 대상 prune 의존성을 제거한다. 원인: T237 test의 worktree-prune import가 제거를 막는다. WORKTREE_STOP 검증은 유지한다.scripts/repo/worktree-stop.test.mts; paired T237 verification descriptionnode --test scripts/repo/worktree-stop.test.mts; no worktree-prune import or prune-only case remains[o]
T239서버를 초기화할 때 ORM 테스트 데이터베이스가 로컬 served-test 로그인을 지원하면서 인증과 TLS 검사를 유지한다. 원인: PostgreSQL이 loopback의 모든 역할을 trust하고, MySQL binary logging은 SUPER가 아닌 로그인 사용자의 audit trigger 생성을 막으며, replica 인증서는 VERIFY_IDENTITY 정상 연결을 막는다. 기존 PostgreSQL 인증과 MySQL daemon 설정은 서버를 기존 절차로 stop하고 start한 뒤 적용되며, start 명령은 lease 중인 서버를 재시작하지 않는다.scripts/test-servers.mts; scripts/test-servers.test.mtsnode --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설치된 로컬 Kit package에서 추적하는 Kit 사본을 갱신하고 일반 파일로 유지한다.claude/settings.json, .editorconfig, PRINCIPLES.md, PRINCIPLES.ko.md, .github/workflows/release.yml, .github/workflows/gates.ymlmake kit-check; confirm the six tracked copies are regular files matching the installed local Kit source.[o]
T2410.0.19를 release한다. 원인: kit 0.0.66 채택(KSTD-5-101), dependency target 제거(KSTD-5-100), lease 전용 release build(KSTD-5-98), 선언된 wheel backend(KSTD-5-99), relation step의 row lock(T257), editable Python 설치(T259), 서식을 맞춘 Rust test(T260), kit 0.0.75(KSTD-5-122)가 main에 있다. 인수: version-bump commit이 모든 manifest를 0.0.19로 맞추고 Unreleased를 0.0.19로 옮긴다. make release-candidate-assets가 일곱 archive를 만들고, T244 이후 orm-gen은 PHP archive 안에 있다. tag는 owner를 기다린다VERSION, every manifest and lock that carries the version, CHANGELOG.md, CHANGELOG.ko.mdmake release-candidate-assets VERSION=0.0.19[v]
T242orm에는 version 검사가 없다: scripts/version/check.mts, 그 test scripts/version/check.test.mts, version-check target, PLN-5를 삭제하고, release 때는 kit make release-versions가 모든 manifest에 tag version을 요구한다. 원인: PLN-5 test가 version을 literal로 적어 모든 release commit이 그것을 바꿔야 했고, kit K449는 release commit의 test를 거부한다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.mtsmake parity-declarations-check owner-validate[o]
T243config/parity.json이 packages/rust/orm/tests/unit/temporal_args.rs를 선언하므로 make parity-declarations-check가 main에서 통과한다. 원인: main 8685a2758에서 이 검사는 그 파일에 R5 undeclared-test를 보고한다. 4dedfb2d6(KSTD-5-94)이 owner target rust-temporal-args-check와 함께 파일을 추가했지만 parity 선언은 빠졌다config/parity.jsonmake parity-declarations-check[o]
T244polyspec/orm이 다시 orm-gen을 담는다: bin/orm-gen과 OrmGen, Generator, Scan, ScanVisitor, ChainDeclare, UsageError, 그리고 그 test가 0.0.18처럼 packages/php에 있고, packages/php-gen package와 그 release 항목은 삭제된다. 원인: T182-6-19-4(7f0249f84)가 kit이 삭제하는 kit 규칙 S10에 따라 orm-gen을 polyspec/orm-gen으로 나눴으므로, polyspec/orm link의 소비자에게 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.mdmake php-gen-unit-check[o]
T244-1packages/php의 orm-gen class는 internal이다: OrmGen, Generator, Scan, ScanVisitor, ChainDeclare, UsageError는 generator 명령을 구현하며 PHP client의 API가 아니다. Go(cmd/orm-gen)와 TypeScript(src/tools, export 안 함)의 generator도 그 API가 아니다. 원인: T244가 이들을 PHP 구현의 symbol entry인 packages/php/src로 옮겨, parity API phase가 public symbol 23개를 선언되지 않은 것으로 보고한다(R11)packages/php/src/OrmGen.php, packages/php/src/Generator.php, packages/php/src/Scan.php, packages/php/src/ScanVisitor.php, packages/php/src/ChainDeclare.php, packages/php/src/UsageError.phpmake client-unit-check/orm-gen[v]
T245CI tool 선택과 orm-install-python의 repository test는 재귀 make의 command text가 아니라 호출된 install-tools target과 그 TOOLS를 맞춘다. 원인: kit K762가 모든 재귀 make의 command line에 KIT와 KIT_EXT를 넣으므로, make --no-print-directory install-tools를 고정한 scripts/repo/check.test.mts와 scripts/repo/python-venv.test.mts의 regex가 선택이 옳은데도 실패한다scripts/repo/check.test.mts, scripts/repo/python-venv.test.mtsmake repo-check/unit[o]
T246scripts/repo/testcases.mts의 runtime path 검사는 checkout 하나가 공유하고 run이 덮어쓸 수 있는 server 환경 TEST_ENV ?= .runtime/servers/env를 받아들인다. 원인: KSTD-5-94(b597a6c49)가 TEST_ENV를 ?=로 덮어쓸 수 있게 했는데 검사는 여전히 TEST_ENV =만 허용했다scripts/repo/testcases.mts, scripts/repo/check.test.mtsnode --test --test-name-pattern='the files of one run live in its run directory' scripts/repo/check.test.mts[o]
T247추적되는 모든 lock file이 다시 소유자를 가진다: packages/ts/testing/interfaces/typescript/package-lock.json은 그것이 설치하는 TypeScript를 실행하는 interface-typescript-extractor-check의 input이고, release consumer lock인 tests/release-consumer/npm/package-lock.json과 tests/release-consumer/composer/composer.lock은 contracts/check-inputs.json에 scope를 선언한다. 원인: KSTD-5-100이 inputs **/package-lock.json과 **/composer.lock으로 그것들을 소유하던 dependency-policy-check를 삭제했다contracts/check-inputs.jsonmake feature-unit-check/owners[o]
T248config/parity.json의 TypeScript binding command 세 개는 make가 include한 kit의 node:test reporter를 실행한다: parity target이 ORM_NODE_REPORTER = $(KIT)/parity-node-reporter.$(KIT_EXT)를 export하고, command는 --test-reporter=${ORM_NODE_REPORTER}를 넘긴다. 원인: KSTD-5-94(16204d8e6)가 link된 kit의 gitignore된 로컬 build인 node_modules/@polyspec/kit/dist/parity-node-reporter.mjs를 지정해서, command가 오래된 build를 실행하거나 kit이 build되지 않은 곳에서 실패한다config/parity.json, Makefilemake parity-declarations-check[o]
T249make duplication-check가 clone을 찾지 않고, parity context test는 config/ci.json의 ts setup을 읽는다: SqliteWithoutMysqlTest.php는 PHP child를 한 method로 실행하고, parity-context.test.mts는 실패하는 command와 삭제된 directory 검사를 각각 한 helper로 만들고 ts setup에 orm-install-interfaces-typescript를 기대하며, performance-reference.mts는 scripts/check/record.mts의 git reader로 git을 읽는다. 원인: KSTD-5-94, KSTD-5-12, KSTD-7-6이 clone 5개를 남겼고 land gate는 TypeScript나 PHP file을 바꾸는 모든 branch에서 그것을 보고하며, 4dedfb2d6은 interfaces TypeScript를 ts setup에 더하면서 그 test를 고치지 않았다packages/php/tests/SqliteWithoutMysqlTest.php, scripts/check/parity-context.test.mts, scripts/check/record.mts, scripts/features/performance-reference.mtsmake duplication-check; node --test scripts/check/parity-context.test.mts scripts/features/performance-reference.test.mts[o]
T250packages/python/tests/coverage_catalog_connection.py는 고정된 ruff format의 형식이다. 원인: KSTD-5-94가 line 길이를 넘는 두 line을 남겼고, make lint-python은 Python file을 바꾸는 모든 branch에서 그것을 보고한다packages/python/tests/coverage_catalog_connection.pymake lint-python[o]
T251MySQL에서 planner는 write 순서의 모든 뒤 단계가 SQL에서 실행될 때만 그 단계를 SQL에서 실행하므로, codec ip hex column은 모든 구현에서 주소를 client에서 pack하고 unpack하며 INET6_ATON이나 INET6_NTOA 없이 hex text를 bind하고, planner vector가 그것을 고정한다. 원인: planner가 단계마다 SQL을 골라 MySQL에서 client가 주소 text에 hex를 적용하고 INET6_ATON이 그 hex text를 받았다(1411 Incorrect string value for function inet6_aton, limepie MySQL 실행에서 발견). hex는 T103부터 client에서 실행된다docs/spec/dialects.md, contracts/fixtures/planner.json, contracts/fixtures/ip_hex.dbs, the planners of Go, PHP, the PHP extension, TypeScript, Python and Rustnode packages/ts/tests/coverage_planner.mjs planner_ip_hex_insert planner_ip_hex_select[o]
T252packages/rust/tests/src/integration.rs의 privilege test는 ORM_TEST_POSTGRES_SERVER_DSN을 Result::ok 없이 변수 오류를 밝히는 match로 읽는다. 원인: KSTD-5-94가 그것을 clippy가 금지하는 ok()로 읽어(P8) make lint-rust가 Rust file을 바꾸는 모든 branch에서 실패한다packages/rust/tests/src/integration.rsmake lint-rust[o]
T253codec column의 값 predicate는 비교하는 대상이 값의 결정적 함수일 때(모든 단계가 hex나 ip인 column의 셀, 또는 AES column의 blind index) equality 연산자를 받고, 그 밖에는 null 검사만 받는다. codec.md COD-43이 그 규칙을 밝히고 Go ir.OpAllowed가 그것을 따르며, 모든 구현에 hex와 ip hex의 eq planner case가 있다. 원인: Go는 단일 ip에만 equality를 허용해 hex나 ip hex column의 eq가 Go에서는 OPERATOR_NOT_ALLOWED이고 PHP, PHP extension, TypeScript, Python, Rust에서는 받아들여졌으며, COD-43은 COD-4, PRT-22, PRT-67과 모든 구현에 어긋나게 스타일 컬럼의 predicate를 null 검사로 제한했다docs/spec/codec.md, packages/go/engine/ir/ir.go, contracts/fixtures/planner.json, contracts/fixtures/ip_hex.dbsnode tests/go-test.mts -tags featurecoverage -timeout 0 ./packages/go/orm -run '^TestCoveragePlanner(IpHex|Hex)' -count=1[v]
T254codec column은 모든 단계가 술어 값을 인코딩하는 host stage(aes, hex, ip)일 때에만 equality로 비교한다: hex와 ip는 저장된 셀을, aes는 위치와 관계없이 값의 HMAC인 blind index를 비교하며, ordered_json aes처럼 executor codec을 가진 column은 null 검사만 받는다. codec.md COD-43이 그 규칙을 밝히고 Go, PHP, PHP extension, Rust가 그것을 따르며, 모든 구현에 aes hex, hex aes, ordered_json aes의 eq planner case가 있다. 원인: Go와 PHP는 첫 단계가 aes일 때, Rust는 어느 단계든 aes일 때, TypeScript와 Python은 모든 단계가 aes, hex, ip일 때 equality를 받아 hex aes나 ordered_json aes의 eq가 어떤 구현에서는 compile되고 다른 구현에서는 OPERATOR_NOT_ALLOWED였다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.dbsnode tests/go-test.mts -tags featurecoverage -timeout 0 ./packages/go/orm -run '^TestCoveragePlanner(AesHex|HexAes|OrderedJsonAes)Eq$' -count=1[v]
T255시작이 실패한 Python transaction은 pool connection을 돌려준다: 실패한 BEGIN은 connection을 닫고, 그 뒤 시작 단계가 실패하면 열린 transaction을 rollback한다. TypeScript, Go, Rust와 같으며 interfaces.md INT-72가 그 규칙을 밝힌다. 원인: Db._begin이 _finish를 부르기 전에 frame을 끝난 것으로 표시해 뒤 단계의 실패는 ROLLBACK 없이 곧바로 돌아왔고, 실패한 BEGIN은 _give_back 없이 다시 raise했다. connection은 사용 중으로 남아 Db.close()가 닫지 못했고, SQLite lock 경합에서 그 lock이 garbage collector가 닫을 때까지 잡혀 있었다(limepie U-10-9 보고)packages/python/src/polyspec/orm/database.py, packages/python/tests/test_rollback_fault.py, docs/spec/interfaces.mdmake transaction-failure-python-check[v]
T256Python client는 dbspec.md DBS-104가 요구하고 TypeScript가 하듯 manifest의 model을 process마다 한 번 만든다: manifest hash를 key로 하고 manifest와 external text로 확인하는 registry가 이미 만든 model을 돌려주므로 register_model, Db.register, SchemaUtils.register, connect_schema는 schema를 한 번 parse하고, RuntimeModel.load_set은 같은 text에 대해 이미 parse한 document를 돌려준다. 원인: model_of_manifest와 RuntimeModel.load_set이 호출마다 document set 전체를 parse해, request마다 2개 connection에 9개 schema를 등록하는 limepie가 request의 약 85퍼센트를 dbspec parser에서 썼다(limepie U-10-10 보고)packages/python/src/polyspec/orm/engine/model.py, packages/python/tests/test_load_set.pypackages/python/.venv/bin/python -m packages.python.tests.test_load_set[v]
T257row lock(forShare 또는 forUpdate)은 limit_per_parent와 group_by가 없는 relation step에서 모든 구현에서 유효하다. 원인: 모든 validator가 relation step의 lock을 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). planner는 plain relation 경로에 이미 lock을 붙인다(ts planner.ts:394-399). limepie는 lock한 batch를 참조하는 row를 FOR SHARE로 읽어야 하며, 손으로 모은 key도 subquery IN 값(limit 없음, parent 조건을 다시 평가)도 할 수 없다. per-parent window 경로(planner.ts:362-377)는 계속 거부한다: window는 row lock을 가질 수 없다. 완료 기준: spec이 규칙을 명시하고, 모든 구현이 plain relation step의 lock을 받아들이며 limit_per_parent나 group_by가 있으면 거부하고 공유 validate case가 있다. conformance case가 MySQL과 PostgreSQL에서 relation statement에 lock이 붙음을, SQLite에서 client의 row lock 동작을 보인다CHANGELOG.ko.md, CHANGELOG.md, docs/spec/dsl.ko.md, docs/spec/dsl.md, docs/spec/protocol.ko.md, docs/spec/protocol.md, packages/go/engine/ir/ir.go, packages/go/internal/conformance/main.go, packages/go/orm/exec.go, packages/php-ext/src/db.c, packages/php-ext/src/validator.c, packages/php/src/Db.php, packages/php/src/Validator.php, packages/php/testing/conformance/runner.php, packages/python/src/polyspec/orm/database.py, packages/python/src/polyspec/orm/engine/validate.py, packages/python/testing/conformance/runner_python.py, packages/rust/orm/src/engine/validate.rs, packages/rust/orm/src/model.rs, packages/rust/orm/tests/unit/ir_request.rs, packages/rust/tests/src/conformance.rs, packages/ts/src/database.ts, packages/ts/src/engine/validate.ts, packages/ts/testing/conformance/runner_typescript.mts, tests/conformance/vectors.json, tests/conformance/vectors.postgres.json, tests/conformance/vectors.sqlite.json, tests/ir/validate.jsonmake run-databases TARGETS=conformance-check VECTOR=relation_row_lock[v]
T258공개 PHP signature는 internal type을 가리키지 않는다. 원인: parity API phase가 R17을 보고한다: Catalog::rowSnapshotFromPage, updateRow, deleteRow가 CatalogRowSnapshot을 가리키는데, interfaces.md "Catalog row snapshots and writes"가 row snapshot을 API로 정하고 Go와 TypeScript가 export함에도 PHP library는 이를 internal로 표시한다. Model은 DBS-120이 각 생성 class의 계약으로 정한 meta()의 interface ModelMetadata를 구현하는데, library와 extension 모두 이를 internal로 표시한다. extension stub은 library가 internal로 표시한 Model::registerJoins(Frame)을 public으로 선언한다packages/php/src/CatalogRowSnapshot.php, packages/php/src/ModelMetadata.php, packages/php-ext/src/polyspec_orm.stub.php, config/parity.jsonmake lint-php-ext; make parity-declarations-check[v]
T259kit K855가 Python symbol을 source package에서 읽게 되면 make orm-install-python은 Python client를 uv sync의 기본값인 editable로 다시 설치한다. 원인: KSTD-5-104는 kit K803이 non-editable 환경의 설치 파일에서 symbol을 읽어서 설치를 --no-editable로 바꿨다. make python-models가 생성하는 fixture model은 package가 제외하는 packages/python/src/polyspec/orm/models에 있어서, 새 non-editable venv에서는 polyspec.orm.models를 import하는 모든 Python test가 ModuleNotFoundError로 실패하고(T257의 conformance-check/python, coverage_dsn, coverage_transactions), source 변경은 uv가 package를 다시 build할 때까지 설치되지 않는다. 인수 조건: 새 checkout에서 make orm-install-python 뒤 make python-check와 conformance-check/python이 통과하고, 편집한 source 파일이 venv가 import하는 파일이다Makefile, CHANGELOG.md, CHANGELOG.ko.mdpackages/python/.venv/bin/python -m unittest packages/python/tests/test_model_fields.py[v]
T260make rust-fmt-check가 main에서 통과한다. 원인: d76cd25f4에서 rustfmt가 KSTD-5-110이 IpHexCell을 순서에 맞지 않게 더한 import 목록 packages/rust/tests/src/conformance.rs:12와 폭을 넘는 panic! message packages/rust/tests/src/integration.rs:460을 보고하므로, 0.0.19 release 전에 static gate가 red다packages/rust/tests/src/conformance.rs, packages/rust/tests/src/integration.rs, CHANGELOG.md, CHANGELOG.ko.mdmake rust-fmt-check[v]
T261gates shared job은 tests/lease가 build하는 Go module을 설치한다. 원인: kit-gates-static이 parity-declarations-check를 돌리고 그 prerequisite typescript-build가 lease-tool을 돌리는데 config/ci.json setup.shared가 비어 있어, go build ./tests/lease가 GOPROXY=off에서 golang.org/x/sys를 찾지 못한다(gates 40979519777, job shared)config/ci.json, scripts/check/parity-context.test.mts, CHANGELOG.md, CHANGELOG.ko.mdmake runner-check[v]
T262feature-coverage/typescript/mysql은 interfaces-typescript need를 선언한다. 원인: 그 interface_contract case가 packages/ts/testing/interfaces/typescript/node_modules/typescript를 import하는 TypeScript interface check를 돌리는데, contracts/check-inputs.json이 그 target에 interfaces-typescript need를 주지 않아 CI가 make orm-install-interfaces-typescript를 건너뛴다(ci 40979519721, job feature_typescript_mysql: ERR_MODULE_NOT_FOUND)contracts/check-inputs.json, scripts/check/run.test.mts, CHANGELOG.md, CHANGELOG.ko.mdmake runner-check[v]
T263offline Makefile 검사는 본래 network에서 lock을 쓰는 lock target을 받아들인다. 원인: scripts/check/downloads.mts가 $(ONLINE)을 install target에서만 허용하는데 orm-python-lock(KSTD-5-102)이 uv lock을 online으로 돌려 runner-check가 실패한다(ci 40979519721, job static)scripts/check/downloads.mts, scripts/check/downloads.test.mts, CHANGELOG.md, CHANGELOG.ko.mdmake runner-check[v]
T264한 feature가 PHP extension run guard와 그 test를 소유한다. 원인: KSTD-5-109가 packages/php/tests/extension_run_guard.php와 ExtensionRunGuardTest.php를 contracts/features.json 항목 없이 추가해, feature_helpers, stress_mysql, stress_pg_sqlite, feature_verify_rust, feature_verify_other에서 features contract가 실패한다(ci 40979519721)contracts/features.json, CHANGELOG.md, CHANGELOG.ko.mdvar/tools/bin/node scripts/features/check.mts[v]
T265docs/features.md와 그 한국어 twin을 contracts/features.json에서 다시 생성한다. 원인: 5740e7397(KSTD-7-6)이 contract에서 performance_gate를 implemented로 옮기면서 scripts/features/build.mts를 돌리지 않아, feature-unit-check가 generated document is stale: docs/features.md로 실패한다(ci 40979519721, job static)docs/features.md, docs/features.ko.md, CHANGELOG.md, CHANGELOG.ko.mdmake feature-unit-check[v]
T266bench vector는 bench fixture의 ip_hex_cell table을 따른다. 원인: 9dd8819ac(KSTD-5-110)가 contracts/fixtures/bench/bench.dbs에 codec setting을 가진 ip_hex_cell table을 추가했는데, tests/dbspec/mermaid.json의 bench round trip dropped 목록도 packages/python/tests/test_orm_gen.py의 생성 module hash와 entity 수도 따라가지 않아, 모든 runtime이 24개 대신 export dropped 25 objects를 보고한다(ci 40979519721, job dbspec_go, dbspec_php_extension, client_db_rust, clients_pooler, python)tests/dbspec/mermaid.json, packages/python/tests/test_orm_gen.py, CHANGELOG.md, CHANGELOG.ko.mdcd packages/go && go test -count=1 -run TestMermaidRoundTripVectors ./engine/dbspec, packages/python/.venv/bin/python -m unittest packages.python.tests.test_orm_gen packages.python.tests.test_dbspec_mermaid_round_trip[v]
T267PHP extension declarations test는 ModelMetadata를 client class로 센다. 원인: T258(9526d6183)이 library와 stub에서 Polyspec\Orm\ModelMetadata를 공개하면서 packages/php-ext/tests/declarations_functions.php의 INTERNAL_CLASSES에 남겨 두어 DeclarationsTest::testClient가 실패한다(ci 40979519721, job dbspec_php_extension)packages/php-ext/tests/declarations_functions.php, CHANGELOG.md, CHANGELOG.ko.mdmake dbspec-php-extension-check/declarations[v]
T268config/parity.json의 Python symbol은 import root 접두사 polyspec.orm을 가진다. 원인: kit K788(0.0.70)이 Python symbol을 import root에서 읽어 id가 polyspec.orm.X인데 Python 선언 673개는 여전히 orm.X라, parity-api가 각각을 누락이자 미선언으로 보고한다(gates 40979519777, job parity-api)config/parity.json, CHANGELOG.md, CHANGELOG.ko.md! var/tools/bin/node node_modules/@polyspec/kit/dist/parity.mjs --phase api 2>&1 | grep -E 'R(1|11) [a-z-]+ python'[v]
T269complex query와 hot-path gate의 Python parity binding은 load 가능한 test를 가리킨다. 원인: KSTD-5-94(16204d8e6)가 complex-query와 mysql hot-path test를 test class 없이 가리켰고, KSTD-5-89-9(fcda34baa)가 postgres와 sqlite hot-path binding에 command를 주지 않아, unittest가 _FailedTest를 load하거나 ORM_FEATURE_DATABASE 없이 돈다(gates 40979519777, job parity-bindings python)config/parity.json, CHANGELOG.md, CHANGELOG.ko.mdmake parity-bindings-python-check[ ]
T270PHP test helper는 model을 process 밖에서 생성하므로, extension run은 PHP library의 file을 load하지 않는다. 원인: statement_events_run.php, request_hook_cases.php, CoverageAuditColumnsTest.php, generated_fixture.php가 Polyspec\Orm\Generator를 process 안에서 부르고, KSTD-5-109의 run guard가 Generator.php를 load한 extension run을 실패시킨다(ci 40979519721, job feature_php_extension_mysql, _postgres, _sqlite, client_db_php_extension)packages/php/tests/generated_classes.php, packages/php/tests/coverage_cases.php, packages/php/tests/statement_events_run.php, packages/php/tests/request_hook_cases.php, packages/php/tests/CoverageAuditColumnsTest.php, packages/php/tests/generated_fixture.php, CHANGELOG.md, CHANGELOG.ko.mdmake run-databases TARGETS=feature-coverage/php-extension/sqlite[v]
T271TypeScript thin-slice 예제는 datetime을 text로 bind한다. 원인: T89-3(ba706048e)이 date, time, datetime slot을 text 전용으로 만들었는데, 그 뒤 추가된 예제(6df466075)가 Date object를 넘겨 example-check가 CODEC_ENCODE: parameter 4 (object) is not a datetime value로 실패한다(ci 40979519721, job clients_db)examples/thin-slice/ts/main.mts, CHANGELOG.md, CHANGELOG.ko.mdmake example-check[ ]
T272client-pooler-check는 그 php-extension lane이 build에 쓰는 PHP extension tool을 선언하고 설치한다. 원인: KSTD-5-88-6(97c9bd975)이 client test에 php-extension lane을 추가했는데, client-pooler-check에는 contracts/check-inputs.json의 php-extension-tools need도 prerequisite orm-install-php-extension-tools도 없어 build가 var/tools/php-arginfo/gen_stub.php를 찾지 못한다(ci 40979519721, job clients_pooler)contracts/check-inputs.json, Makefile, scripts/check/run.test.mts, CHANGELOG.md, CHANGELOG.ko.mdmake runner-check[v]
T273TypeScript MySQL driver는 i64 slot을 정수로 bind하므로, ProxySQL을 거친 LIMIT bind가 받아들여진다. 원인: T236(5fb006f26)이 root page limit과 offset을 i64 slot으로 bind하는데, mysqlExecute가 JS number를 넘기고 mysql2가 그것을 DOUBLE로 encode하며, ProxySQL 3.0.9가 DOUBLE LIMIT을 Incorrect arguments to mysqld_stmt_execute로 거부한다(ci 40979519721, job clients_pooler, mysql/columnsAndSubqueries)packages/ts/src/driver.ts, packages/ts/tests/driver-cell-bytes.mjs, CHANGELOG.md, CHANGELOG.ko.mdmake native-error-cause-check/typescript-driver, ORM_CLIENT_DB_LANGS=typescript make run-databases TARGETS=client-pooler-check[v]
T274어떤 public signature도 internal type을 이름 붙이지 않고, 어떤 internal test도 spec requirement를 이름 붙이지 않는다. 원인: kit parity API phase가 TypeScript public symbol 25개와 Python 6개(Python _Dialect, _RegisteredModel, _FunctionValue)에 R17 leaked-internal을, PRT-8을 이름 붙인 packages/rust/tests/src/conformance.rs에 R16 internal-contract를 보고한다(gates 40979519777, job parity-api)packages/ts/src, packages/python/src/polyspec/orm, packages/rust/tests/src/conformance.rs, config/parity.json, CHANGELOG.md, CHANGELOG.ko.mdmake parity-api-check[ ]
T275CARGO_SIBLINGS는 --siblings 없이 kit cargo link를 돌린다. 원인: kit K869가 sibling flag를 link 선언 file로 대체하고 그 새 CLI가 --siblings를 folder로 읽으므로, Makefile은 K869 land 전에 flag를 뺀다. KSTD-5-123이 선언 file을 쓰기 전까지 cargo는 polyspec crate를 공개 tag로 build한다Makefile, CHANGELOG.md, CHANGELOG.ko.mdvar/tools/bin/node node_modules/@polyspec/kit/dist/link.mjs cargo $PWD/packages/rust metadata --locked --offline --format-version 1[v]

MIT License · Go / PHP / Rust / TypeScript