Skip to content

signature / qrcode have no maxLength enforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875

Description

@huangyiirene

Found while fixing #11794 (richtext emitted as varchar(255)). Filed unassigned; deliberately not fixed there — that card's fence was "if it turns out to widen accepted physical shapes beyond the declared contract, stop and report", and these two are exactly what it caught.

Related: #11431 (the string family ignores maxLength). Independent of it — that card is about a bound that exists and is not honoured; this one is about a bound that is honoured by nothing at all.

The invariant #11794 established

A field type may take an unbounded TEXT column exactly when the write seam enforces its declared maxLength. That is not a new rule — schema-drift.ts already rests on it, in as many words:

A TEXT column refuses nothing a maxLength allows, so there is no divergence to plan an ALTER for; the bound is enforced at the write seam.

The measurement

validateRecord (packages/objectql/src/validation/record-validator.ts) applies its max_length / min_length branch to a hard-coded type list. Measured — a { type: T, maxLength: 64 } field, a 100-character value, mode: 'insert':

text       REFUSED  VALIDATION_FAILED  f must be <= 64 characters (got 100)
textarea   REFUSED  VALIDATION_FAILED  f must be <= 64 characters (got 100)
markdown   REFUSED  VALIDATION_FAILED  f must be <= 64 characters (got 100)
html       REFUSED  VALIDATION_FAILED  f must be <= 64 characters (got 100)
richtext   REFUSED  VALIDATION_FAILED  f must be <= 64 characters (got 100)
code       REFUSED  VALIDATION_FAILED  f must be <= 64 characters (got 100)
signature  ACCEPTED   <- no max_length branch
qrcode     ACCEPTED   <- no max_length branch
secret     ACCEPTED   <- no max_length branch
color      ACCEPTED   <- no max_length branch
string     ACCEPTED   <- no max_length branch (the column carries it — #11431)

maxLength is a plain optional key on FieldSchema (field.zod.ts), admitted on every field type. So Field.signature({ maxLength: 64 }) parses, publishes, and binds nothing.

Why that leaves these two types stuck

signature and qrcode are STRING_VALUE_TYPES members whose stored value is the author's own value, and it is routinely far past 255 characters — field-zoo writes a data-URI PNG for signature. So they have the same live defect #11794 fixed for richtext. Measured on live MySQL 8.0.46 (STRICT_TRANS_TABLES) and Postgres 16, a 1000-character value today:

                 physical column           1000-char write
signature   pg   character varying(255)    REFUSED 22001
signature   my   varchar(255)              REFUSED ER_DATA_TOO_LONG
qrcode      pg   character varying(255)    REFUSED 22001
qrcode      my   varchar(255)              REFUSED ER_DATA_TOO_LONG

But moving them into the TEXT family the way richtext and code moved would replace an under-accepting column with an over-accepting one: with no write-seam bound and no column bound, a declared maxLength would be enforced by nothing on any dialect. That is a physical surface wider than the declared contract, so #11794 left them where they were and asserted the refusal out loud instead (sql-driver-11794-richtext-text-family.test.ts, "records the STILL-OPEN half"). Note the enforcement is not uniformly absent even today: the #11374 keyed-and-bounded rule would give a keyed bounded column varchar(maxLength), so a move would make enforcement depend on whether an index happens to key the column — the dialect/shape-divergent enforcement of one declaration that this file's conformance matrices exist to close.

The decision this needs

Not prescribing a remedy. At least three readings, and they shape a public contract:

  1. Give the write seam a bound for them — add signature / qrcode to the record-validator's string branch, then move them into the TEXT family. Declared = enforced, and the data-URI defect goes away.
  2. Treat them as string-family — declaredVarcharLength, i.e. varchar(maxLength) with TEXT above MAX_VARCHAR_CHARS. Honours a declared bound, but an undeclared signature still takes varchar(255) and still refuses a data URI.
  3. Rule maxLength inapplicable to them (ADR-0049 enforce-or-remove) and refuse it at publish time — then TEXT is unambiguously correct and option 1's validator change is unnecessary.

secret and color sit in the same measurement and may or may not travel with the answer: secret persists an opaque sys_secret ref (ADR-0100) rather than the declared value, and color is short by construction — neither has the live refusal these two have.

Reproduce

  • Write-seam table: validateRecord({ fields: { f: { type: T, maxLength: 64 } } }, { f: 'y'.repeat(100) }, 'insert') per type.
  • Physical columns and refusals: packages/drivers/driver-sql/src/sql-driver-11794-richtext-text-family.test.ts with OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL provisioned; its body_sig / body_qr are the controls.

Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    Contributor

    Triage: routed to the decision inbox (needs-user-decision + domain:engine, type Bug — the data-URI refusal at 255 chars is live on the published face, but every repair direction shapes a public contract, which is the manual floor). 四维分析(从业务角度):

    ① 项目长远合理性:#11794 立了不变量「进 TEXT 家族 = 写缝强制 maxLength」。选项 1(写缝加界 + 两类型入 TEXT)缩小特例、declared = enforced;选项 3(裁 maxLength 对这两类型不适用,发布期拒绝 + TEXT)同样闭合、但走 ADR-0049 退役流程;选项 2(varchar(maxLength))让「未声明界的 signature」继续在 255 被拒,且强制与否取决于是否恰好有索引 —— 加深同一声明的方言分叉,最差。
    ② 实际业务拉动:真实存在 —— field-zoo 自己就给 signature 写 data-URI PNG,在 PG(22001)与 MySQL(ER_DATA_TOO_LONG)双双实测被拒。签名/二维码是常见业务字段,这是用户今天就撞的已发布面缺陷。
    ③ 防 AI 写代码犯错:选项 1 的写缝响亮拒绝优于列上限静默截断;「maxLength 声明了却无人兑现」正是声明即须强制的反例 —— 今天 Field.signature({ maxLength: 64 }) 解析、发布、绑定为零。
    ④ 创业阶段不扩散需求:选项 1 只是把两个类型加进既有 string 分支,零新面;选项 3 义务更小(少两个可声明键)但要走退役流程。secret/color 不随裁 —— secret 存的是 sys_secret ref(ADR-0100),color 构造上就短。

    推荐 1(写缝给 signature/qrcode 加 max_length 分支,然后按 #11794 的既定路线入 TEXT 家族);若维护者认为 maxLength 对这两类型本无意义,则 3。⛔ 不推荐 2。
    置信缺口:存量部署里是否有作者真的声明过 signature/qrcode 的 maxLength(census 未跑);两类型入 TEXT 后对 #11374 keyed-and-bounded 规则的交互只在卡内推理、未实测。

    <!-- os-decision-facets -->


    Generated by Claude Code

  2. added theissue type on Aug 25, 2026
  3. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    Maintainer ruling — option 1: the write seam enforces maxLength for signature / qrcode, then both join the TEXT family

    Source: maintainer, 2026-08-25, live PM chat (decision-inbox batch 6 review, session session_01KWRU3s15AJz7PGW7a7wdCh), verbatim: 「其他同意」 — accepting the batch's presented recommendation for this card: option 1.

    Discipline: Clause-② yes (accept/reject behaviour changes in both directions: long values become storable, over-bound values become refused); CONTRACT_REVIEW_TIER.

    State: needs-user-decision → pm:queue in the same write; domain:engine stands.


    Generated by Claude Code

  4. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    Claim: PM loop (domain:engine seat)
    Session: session_01W6HFzyH98W1YaQXhJUJt6o
    Branch: claude/issue-11875-signature-qrcode-bounded-text-family
    Worktree: objectstack-issue-11875
    Domain: domain:engine
    Container & model: M, mode:subagent, CONTRACT_REVIEW_TIER
    Clause-②: yes — both limbs. The maintainer ruling already declared the declaration limb (accept/reject moves in both directions). The path limb fires too, for the reason below.

    ⚠️ The card's premise sentence is stale — read this before starting

    The card states: "maxLength is a plain optional key on FieldSchema (field.zod.ts), admitted on every field type. So Field.signature({ maxLength: 64 }) parses, publishes, and binds nothing."

    That was true when filed (2026-08-24). It is no longer true on origin/main. #11566/#11989 landed after it:

    • packages/spec/src/data/field.zod.ts:123 — BOUNDED_STRING_FIELD_TYPES = text, textarea, email, url, phone, password, markdown, html, richtext, code. Neither signature nor qrcode is a member.
    • packages/spec/src/data/field.zod.ts:1649 — a superRefine that refuses maxLength on any type outside that set.

    So Field.signature({ maxLength: 64 }) is refused at the authoring seam today. The defect's shape has changed: it is no longer "a bound that binds nothing", it is "a bound that cannot be declared at all, on a type whose real values are routinely past 255".

    ⇒ Option 1 therefore requires a packages/spec/src/** edit (adding both types to the set), which is the clause ② path limb. Note the refusal message at :1655-1657 hand-enumerates the ten types in prose — a second copy of the same list that must move with it or it drifts on day one. This is the #12017 family; do not leave it inconsistent.

    The live physical defect the card measured (data-URI signature refused at 255 on PG 22001 / MySQL ER_DATA_TOO_LONG) is unaffected by this and still stands — re-measure it, don't recall it.

    Serial constraints cleared

    Measured by symbol on origin/main (packages/drivers/driver-sql/src/sql-driver.ts, 15,248 lines):

    Prior art to read first, not recall: #11876 landed — richtext/code are already in the TEXT family on main, so the card's "PR #11876 (open)" note is resolved.


    Generated by Claude Code

  5. self-assigned this
    on Aug 25, 2026
  6. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator
    {
      "issue": 11875,
      "status": "done",
      "branch": "claude/issue-11875-signature-qrcode-bounded-text-family",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12119",
      "premise_still_valid": true,
      "summary": "Ruled option 1 implemented at head 5311cee259 (draft PR #12119, base 8450eeacb6): signature/qrcode join BOUNDED_STRING_FIELD_TYPES in field.zod.ts (the claim comment's stale-premise warning re-measured and confirmed on this tree — the #11566 superRefine did refuse maxLength on them, so the spec edit was required; the refusal message now enumerates the set itself instead of a prose copy, and both authoring forms move with it); the record-validator's max_length/min_length branch now reads the spec set instead of a hand-copied ten-type list, so declared bounds on both types refuse loudly with field-named ADR-0112 envelopes; createColumn and its varcharColumnChars mirror move both types from catch-all varchar(255) into the TEXT family under the #11794 invariant. secret/color untouched per the ruling. The 11794 test file's STILL-OPEN half is extended into the closed shape (same data-URI measurement, direction flipped) plus a live keyed-and-bounded case; #12015's 8600-9130 band untouched; PR #12003's four fenced api.json files unchanged after regeneration (only gen:docs was stale). Changeset minor x3 (spec/objectql/driver-sql), not declared-breaking, no ADR-0087 marker owed per the gate's own verdict.",
      "tests": "All at head 5311cee259 (single commit; union derived at it). Live re-measurement on PG 16 + MySQL 8.0.46 STRICT_TRANS_TABLES via information_schema.COLUMNS read by a separate query: post-change body_sig/body_qr = text on both dialects, 1000-char data-URI accepted and round-trips byte-identically, c_color control refused by the server (22001 / ER_DATA_TOO_LONG); keyed signature/qrcode maxLength:64 = varchar(64) in the catalog, 64-char boundary accepted, 65 refused by the server on both dialects (#11374 interaction: enforcement of the declared bound never depends on the index — the write seam holds it everywhere, keyed columns also enforce it physically). Validator boundary both directions in record-validator.test.ts (+10 tests: 100>64 refused max_length {maxLength:64,actual:100}, 64 accepted, 65 refused, update covered, min_length travels, bound-less data-URI accepted, secret/color carve-out controls). Suites: spec 'Test Files 425 passed / Tests 11322 passed'; objectql '232 / 4127 passed'; driver-sql full suite with both live servers under CI's skewed zones 'Test Files 139 passed / Tests 2833 passed | 1 skipped'. Typecheck turbo 14 successful exit 0; repo-wide pnpm lint (eslint . --no-inline-config) exit 0 full run, no narrowing. Gates: dispatch-gates.mjs --repo objectstack-ai/objectstack provenance-stamped at 5311cee259; all 40 matched + 7 convention families ran locally, exits captured pre-pipe, all 0 (adr-0087: 'this PR adds no declared-breaking changeset'; i18n: '9 package(s) — all bundles in sync'; nul-bytes: 'no raw ASCII control bytes'; type-check-debt --re-measure on the fully built closure: 'none above its recorded number'). Ablations from the committed state, predictions written first, mutations proved by anchored grep counts, restores verified by git hash-object == HEAD blob + clean porcelain; both consuming suites import their subject from source (./record-validator.js, ../src/index.js) so no dist rebuild leg applies: write-seam leg (set-read 1->0, hand-list 0->1) = predicted 6 red / 4 green; storage leg (case labels 2->0 each, 2 markers) = predicted-per-test 9 red / 2 green, its red leg reproducing the card's live defect verbatim (PG 'value too long for type character varying(255)', MySQL ER_DATA_TOO_LONG). Storage-leg take 1 was VOID (replacement swallowed the block-opening brace, transform error) — discarded and redone brace-preserving.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12117 (finding label): type-compat.ts CANONICAL_TO_FIELD.text.exact omits signature/qrcode, which this change makes the driver's own emitted TEXT shape for them (binary.exact still lists signature); pre-filing REST issue search unavailable in this session (403 'GitHub access is not enabled') — local grep found no covering card, filed with a dedup note",
        "already filed as #11949 (not re-filed): minLength twin defect (shape + every-type applicability + 3-vs-9 form lists); left an interaction note there — the validator branch now reads BOUNDED_STRING_FIELD_TYPES, so a minLength ruling needs only a spec-side superRefine, and the set is twelve members now"
      ]
    }

    Generated by Claude Code

  7. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    os-dev-report

    (Authoritative copy — the marker on the previous comment was stripped by the body sanitizer, making it invisible to the report scan; same JSON.)

    {
      "issue": 11875,
      "status": "done",
      "branch": "claude/issue-11875-signature-qrcode-bounded-text-family",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12119",
      "premise_still_valid": true,
      "summary": "Ruled option 1 implemented at head 5311cee259 (draft PR #12119, base 8450eeacb6): signature/qrcode join BOUNDED_STRING_FIELD_TYPES in field.zod.ts (the claim comment's stale-premise warning re-measured and confirmed on this tree — the #11566 superRefine did refuse maxLength on them, so the spec edit was required; the refusal message now enumerates the set itself instead of a prose copy, and both authoring forms move with it); the record-validator's max_length/min_length branch now reads the spec set instead of a hand-copied ten-type list, so declared bounds on both types refuse loudly with field-named ADR-0112 envelopes; createColumn and its varcharColumnChars mirror move both types from catch-all varchar(255) into the TEXT family under the #11794 invariant. secret/color untouched per the ruling. The 11794 test file's STILL-OPEN half is extended into the closed shape (same data-URI measurement, direction flipped) plus a live keyed-and-bounded case; #12015's 8600-9130 band untouched; PR #12003's four fenced api.json files unchanged after regeneration (only gen:docs was stale). Changeset minor x3 (spec/objectql/driver-sql), not declared-breaking, no ADR-0087 marker owed per the gate's own verdict.",
      "tests": "All at head 5311cee259 (single commit; union derived at it). Live re-measurement on PG 16 + MySQL 8.0.46 STRICT_TRANS_TABLES via information_schema.COLUMNS read by a separate query: post-change body_sig/body_qr = text on both dialects, 1000-char data-URI accepted and round-trips byte-identically, c_color control refused by the server (22001 / ER_DATA_TOO_LONG); keyed signature/qrcode maxLength:64 = varchar(64) in the catalog, 64-char boundary accepted, 65 refused by the server on both dialects (#11374 interaction: enforcement of the declared bound never depends on the index — the write seam holds it everywhere, keyed columns also enforce it physically). Validator boundary both directions in record-validator.test.ts (+10 tests: 100-char vs 64 refused max_length {maxLength:64,actual:100}, 64 accepted, 65 refused, update covered, min_length travels, bound-less data-URI accepted, secret/color carve-out controls). Suites: spec 'Test Files 425 passed / Tests 11322 passed'; objectql '232 / 4127 passed'; driver-sql full suite with both live servers under CI's skewed zones 'Test Files 139 passed / Tests 2833 passed | 1 skipped'. Typecheck turbo 14 successful exit 0; repo-wide pnpm lint (eslint . --no-inline-config) exit 0 full run, no narrowing. Gates: dispatch-gates.mjs --repo objectstack-ai/objectstack provenance-stamped at 5311cee259; all 40 matched + 7 convention families ran locally, exits captured pre-pipe, all 0 (adr-0087: 'this PR adds no declared-breaking changeset'; i18n: '9 package(s) — all bundles in sync'; nul-bytes: 'no raw ASCII control bytes'; type-check-debt --re-measure on the fully built closure: 'none above its recorded number'). Ablations from the committed state, predictions written first, mutations proved by anchored grep counts, restores verified by git hash-object == HEAD blob + clean porcelain; both consuming suites import their subject from source (./record-validator.js, ../src/index.js) so no dist rebuild leg applies: write-seam leg (set-read 1 to 0, hand-list 0 to 1) = predicted 6 red / 4 green; storage leg (case labels 2 to 0 each, 2 markers) = predicted-per-test 9 red / 2 green, its red leg reproducing the card's live defect verbatim (PG 'value too long for type character varying(255)', MySQL ER_DATA_TOO_LONG). Storage-leg take 1 was VOID (replacement swallowed the block-opening brace, transform error) — discarded and redone brace-preserving.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12117 (finding label): type-compat.ts CANONICAL_TO_FIELD.text.exact omits signature/qrcode, which this change makes the driver's own emitted TEXT shape for them (binary.exact still lists signature); pre-filing REST issue search unavailable in this session (403 'GitHub access is not enabled') — local grep found no covering card, filed with a dedup note",
        "already filed as #11949 (not re-filed): minLength twin defect (shape + every-type applicability + 3-vs-9 form lists); left an interaction note there — the validator branch now reads BOUNDED_STRING_FIELD_TYPES, so a minLength ruling needs only a spec-side superRefine, and the set is twelve members now"
      ]
    }

    Generated by Claude Code

  8. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    ACCEPT — PR #12119. Reviewed at CONTRACT_REVIEW_TIER, so the accept/reject transitions were held to the bar rather than taken on the report's word.

    The stale premise I flagged at dispatch was real, and the fix went one better than asked

    I warned that the card's "maxLength is a plain optional key admitted on every field type" had been retired by #11566/#11989. The dev re-measured on its own tree and confirmed it: the superRefine did refuse maxLength on both types, so the spec edit was required, not optional. Verified on the branch:

    BOUNDED_STRING_FIELD_TYPES = text, textarea, email, url, phone, password,
                                 markdown, html, richtext, code, signature, qrcode
    

    12 members. secret and color correctly absent — the ruling's carve-outs hold.

    The second hand-maintained copy I flagged is gone by construction, not by being updated. The refusal message used to spell the ten types in prose; it now derives them:

    `${[...BOUNDED_STRING_FIELD_TYPES].map((t) => `'${t}'`).join(', ')} — `

    And the write seam did the same thing — record-validator.ts replaced a ten-way || chain with BOUNDED_STRING_FIELD_TYPES.has(t). So two hand-copies became one shared constant. That is the #12017 family's defect fixed a layer down as a side effect of this card, and it is the reason #12017 was serial-queued behind this one rather than run alongside it.

    Both declared fences held — measured, not taken on trust

    fence result
    #12015's band 8600–9200 of sql-driver.ts untouched — every hunk is at 13485 / 13795 / 13813 / 13823 / 13980
    #11722's band 10100–10560 untouched
    PR #12003's four packages/spec/*/api.json 0 files changed ✅

    (The 13980 hunk is a modest overrun of the band I declared, into territory no card holds. Noted, not a breach.)

    Clause ② — the transitions, both directions, on live servers

    Read from information_schema.COLUMNS by a separate query rather than from emitted DDL, on PG 16 and MySQL 8.0.46 STRICT_TRANS_TABLES:

    • Widening: body_sig / body_qr read back as text on both dialects; a 1000-char data URI is accepted and round-trips byte-identically — where the card measured 22001 / ER_DATA_TOO_LONG before.
    • Narrowing: a declared maxLength: 64 refuses at 65 and accepts at exactly 64, on insert and update, with a field-named ADR-0112 max_length envelope.
    • The control fires: c_color is still refused by the server, so the widening is specific to the two types and not an artifact of the harness.

    The #11374 interaction I fenced at dispatch was measured and came out clean: a keyed, bounded signature is emitted varchar(64) and the server refuses 65 — so enforcement of the declared bound never depends on whether an index happens to key the column. That dialect/shape-divergent enforcement was the specific hazard that could have made this fix worse than the defect, and it does not survive.

    Both authoring forms and the docs page moved with the set. Changeset minor ×3, correctly graded.

    Two process points handled the way I want them handled

    1. A void ablation leg was discarded and redone, not salvaged. The storage leg's first take swallowed a block-opening brace and produced a transform error; it was thrown out and rerun brace-preserving. The red leg then reproduced the card's live defect verbatim (value too long for type character varying(255) / ER_DATA_TOO_LONG).
    2. The sanitizer trap fired for real. The HTML-comment report marker was stripped from comment 5408667003; the dev noticed, re-posted with a literal marker, and verified the marker survived. This is the hazard my seat post warns about — reading back only the marker is not verification — and it was handled correctly rather than left as a silently truncated report.

    One gap I found in review, filed rather than used to hold the PR

    The repair reaches new tables. On a pre-existing table, schema-drift.ts's varchar differ requires declaredMaxLength !== undefined, so an undeclared signature / qrcode column stays varchar(255) and no drift op reports it at all — while a declared-bound one correctly gets widen_varchar. The undeclared case is the card's own motivating example (field-zoo's data-URI PNG), so an existing deployment sees no change and no diagnostic.

    ⛔ Not a defect in this PR, which declares the "existing tables stay varchar(255)" boundary honestly in its changeset and delivers all three ruled seams. Filed as #12121 so the remaining half is not rediscovered.

    Also carried out of this card: #12117 (type-compat.ts's text.exact omits both types, while binary.exact still lists signature). I checked the blast radius independently — the only non-test consumer of isCompatible / suggestFieldTypeForSqlType is service-datasource/src/external-datasource-service.ts, the external-column import path, so there is no managed-table drift impact and filing rather than widening this PR was the right call. Its dedup note says the filing session's issue search returned 403; I re-ran it here and the channel answered with a firing control, so the "no covering card" reading is live.

    Flipping to ready and enqueueing once CI converges.


    Generated by Claude Code

  9. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    LANDED — PR #12119 merged as a11c1a57d6.

    Verified by content on origin/main: 'signature', 'qrcode' present in BOUNDED_STRING_FIELD_TYPES → 1 (0 before). Control: the set declaration itself → 1, so the probe fires.

    All three ruled seams are in:

    The #11374 keyed-and-bounded interaction came out clean — the hazard that could have made this fix worse than the defect. A keyed, bounded signature is emitted varchar(64) and the server refuses 65, so enforcement of the declared bound never depends on whether an index happens to key the column.

    Two hand-maintained copies were removed rather than updated, which was not asked for and is the most valuable part of the diff: record-validator's ten-way || chain and field.zod.ts's prose enumeration in the refusal message both now read BOUNDED_STRING_FIELD_TYPES. That is the #12017 family closed one layer down — and #12017 has been re-scoped accordingly, since only the driver half remains.

    secret and color stayed out, per the ruling.

    Two follow-ups carried out of this card, neither a defect in it:

    Closing as completed; pm:dispatched stripped in the same stroke.


    Generated by Claude Code

  10. added a commit that references this issue on Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions