Skip to content

fix(runtime): AppPlugin registers the forward-converted copy of every collection - #23024

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22994-app-plugin-converted-collections
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22994-app-plugin-converted-collections

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22994
Clause-②: no

Door 2 only, as triage graded it. Door 1 (the conversion window in packages/metadata-core) is #23001, untouched here.

What changes

AppPlugin.init() handed the manifest service the RAW bundle. That service decomposes it into the ObjectQL registry over METADATA_ARRAY_KEYS. The registry is the code layer of the flattened list that GET /api/v1/meta/TYPE serves and that getMetaItemsForExecution hands to the email template loader. On the artifact boot, the door (MetadataPlugin) registered the forward-converted copy into the metadata service. So the item read served the converted copy while the list served the raw one.

Now the boot runs ONE ADR-0087 forward conversion (forwardConverted, memoized, packages/runtime/src/app-plugin.ts):

Why this input and not this.bundle:

Measured, before and after

The card's door: os dev -a dist/objectstack.json --fresh on an undeclared-engines artifact (objectstack-ai/objectstack at BASE 6a7eca6868, then at this branch), read over HTTP.

The second type is dashboard, converted by the major-18 retired entry dashboard-refresh-interval-to-refresh-interval-seconds (retiredAfter 17.3.0).

read before after
GET /api/v1/meta/dashboard, ops_board refreshInterval: 300, _diagnostics.valid: false (the tombstone's rename prescription) refreshIntervalSeconds: 300, valid: true
GET /api/v1/meta/dashboard/ops_board refreshIntervalSeconds: 300, valid: true refreshIntervalSeconds: 300, valid: true (the same document as the list)
GET /api/v1/meta/email_template, probe.welcome carries category / isSystem, valid: true unchanged
GET /api/v1/meta/email_template/probe.welcome the door's parsed copy, valid: true unchanged

On this tree category / isSystem are live keys: their retirement is #22990, which is parked on this card. So the email half was measured on #22990's head e4e96505e0 with and without this patch, in a scratch tree, through the real createRegistryEmailTemplateLoader over the booted kernel's protocol service:

  • Without the patch:
    • the list carries category: custom, isSystem: false with _diagnostics.valid: false;
    • the item read is clean;
    • the loader refuses: "email template 'probe.welcome' (en-US) is not a valid email template, so it cannot be sent: email_template.category was removed…".
  • With the patch:
    • the list is valid with both keys gone;
    • the loader sends { name: probe.welcome, locale: en-US, subject: Welcome, body_html, active: true }.

The list and the item still differ on the PARSE axis: the door's copy carries the email schema's variables: [] / active: true defaults. That is route ownership (#12892), not this conversion. The pins assert "every key the list serves equals the item's", never "one document", except for the dashboard, where the two really are one document.

Tests

packages/runtime/src/app-plugin-converted-collections.test.ts:

  • A real createStandaloneStack boot over the card's artifact. Four checks:
    • the dashboard list equals the item read, valid: true;
    • the dashboard execution face equals the conversion's own output;
    • every key of the email list equals the item read's, valid: true;
    • the email execution face equals the conversion's output and passes the loader's own predicate, EmailTemplateDefinitionSchema.safeParse(stripReadDecorations(item)).
  • A control boot of the same app in current spellings: the registry holds the authored items plus stamps, nothing rewritten.
  • Three unit legs:
    • one conversion: the security registrar registers the very object the manifest payload carries (identity, not equality);
    • a current bundle is handed over with its collection arrays by reference;
    • a packages[] bundle is handed over as authored.

The email legs are positive controls on this tree. Measured on #22990's head: this file is 8/8 green with the patch and 5 red without it, both email legs included. So they become live pins the day that retirement lands, with no edit.

Ablation of the one conversion line in init(), run through scripts/ablation-replace.mjs:

All on 86d2782f09:

#14491's measurement: five conversion-axis pins rewritten (seat ruling A, 6122651929 → claim 6121395708 widened)

packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts now pins the converged conversion axis. None of the five pins is skipped, relaxed or deleted; each keeps its subject and asserts the measured converged values (commit 0f6556d1c5):

  1. permission probe_agent: the registry grant map and RLS entry carry no allowRestore / allowPurge / priority. The three door defaults are still absent (the parse axis).
  2. The stored sys_permission_set RLS equals the door's.
  3. share_legacy_deals carries sharedWith.type: position and accessLevel: edit in the registry, equal to the door's.
  4. share_legacy_level carries accessLevel: edit in the registry.
  5. An authorization outcome. share_legacy_deals is now SEEDED, exactly once, as a position share: recipient_type: position, recipient_id: sales_mgr, access_level: edit. That gives three rows where there were two. Before this PR it was dropped at seeding (role is refused by mapRecipientType). The changeset says so.

The file's header now records that #22994 closed the conversion axis, while the parse axis (string condition, and the missing scope / isDefault / active defaults) stays #14491's. Those pins are unchanged.

Ablating the init() conversion line turns exactly the five rewritten pins red against the raw registry copy (5 failed | 8 passed). The restore is proven against HEAD.

Open PR #22962 (#15204 stage 8) edits the same file and deletes pin 2; whichever lands second merges main. The seat declared this on #15204.

Acceptance notes (not this PR)

  • The conversion layer does not reach option-B package bodies. Measured: the artifact door (_parseAndRegisterArtifact) refuses an artifact whose only dashboard sits in packages[0].manifest.dashboards with refreshInterval, with "invalid_type at packages.0.manifest.dashboards.0.refreshInterval". The conversion walks a definition's own collections. Same family as ADR-0087 window: a ^18 artifact built during the v18 prerelease gets no D2 replay of an entry retired after it was built, so it fails to boot (door 1, split from #22994) #23001, for the seat to route.
  • Readers of the raw this.bundle / this.collections after the seam, answered for assumption 5. Each reader takes one of three answers: it needs the converted copy, it keeps the raw one, or it does not care.
    • Datasources (codeDefinedDatasourceOwners, the in-memory datasource registration, connectDeclared): raw. The drivers read config.timeoutMs / persistence.autoSaveIntervalMs, so a datasource still carrying the retired config.timeout (turso) or autoSaveInterval (memory) loses the value at connect.
    • Jobs (scheduleAppArtifactJobs(ctx, this.bundle)): raw. The binder reads job.timeoutMs, so a retired jobs[].timeout schedules with no timeout.
    • These two are retired keys a consumer reads. Reported, not converted here, as the dispatch ruled.
    • Does not care:
      • datasourceMapping, objects passed to connectDeclared (read for name / datasource only), seed data: no conversion targets what they read;
      • hooks: the binder reads no timeout / timeoutMs;
      • emitCatalogEvent, grantedPermissions: no conversion touches them.
    • Translations (loadTranslations): NOT MEASURED.
    • Residual of a multi-package config stack (registerUnclaimedTopLevel): raw, same class.

Generated by Claude Code

… collection

AppPlugin handed the manifest service the RAW bundle, so the ObjectQL
registry (the code layer GET /meta/<type> lists and the email loader reads)
held items with retired keys while the artifact door's item read served the
converted copy. The boot now runs ONE ADR-0087 forward conversion over
{ ...manifest, ...collections } after the D1 handshake, memoized, and both
the manifest registration and the security block take it.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
…beside a current-spelling control

The list read, the item read and the execution face of a real
createStandaloneStack boot over an undeclared-engines artifact, for a
dashboard (dashboard-refresh-interval-to-refresh-interval-seconds) and an
email template, plus a control artifact in the current spellings and three
unit legs: one conversion shared by both registrars, a current bundle handed
over by reference, and a packages[] bundle handed over as authored.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 4 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/services-checklist.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/permissions/capabilities.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/permissions/system-context.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/plugins/index.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/lifecycle.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/plugin-spec.mdx (via AppPlugin (symbol, a top-level class))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AppPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via AppPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 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 9432bac077ecbfa703c09409f3fed416edd41d99 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f9ef3f7951b312263e75a1b08a3c723fe218d4ff — the merge of head 0f6556d1c5c1cc53f73896f494dff140be5a0180 into base 9432bac077ecbfa703c09409f3fed416edd41d99, 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 f9ef3f7951b312263e75a1b08a3c723fe218d4ff && git checkout f9ef3f7951b312263e75a1b08a3c723fe218d4ff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9432bac077ecbfa703c09409f3fed416edd41d99 0f6556d1c5c1cc53f73896f494dff140be5a0180 && git checkout -B drift-repro 9432bac077ecbfa703c09409f3fed416edd41d99 && git merge --no-ff 0f6556d1c5c1cc53f73896f494dff140be5a0180

node scripts/docs-audit/affected-docs.mjs --json 9432bac077ecbfa703c09409f3fed416edd41d99

⚠️ 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 9432bac077ecbfa703c09409f3fed416edd41d99 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ion axis

The five pins that measured the registry copy against the door's on the
ADR-0087 conversion axis now pin the converged state the forward-converted
registry copy produces: the grant map and RLS entry without the retired keys,
the converted sharing-rule recipient and access level, and the legacy role
share seeded once as a position share. The parse-axis pins are unchanged.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0f6556d1c5c1cc53f73896f494dff140be5a0180
Local-runs: none

Inputs: card #22994 (body, triage 6121248027, claim 6121395708 as edited, reports 6122651929 / 6123394404, the ACCEPT 6123477244, read as a claim); PR #23024 body, file list and the net diff against merge base 6a7eca6868 (4 files, +514 / -54); the 45 check-runs on this head (latest per name: 34 names, all concluded at the time of writing, 29 success, 5 skipped — Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke opt-in — 0 failed; Check Changeset was in progress on the first read and concluded success on the second); #14491 body and pointer 6122725038; #15204 comment 6122715417. Governing rules: AGENTS.md Prime Directives #10 and #12, Post-Task Checklist step 3, the clause ② definition in scripts/check-changeset-no-major.mjs ("a new key on a published payload") and the level paragraph in .github/workflows/pr-automation.yml ("a purely additive widening of a published package's public surface ... takes at least minor"). Code read at the head: app-plugin.ts, artifact-collections.ts, metadata-core/src/artifact-forward-conversion.ts, spec/src/conversions/{apply,registry}.ts, plugin-sharing/src/bootstrap-declared-sharing-rules.ts, plugin-security/src/bootstrap-declared-permissions.ts, both test files.

① Derived judgments

1. What the ObjectQL registry now holds, and every reader that moves with it — RIGHT. init() hands manifest.register this.forwardConverted(ctx).definition whenever this.bundle.packages === undefined. forwardConverted memoizes one applyArtifactForwardConversions over collections.manifest ? { ...manifest, ...collections } : collections, with collections = resolveArtifactCollections(this.bundle), which returns its argument by identity when packages is absent (artifact-collections.ts, the D7 branch). So the input is byte for byte the old servicePayload, and the registry decomposed over METADATA_ARRAY_KEYS now carries the converted copy of every collection. Readers that change, all through that one code layer: readFlattenedMetaItems on the served face (GET /api/v1/meta/TYPE, protocol.ts:8786) and the execution face (getMetaItemsForExecution, :8814), the email loader (which safe-parses the execution face), readDeclared(ql, 'permission') in the permission seeder, readDeclared(engine, 'sharing_rule') in the sharing seeder, and readDeclaredCapabilityContext. Every one moves toward the converted shape the item endpoint already serves on an artifact boot: on the conversion axis the list and item are now equal (the dashboard pin asserts one document); on the parse axis nothing moves (the registry copy still lacks the door's schema defaults — the email pin asserts subset, not equality, and the #14491 parse-axis pins are untouched). _diagnostics.valid flips false to true only for items the conversion rewrote, i.e. toward the item read's answer. No public answer moves in any other direction. The one reader class that now DIVERGES from the registry is AppPlugin's own raw readers (jobs, datasources, translations, registerUnclaimedTopLevel) — they read raw before and after, so nothing regresses, and #23029 carries them. RIGHT.

2. An already-current bundle registers byte-identically — RIGHT. applyConversions is copy-on-write per entry (each entry returns its input when it matches nothing), the two default flips are excluded by DEFAULT_FLIPS_NOT_REPLAYED_HERE, and resolveArtifactCollections is identity without packages. The control pin (a current bundle is handed over unchanged) asserts toStrictEqual({ ...manifest, ...bundle }) and reference identity of the dashboards / emailTemplates arrays; the control boot asserts the registry holds exactly the authored items plus stamps. Test Core is green on this head. Note that this is the first time a defineStack() config boot (os serve, DevPlugin, verify's bootStack, embedders) replays the retired conversions over its NON-security collections; #12844 already ran the identical function on the identical bytes for the security collections of that boot, and the door runs it on every artifact boot, so no new policy is introduced — the same function, the same window verdicts. RIGHT.

3. The authorization outcome — RIGHT, and Clause-②: no is the right line. What I checked:

  • The conversion that produces it is a declared ADR-0087 entry: sharing-recipient-role-to-position (registry.ts:357, protocol 13, ADR-0090 D3, retiredFromLoadPath: true, retiredAfter: '15.0.0', "one-step rename, no alias window"), and the level is sharing-rule-access-level-full-to-edit (registry.ts:878, protocol 17, documented "strictly lossless: full never granted more than edit"). ADR-0087's chain record (step 13) names both. The [finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy of permissions / capabilities / sharingRules in the ObjectQL SchemaRegistry (AppPlugin.init → manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 fixture declares an engines.protocol range of greater-or-equal 17.1.0; floor 17.1.0 is below the 17.7.0 label, so the verdict is converted-forward and every retired entry replays — the same verdict the door already reached for the same bytes, which is why the pre-PR pins already read position / edit on the door copy.
  • mapRecipientType is unchanged and still refuses 'role'; nothing was made lenient at the seeder (Directive Add comprehensive test suite for Zod schema validation #12: no consumer alias, the conversion-layer entry does the work).
  • The grant is not new to the platform's contract: bootstrapDeclaredSharingRules reads the registry first and falls back to metadataService.list('sharing_rule') only when the registry is empty (:213–:219), so on an engine-less composition the door's converted copy was ALREADY the seeder's input and this same declaration ALREADY seeded as a position share. The engine boot was the one composition dropping it. The PR makes the two compositions agree; it does not add a grant either of them promised to withhold.
  • Against the Clause-② definition the gate mechanizes ("a new key on a published payload"; "a new exported symbol on an index, a new accepted key or value"): the diff adds no exported symbol (forwardConverted and bootConversion are private), accepts no new key or value anywhere, and moves no packages/spec schema. A seeded row that the declaration, the ADR-0087 entry and the door copy all already said should exist is enforcement catching up to the claim — Directive chore: version packages #10's "implement the rest", with the enforcement now exactly as wide as the declaration, not wider. Declaring it yes (widening) would assert a widening of a published accept set that does not exist, and demand a minor for it. no, bare, is correct; the operator-facing disclosure that a patch cannot carry in its level is carried by the changeset's sentence (judgment 6) and by this review, which the claim's widened ceiling owed for exactly this reason. RIGHT.

4. One conversion per boot, ordering and source decisions — RIGHT. forwardConverted is memoized on the instance and read at exactly two sites (init() and the registering branch of the security block in start()); assertProtocolCompat(sys, …) sits above the servicePayload ternary in init(), so the D1 handshake still runs first and a refused bundle never reaches the conversion. The one-conversion pin asserts identity (toBe) between the security registrar's registered object and the manifest payload's. #18034's fromManifest still reads manifestSource and this.collections[field], the raw inputs; converting before the spread would strip manifest.permissions (manifest-permissions-string-list-removed) and void the "2 of 2" warning, so converting the spread is the right order and the #18034 pin is reported green. Under securityMetadataRegistrar: 'artifact-door' the block still registers nothing. A bundle carrying packages[] takes the raw branch unchanged ({ ...manifest, ...bundle }), leaving resolution to the manifest service per #15292; the security block for that shape converts this.collections exactly as before. Observation, not a finding: packages: [] (present, empty) also takes the raw branch, since the test is === undefined; whether the emitter produces that shape is not read here and belongs with #23027's routing. RIGHT.

5. The five rewritten pins — RIGHT. 13 it( before and after; no skip / todo / only in either file. Each keeps its subject and asserts more, not less: (1) probe_agent registry grant map now toEqual the exact four grants (which also pins the absence of the door's three defaults) and the RLS entry toEqual the door's whole entry without priority; (2) the stored sys_permission_set map still not.toEqual the door's (parse axis kept) while its RLS now equals the door's; (3) share_legacy_deals registry sharedWith and accessLevel equal the door's, active still absent on the registry copy; (4) share_legacy_level lands edit from the conversion rather than from normalizeAccessLevel; (5) the dropped rule is now asserted as one row with its full field set and criteria_json, seeded exactly once, three names where there were two. The diff touches only the header, the fixture comment and those five blocks; the Q2 condition, Q3 scope, #22203 position and row-shape pins are untouched. The header records the conversion/parse split. The dev's ablation (the one init() line) is reported to red exactly these five. RIGHT.

6. The changeset's sentences against the diff — RIGHT, two nits. Every sentence is true of the diff: the raw-bundle cause, the one conversion after the handshake, the identity return for a current bundle, the seeders reading the registry first, the role to position seeding, the dropped allowRestore / allowPurge / priority, and the packages[] case as unchanged. Nits, not errors: the headline says the list serves "the shape" the item serves, which is exact on the conversion axis and glosses the parse-axis defaults the item still carries (the PR body states that split precisely); and "hands its package bodies over as authored" understates the branch, which hands the whole bundle raw — moot in practice because the door refuses such an artifact at the package-body parse first (#23027). Neither misleads an upgrading agent grepping a retired key. RIGHT.

② Semver level

@objectstack/runtime patch matches what the diff publishes: one published source file (packages/runtime/src/app-plugin.ts) with a behaviour fix, no new export, no new accepted key or value, no packages/spec change, no index widened; the tests and the changeset publish nothing. Post-Task step 3: a bug fix in a released package takes patch, never skip-changeset. Clause-②: no is on PR body line 2, in the changeset body, and in the claim; the three agree; a bare no with no arm is a legal declaration (no (widening) would be malformed, and yes would owe minor). check-adr-0087-registration reports no declared-breaking changeset, so no ADR-0087 disposition marker is owed. Check Changeset concluded success on this head. RIGHT.

③ Boundary flags

Implemented-by: claude/issue-22994-app-plugin-converted-collections
Reviewed-by: session_01VoSxBQujKLZKPwK2u5ehQ6

VERDICT: PASS


Generated by Claude Code

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/l tests tooling

Projects

None yet

2 participants