Skip to content

fix(objectql)!: refuse a field whose type is absent or not a FieldType member at the registration door (#16319) - #17444

Merged
os-sam merged 9 commits into
mainfrom
claude/issue-16319-field-type-refused-at-registry
Sep 10, 2026
Merged

os-sam merged 9 commits into
mainfrom
claude/issue-16319-field-type-refused-at-registry

Conversation

@claude

@claude claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #16319

Clause-②: yes

Ruled by the dispatching seat and posted as a corrected claim: #16319 (comment) — same session, same branch, same assignee, so the original claim stays governing and this PR posts no second one. needs:contract-review is hung on both carriers (node scripts/pm/check-clause2-carriers.mjs --pair 17444 exit 0); the seat clears it in the same stroke as the review PASS.

A field declaration whose type is absent, or is not a FieldType member, is now refused at the registration door, and the whole object declaration goes with it. Every downstream family default the card measured becomes a loud refusal instead of a guess.

Maintainer ruling, 2026-09-10 (director seat batch #111 item 2), verbatim: 「16319 一个没写 type(或拼错)的字段 应该禁止加载。这个才是合理的吧?其他同意」 — implemented per the director seat's derived shape in that comment, not per this PR's summary of it.

What was wrong

One declaration produced two different columns. Re-derived by symbol on this branch's own head; the live PostgreSQL 16.13 table is the card's:

declaration driver os generate migration --format sql --format ts
{ maxLength: 100 }, no type character varying(100) TEXT TEXT
{ type: 'this_is_not_a_field_type', maxLength: 100 } character varying(255) TEXT TEXT

SqlDriver.createColumn read field.type \|\| 'string', which heads its STRING-family arm and sizes the column from the declared maxLength. All four os generate loops read String(fieldDef.type \|\| 'text'), which heads the TEXT family. Both directions of harm are in row 1: the platform refuses a 101-character value both generated tables accept.

What this does

  1. The single point of closure is SchemaRegistry.registerObject. It refuses the WHOLE object declaration with the ADR-0112 envelope (INVALID_METADATA + 422), naming the object, the field and the reason, with the spec's own "did you mean?" for a mis-spelling. All three contributor kinds (own / overlay / extend) are judged — ObjectSchema.fields and ObjectExtensionSchema.fields are both z.record(z.string(), FieldSchema). ⛔ The field is never dropped on its own.
  2. loadMetaFromDb's startup policy is revised for this class. 「Registered anyway so it stays serveable and fixable」 no longer applies: the row does not register, and the log states the consequence and the fix once, at error. It is suppressed only for this class, through the door's OWN predicate — the boot log never grows a second opinion about what the registry admits.
  3. Downstream guesses become refusals. createColumn refuses a field with no type; all four os generate loops refuse an absent or non-member type and generate nothing for that object. fieldTypeToSql's docblock is rewritten in the same stroke.
  4. Changeset carries the BREAKING note and its ADR-0087 disposition.

The shared vocabulary lives in @objectstack/metadata-core (object-field-type.ts) for the ordinary reason: the enforcing door is objectql's and the reporting seam is metadata-protocol's, and objectql depends on metadata-protocol, so the reverse import is a cycle turbo refuses.

The ruling's implementer PRECONDITION — measured, not argued

若 Studio 的元数据列表只从注册表读,被拒的行会从界面消失而无法修 —— 那时停手回报

It does not. getMetaItems ALWAYS consults sys_metadata and merges its rows in; it never serves the registry alone. Pinned in registry-field-type-refused-at-door.test.ts §4: a refused row is still listed, still served with the offending field visible, still accepts a corrected write (which then loads on the next boot), and is still deletable. deleteMetaItem gets more permissive, not less: with nothing registered, isArtifactBacked is false and the DB-only branch applies (object is allowRuntimeCreate: true). No stop condition fired.

Clause-② — RESOLVED: noyes

INVALID_METADATA is already registered under @objectstack/objectql in ERROR_CODE_LEDGER (4 hits) and this PR touches no ledger file; the driver reuses the standard-catalog VALIDATION_ERROR. ⛔ No new error code is minted, so the ruling's own stated flip condition did not fire.

⭐ The declaration flips for a reason the ruling never anticipated. This PR adds new exported symbols to @objectstack/metadata-corefindUndeclarableFieldType, describeUndeclarableFieldType, isDeclarableFieldType, isObjectFieldTypeRefused, OBJECT_FIELD_TYPE_REFUSED_ERROR_NAME, plus three types — and contract-review's mechanical floor is 「新导出符号…恒 yes」. The dispatching seat re-measured rather than accepting the reading: all five read 0 on origin/main d57611dfd3 with index.ts itself as the firing control. Both conditions had to be checked; only one fired, and it was not the written-down one.

This PR raised it rather than rewriting the declaration, because the declaration is the claiming seat's act. The seat has now corrected it.

@objectstack/objectql's own public surface is deliberately unchanged: the new names are not re-exported from its index, and the error CLASS stays unexported — the #14936 dual-realm convention, where a consumer holding the other realm's copy gets instanceof === false silently, so consumers discriminate on the error's name. ⛔ No changeset regrade is owed: all five moved packages/**/src/** packages are graded minor.

Acceptance

  • ✅ The card's two-row PostgreSQL table becomes two refusals — at the registry, so the driver is never reached (registry-field-type-refused-at-door.test.ts §1–2).
  • ✅ The in-repo field scan stays green: 1341 fields / 116 objects / 111 modules loaded, 0 undeclarable. Control fired (sys_file.id type='text', sys_file.key type='text', sys_file.name type='text'), so the zero is a reading, not an empty walk. The one module that fails to load is create-objectstack's blank template, which declares through Field.text() / Field.textarea().
  • ✅ A seeded typeless sys_metadata row leaves the object out of the registry, is named at error, and stays deletable.
  • Ablation — see below.

Ablation

Fix committed first; mutation proved on disk (git hash-object before/after) and proved to reach dist/ (scripts/ablation-dist-preflight.mjs, marker present in 4 built files); probe imports resolve entirely through package exports.

leg registry, row 1 (no type) registry, row 2 (non-member) control (email)
fix in place absent (loaded=0 errors=1) absent (loaded=0 errors=1) present
door removed present (loaded=1 errors=0) present (loaded=1 errors=0) present

With the door removed the object reappears and, driven into the driver, row 2 falls back to varchar(255) — the ruling's own predicted ablation, observed. Row 1 is refused by the driver's own defence instead, which is stronger than the ablation predicted; reported as observed rather than as the template's expectation. A second leg ablated the same statement and ran the new suite: 6 of 12 tests go red, the six that do not being the positive controls and the non-throwing seam test.

Restore verified whole-tree, not per path: git status --porcelain empty, git hash-object equals the HEAD blob, --absent preflight exit 0.

Scope — the driver's defence is narrower than the door, deliberately (⛔ not a dropped half)

⭐ First, so the commit title narrow the driver refusal to an absent type cannot be read as half the ruling being dropped: BOTH shapes — absent AND non-member — are refused. They are refused at the REGISTRATION DOOR, for the whole object, which is the single point of closure the ruling asked for and what its acceptance list means by 「驱动永远到不了」. What is narrower is only the driver's own defence-in-depth layer, which the ruling itself describes as 「按构造它们应当不可达,拒绝是防御」.

SqlDriver.createColumn refuses type ABSENCE (the \|\| 'string' default the ruling names), not FieldType MEMBERSHIP. Membership is refused for the whole object at the registration door, which fronts every route into syncSchema — which is what the acceptance list's 「驱动永远到不了」 means. Measured reason for not going further here: driver-sql's own corpus declares 388 non-member spellings across ~100 files ('string' 361, 'integer' 17, 'auto_number' 5, 'varchar' 4, 'object' 1) that drive initObjects directly and never touch the registry, and 'string' is a declared case arm of that switch whose column shape differs from every member's — so closing that half is a corpus migration with column consequences, not a spelling fix. A pin holds the boundary in both directions so it cannot move silently. Control for that census: type: 'text' reads 164 and type: 'datetime' reads 60 in the same corpus.

Verification

pnpm build (whole repo) · pnpm lint in its exact spelling (node --stack-size=4000 … . --no-inline-config) exit 0 · --format json over the same population: 6556 files, 0 errors, 0 warnings, and all 15 changed files are inside it — a whole-tree reading, not a narrowing. All at a05a28520a.

  • @objectstack/objectql 4959 passed / 296 files · @objectstack/metadata-protocol 2487 passed · @objectstack/driver-sql 2495 passed · @objectstack/metadata-core 272 passed · @objectstack/cli --project unit 2678 passed (the integration tier is declared to CI: the diff touches no spawn entry point or driver/kernel boot path).
  • typecheck green on all five packages, and the test-typecheck-debt ledger shrank — see below.
  • 69 of 69 derived gate families run, all green (node scripts/pm/dispatch-gates.mjs --commands, reconciled with --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN). Two needed a whole-repo pnpm build before they could measure at all (check:dual-build-cjs-loads, check:i18n-coverage — both exit 3 = PREREQUISITE NOT MET, never a pass) and are green after it. check:query-options-erasure went red on the new test files and is fixed, not waived. check:type-check-debt needed the CI-shaped heap ceiling; at 4096 MB its own tsc re-measure OOMs and it exits 3.

⭐ The type-check debt ledger shrank, and WHY is the best finding in this round

packages/objectql/test-typecheck-debt.json goes 44 → 40 files / 242 → 234 errors, and the four deletions are not tidying: they are the defect this card closes, seen from the compiler's side.

Those four fixtures declared type: 'longtext' as const (three of them, on a sys_metadata metadata field) and type: 'id' / type: 'string' (the fourth) — five spellings the FieldType enum has never contained. The as const / as casts they needed to get past tsc WERE the recorded errors. The debt was a symptom. Once the registration door refuses such a declaration outright, the fixtures had to be corrected to the members the platform's own sys_metadata object actually uses (Field.textarea / Field.text), the casts stopped straining, and the entries graduated — which the ledger's own header requires be deleted in the same change.

⛔ Nothing was raised to absorb an error: this is the shrink direction throughout. The ledger's _note is amended so its seed counts ("242 errors across 44 files", measured at 2a181174a6) are not read as this file's current totals.

Acceptance notes

Noted, not filed:

  • createColumn's case 'string': arm and varcharColumnChars' matching arm are now unreachable from any registry-fronted path — 'string' is not a FieldType member, and [finding] sql-driver.ts twice cites a Field.string builder that has never existed — and the prose demonstrably steered a ruling onto an unauthorable target #12593 already measured that. Dead-code removal, out of scope here.
  • sql-driver.ts carries 11 declaration-side 'string' defaults, not the 7 the dispatch measured or the 1 the card implies: \|\| 'string' at :2411 :9976 :10054 :11951 :16189 :16555 and ?? 'string' at :10686 :10891 :14753 :14760 :14818; two further \|\| 'string' at :17726 :17728 default an INTROSPECTED column's type and are a different question. The dispatch's list of 7 missed :17726 and did not separate the ?? spelling. Only :16555 (createColumn) is the ruled default; the rest are comparison classes, coercion-registry fills, candidate sets and error text, all downstream of the same closed door. schema-drift.ts carries 4 more (:549 :1066 :1282 :1297).
  • os generate types' asymmetry between the two shapes (a typeless field emitted string, a mis-spelled one unknown) is retired by construction — both now refuse — so the domain:cli seat's earlier note on it needs no card.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU


Generated by Claude Code

…ctql fixtures

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…e two downstream defences

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…-type class

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…ratchet holds

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…e triage graduated

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/driver-sql, @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/objectql, touching 33 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/objectql/src/index.ts, packages/objectql/test-typecheck-debt.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e758131b3900eb13260f03643e295ca6d625c42b.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/objectql/src/index.ts, packages/objectql/test-typecheck-debt.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 35 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 45 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e758131b3900eb13260f03643e295ca6d625c42bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from ddf89d6de8ac6c4157083773f10213d5189bb12b — the merge of head 4be5d181258262cff3033caf829a42256c83c760 into base e758131b3900eb13260f03643e295ca6d625c42b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ddf89d6de8ac6c4157083773f10213d5189bb12b && git checkout ddf89d6de8ac6c4157083773f10213d5189bb12b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e758131b3900eb13260f03643e295ca6d625c42b 4be5d181258262cff3033caf829a42256c83c760 && git checkout -B drift-repro e758131b3900eb13260f03643e295ca6d625c42b && git merge --no-ff 4be5d181258262cff3033caf829a42256c83c760

node scripts/docs-audit/affected-docs.mjs --json e758131b3900eb13260f03643e295ca6d625c42b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e758131b3900eb13260f03643e295ca6d625c42b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… member the door admits

`CROSS_FIELD_OBJECT_FIELDS` declared `stage` and `owner` as `type: 'string'`.
`string` is absent from `FieldType`, so this PR's own registration door refused
the whole `cross_field_deal` object and the runtime suite's `beforeAll` threw
before its subject ran — CI shard `Test Core (3/6)`, 1 failed | 250 passed.

`text` is not a re-typing. `canonicalizeSqlType('varchar(255)')` is `'text'` and
`suggestFieldTypeForSqlType('varchar(255)')` is `'text'`, both pinned in
`spec/data/type-compat.test.ts`, so `text` is the spelling of the column
`string` was already producing. Matches `id` / `organization_id` in the same
map and the text columns of `CROSS_FIELD_OFFSET_OBJECT_FIELDS` below it.

The emitted column does move `varchar(255)` -> `TEXT` (measured on sqlite-wasm),
which is inert here: no index keys either column, `initObjects` is passed no
indexes, and the longest value in the corpus is four characters. All four
`initObjects` consumers stay green.

A static census over the tree — `fields:` regions resolved through cross-package
imports, classified as ObjectStack field maps, `type` checked against the
`FieldType` union read out of `field.zod.ts` — reports these two as the only
non-member declarations reaching a `registerObject` door. After this change that
count is zero; the 108 that remain all reach `driver.initObjects` only, which the
changeset's scope section deliberately leaves alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
@os-sam
os-sam marked this pull request as ready for review September 10, 2026 16:09
@os-sam
os-sam enabled auto-merge September 10, 2026 16:10
@os-sam
os-sam added this pull request to the merge queue Sep 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 34500588865 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-acting-organization.dogfood.test.ts > dogfood [sqlite-wasm]: a scheduled run executes as its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-acting-organization.dogfood.test.ts > dogfood [memory]: a scheduled run executes as its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    
  • Dogfood Regression Gate (2/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-sweep-organization-scope.dogfood.test.ts > dogfood [sqlite-wasm]: a time-relative sweep selects inside its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/dogfood:test:  FAIL   isolated  test/schedule-sweep-organization-scope.dogfood.test.ts > dogfood [memory]: a time-relative sweep selects inside its declared organization (#16659)
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • test/schedule-acting-organization.dogfood.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • test/schedule-sweep-organization-scope.dogfood.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 0 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

…lean`

`packages/qa/dogfood/test/fixtures/schedule-organization-fixture.ts` declared
`touched: { type: 'checkbox' }`. `checkbox` is not a `FieldType` member — the
enum spells the multi-value checkbox GROUP `checkboxes` and the scalar flag
`boolean` — so this PR's registration door refuses the whole `sched_org_target`
declaration and both #16659 pins fail at `bootStack` on `[sqlite-wasm]` and
`[memory]`.

Neither side is wrong alone. The declaration landed in ecdfc94 (#17334),
after this branch's merge base `d57611dfd3`, where nothing refused it; the door
is this card's deliverable. The merge queue tests the PR merged into current
`main`, which is the first place the two meet — so this is the merge, not a
defect in either diff.

`boolean` and not `checkboxes`: the fixture's `update_record` node writes
`touched: true`, and both pins read it back as a scalar
(`r.touched === true || r.touched === 1`, `Boolean(r.touched)`). `checkboxes`
stores a JSON array of option values, whose EMPTY set is truthy — the sweep
pin's differential would then report rows nothing touched as touched. The
member is chosen from the enum's own grouping and from what the tests assert,
not from the refusal message's Levenshtein suggestion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
@os-sam
os-sam added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 2bed4c3 Sep 10, 2026
35 checks passed
@os-sam
os-sam deleted the claude/issue-16319-field-type-refused-at-registry branch September 10, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants