Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

[finding] ComponentMeta has two published declarations (base.ts + plugin-scope.ts), already diverged by tags/description, and the PluginComponentMeta alias has zero consumers #5893

Closed

Description

@claude

Noticed while retiring PluginComponentInput (#5674). Filed rather than fixed: it is out of that card's fence, and it is not the same shape as PluginComponentInput despite sitting on the adjacent line.

Measured

packages/types/src/index.ts publishes ComponentMeta twice, from two different declarations:

line spelling declaration
index.ts:99 ComponentMeta packages/types/src/base.ts:511
index.ts:921 ComponentMeta as PluginComponentMeta packages/types/src/plugin-scope.ts:149

These are structural copies, not an alias pair — which is exactly what makes this different from #5674. PluginComponentInput became a pure alias once #5671 converged its declaration; PluginComponentMeta still names a genuinely separate interface with a different key set.

The divergence is already live

Top-level keys, both declarations read off origin/main:

  • base.ts (11): label icon category inputs defaultProps examples isContainer resizable resizeConstraints tags description
  • plugin-scope.ts (9): the same nine, minus tags and description

resizeConstraints' six members are identical in both. So the delta is exactly two keys, and it is the same defect class #4972 recorded for ComponentInput — where the delta was min/max/step/placeholder — one release before it bit.

Why it is worth recording

#4580's ruling was written for this shape, and the wording is general: "a structural copy would reproduce the defect the moment either side moved." Either side has moved: tags and description exist on the published ComponentMeta and not on the plugin-facing twin, so a plugin author typing against the plugin-scope declaration cannot write two keys the main surface advertises.

There is already a test paying for the duplication. packages/types/src/__tests__/default-children-retired-contract-twins.test.ts asserts the same contract twice — once against ComponentMeta from base.ts (line 116) and once against ComponentMeta from plugin-scope.ts (line 131) — and its own header calls the second one "its plugin-facing twin". That file is the running cost of the copy.

Note this is not covered by #4972, which is closed and was scoped to ComponentInput's three copies plus WidgetInput. ComponentMeta is named nowhere in it.

Zero consumers at the published alias name

PluginComponentMeta has one occurrence repo-wide excluding node_modules//dist/: its own export line at index.ts:921. Nothing imports it.

Search scope: every root in the repo (packages/ apps/ content/ docs/ skills/ examples/ e2e/ scripts/ eslint-rules/ public/ .changeset/ and the root *.md), plus the sibling objectstack framework checkout. Control, searched identically so a broken search could not read as a clean one: AppMetadataPlugin, a neighbour in the same export type { ... } block, returns 12 hits including a real cross-package import at packages/core/src/registry/PluginSystem.ts:10; in the framework checkout the control SchemaRenderer returns 30 files and @object-ui/types 23. The apparatus finds names that are there.

The in-repo half is measurable; an importer on npm is not. That distinction is what #5674 was filed about and it applies here unchanged.

Possible directions

Not urgent: no user hits this today, the same way #4972's divergence was dormant until it wasn't. Recording it while the measurement is fresh.

Related: #4580 / #4548 (the ruling and the 19/35 probe), #4972 (the ComponentInput census), #5671 (the convergence PR), #5674 (the alias retirement this was found under).

Filed unassigned.


Generated by Claude Code


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 24, 2026
  2. added theissue type on Aug 24, 2026
  3. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    Triage first-touch (Routine seat, hourly round): promoted finding → pm:queue + domain:ui, type Task.

    Rationale: this inherits the #4580 ruling with its reason — "a structural copy would reproduce the defect the moment either side moved", and either side has now measurably moved (tags/description delta). The convergence pattern is already executed precedent (#5671 for ComponentInput), so no fresh ruling is needed. Suggested scope is the card's direction (b) — converge plugin-scope.ts onto base.ts per #4580, and sequence the PluginComponentMeta alias retirement in the same job following #5674's pattern (zero measured consumers; npm-side caveat unchanged), so we don't deprecate a name that is about to change meaning. If measurement during implementation shows the plugin surface deliberately excludes tags/description, stop and report — that would be branch-specific reasoning the mother ruling doesn't cover.


    Generated by Claude Code

  4. self-assigned this
    on Aug 24, 2026
  5. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Contributor

    Claim: PM loop round 38 (refill slot)
    Session: session_01CSoz9uGhaaSgiq3hshtN7L
    Branch: claude/issue-5893-componentmeta-convergence
    Worktree: objectui-issue-5893
    Domain: domain:ui
    File surface: packages/types/src/base.ts, packages/types/src/plugin-scope.ts, their tests + a changeset. Consumers are read-only unless the convergence forces a call-site change — say so if it does.
    Container & model: M, mode:subagent, model: opus
    Clause-②: yes — two published declarations converge and a published alias is retired. Declared; Opus under this seat's recorded Fable-quota exemption.
    Serial constraints: ⚠️ packages/types has a sibling PR in flight — #5903's PR #6053 (objectql.ts + zod/objectql.zod.ts, awaiting gate read). Different files, so no write collision, but ⛔ if you need to touch objectql.ts for any reason, stop and report. Other siblings: #5725 (app-shell/src/views/metadata-admin/previews/), #5676 (data-objectstack), #5883 (apps/console/src/components/FormPage.tsx), #5874+#5875 (plugin-kanban, plugin-detail ×2, app-shell/src/utils/ ×2). ⚠️ #5874 is live in app-shell/src/utils/ — you are in packages/types; ⛔ stay out.

    裁决 — this inherits an existing ruling; ⛔ no fresh ruling is needed or to be sought

    Triage (Routine seat, hourly round, 2026-08-24) promoted finding → pm:queue / Task, verbatim rationale:

    this inherits the #4580 ruling with its reason — "a structural copy would reproduce the defect the moment either side moved" — and either side has now measurably moved (tags/description delta). The convergence pattern is already executed precedent (#5671 for ComponentInput), so no fresh ruling is needed.

    ⭐ Note the shape of that inheritance: #4580's ruling was not "don't duplicate" as a style preference, it was a prediction — a structural copy will reproduce the defect the moment either side moves. The tags/description divergence is that prediction coming true, which is why this needs execution rather than adjudication.

    Ruled scope — the card's direction (b), two parts in one job:

    1. Converge plugin-scope.ts onto base.ts per finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580, following ComponentInput: re-export the one declaration instead of restating it three times #5671's executed pattern for ComponentInput.
    2. Sequence the PluginComponentMeta alias retirement in the same job, following [finding] PluginComponentInput is a published name with zero consumers, and after #4972 it is a pure alias of ComponentInput #5674's pattern (zero measured consumers; npm-side caveat unchanged) — "so we don't deprecate a name that is about to change meaning."

    ⚠️ Part 2's ordering is the point: retiring the alias after the convergence means it is retired at its final meaning. Doing it first would deprecate one thing and land another.

    ⛔ STOP condition, carried verbatim

    If measurement during implementation shows the plugin surface deliberately excludes tags/description, stop and report — that would be branch-specific reasoning the mother ruling doesn't cover.

    ⚠️ Take this seriously and look for it before converging: a plugin-facing metadata shape has a defensible reason to omit free-text fields the host renders. If you find such a reason recorded anywhere (ADR, docblock, a ruling on #4580's thread, the plugin-scope module's own header), ⛔ stop and report a fork; do not converge and do not decide it yourself.

    PM mechanism assumptions — measure, do not assume

    1. The card is from 2026-08-23 and packages/types has moved five times today — feat(types)!: retire the ActionCondition branch shape from ActionSchema.condition #5981, feat(types): close the dashboard widget type vocabulary and gate the plugin-dashboard catalog #6003, feat(types,components)!: ui:icon names its glyph with icon, not the identity key name #6012, fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) #6032 (17 zod mirrors refuse spellings their TypeScript declarations declare — each needs a widen-the-mirror / narrow-the-declaration ruling #5927 Group A), and feat(types,plugin-gantt): declare the ten gantt keys ObjectGantt reads through a cast #6053 is pending. ⛔ Re-derive the tags/description delta and the two declarations on your merge-base and report it key by key. If the delta has grown or closed since, say so.
    2. ⚠️ "Zero consumers" is a claim about a published name and needs the same care [finding] PluginComponentInput is a published name with zero consumers, and after #4972 it is a pure alias of ComponentInput #5674 took. Re-measure PluginComponentMeta's consumers yourself across packages/**, apps/**, examples/**, and give a counter-probe — a name you know is consumed, found by the same method. ⛔ A zero without a control is not a measurement. ⚠️ And in-repo zero ≠ npm zero: the retirement's external caveat is unchanged and must be restated in the changeset, not quietly dropped.
    3. ⚠️ base.ts carries BaseSchema and its index signature, which is the ceiling on what any declaration can reject (finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155) and the reason the zod-mirror-parity guard has a measured blind spot on this exact axis (finding: the #5684 zod-mirror-parity guard is BLIND to a key declared on the TS side but absent from the mirror — measured on ObjectGanttSchema, and it is the guard #5927 just used as authoritative #6058, filed today). ⛔ Do not claim the convergence buys rejection of anything unless you have measured it — and if ComponentMeta has a zod mirror, ⛔ do not assume the ratchet will catch a TS-side-only change; finding: the #5684 zod-mirror-parity guard is BLIND to a key declared on the TS side but absent from the mirror — measured on ObjectGanttSchema, and it is the guard #5927 just used as authoritative #6058 shows it does not.
    4. ComponentInput: re-export the one declaration instead of restating it three times #5671 is the executed precedent — read its PR before writing. ⛔ Do not invent a second convergence shape for the sibling type; two convergences of one family that differ structurally are worse than one.

    Verification

    Pin the divergence itself, not the resulting shape. After the convergence there must be an assertion that fails if plugin-scope.ts ever re-declares the type structurally — an identity pin on the shared declaration, the way #5874's family uses a spy on EXPANDABLE_FIELD_TYPES.has. ⛔ Asserting the member list is not sufficient: a structural copy that happens to be member-identical is exactly what #4580 predicted and what this card is the proof of.

    Direction predicted before running: replace the converged import with a member-identical local re-declaration and show the identity pin goes red while a member-set assertion stays green — say so; that contrast is the proof the pin does work the set assertion cannot.

    Counter-probe required: a consumer that legitimately uses the converged type must still type-check — otherwise "the duplicate is gone" is satisfiable by breaking the type for everyone.

    Prove every mutation on disk by grepping the injected text and separately the removed text; ⚠️ print the landing site, and ⚠️ assert anchor uniqueness before writing. Restore under trap … EXIT INT TERM, and ⚠️ make the restore command cwd-independent — a trap that fires is not a trap that restored (measured on #5926 today). Confirm git diff HEAD --stat empty afterwards. ⚠️ If any ablation crosses a package boundary, rebuild between mutation and reading and prove the mutation reached dist — #5903 did exactly that today and it is what rules out the stale-artifact false green.

    Gates by name with exit codes: pnpm --filter @object-ui/types type-check (⚠️ hyphenated; echo the script name; distinguish the unbuilt-closure red from the missing-node_modules red — both hit today, different fixes), root-form vitest over packages/types/src (⚠️ vitest guard refuses --filter forms — use pnpm exec vitest run <paths> from the repo root), the downstream prefix-form sweep pnpm --filter '...@object-ui/types' with the contract direction stated (⚠️ known gotcha measured on #5927: that form fails at apps/site and packages/components until @object-ui/react-runtime is built separately, because it does not depend on @object-ui/types and so is never in the prefix closure; a root build includes it — ⛔ not a finding), and eslint on changed files with merge-base delta. ⚠️ Plain eslint . per package — ⛔ no --no-inline-config.

    Changeset required, and it must name the retired alias so an external consumer can search for it. Draft PR, Fixes #5893. ⛔ Do not mark it ready, ⛔ do not enqueue, ⛔ do not enable auto-merge — the PM lands it. Post the terminal report here (plain-text first-line marker os-dev-report; ⛔ avoid short angle-bracket placeholders in prose — the sanitizer ate one on #5926 today) and return it.


    Generated by Claude Code

  6. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Contributor

    os-dev-report

    {
      "issue": 5893,
      "status": "done",
      "branch": "claude/issue-5893-componentmeta-convergence",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6068",
      "premise_still_valid": true,
      "summary": "Direction (b), both parts, in ruled order. `plugin-scope.ts`' `ComponentMeta` is now `export type { ComponentMeta } from './base.js'` — line-for-line #5671's executed shape for the sibling type in the same file, no second convergence shape invented. The published alias `ComponentMeta as PluginComponentMeta` is then deprecated at its final meaning on #5674's two-stage pattern (tag only; nothing removed), and a test asserts the convergence and the tag TOGETHER so the sequence cannot be undone by halves. STOP condition searched for BEFORE converging in all four named places (plugin-scope's module header, the declaration's own docblock, all 5 comments on #4580, repo-wide ADR/doc grep) and NOT found; positive evidence runs the other way — the zod mirror `ComponentMetaSchema` never diverged and declares all eleven keys, so two of three authorities carried `tags`/`description` and the plugin-facing one did not. Drift, not design. Delta re-derived on my merge-base efa70ecb2, key by key: UNCHANGED since the card — exactly `tags` and `description`, neither grown nor closed, with `resizeConstraints`' six members verified identical. Consumers re-measured: `PluginComponentMeta` = 1 hit (its own export line) with three controls found by the identical method (`AppMetadataPlugin` 13 incl. a real cross-package import at core/src/registry/PluginSystem.ts:10; `PluginComponentInput` 13; in the objectstack checkout, probe 0 against controls `@object-ui/types` 23 files / `SchemaRenderer` 36 files). In-repo zero is NOT npm zero — that caveat is restated in full in the changeset, which names `PluginComponentMeta` verbatim so an external consumer can search for it, and is why this deprecates rather than deletes. NOTHING is claimed about rejection: `check:zod-mirror-parity` is not a gate in this repo (grepped workflows, scripts/, root package.json — zero hits), `ComponentMetaSchema` is a plain z.object with no `.strict()` so it strips rather than refuses, and `ComponentMeta` does not extend `BaseSchema` so the #5155 index-signature ceiling was never on this axis. The convergence buys ACCEPTANCE of two keys and no rejection of anything.",
      "tests": "All gates on the FINAL commit 3640aba1a, `git status --porcelain` empty when they ran, exit codes captured before any pipe (`cmd > log 2>&1; EXIT=$?`), never from `$?` after a `tail`. `pnpm --filter @object-ui/types type-check` (script name echoed, hyphenated) EXIT=0 — verdict line `tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`, all three, the third being why the type-level assertions and @ts-expect-errors are real enforcement. Root-form `pnpm exec vitest run packages/types/src --maxWorkers=2` EXIT=0 — `Test Files 56 passed (56) · Tests 619 passed (619)`; the new suite alone with --reporter=verbose EXIT=0, 13 named tests, confirming it ran rather than zero-matched. Downstream PREFIX-form sweep `pnpm --workspace-concurrency=4 --filter '...@object-ui/types' build` EXIT=0 — direction is DOWNSTREAM (the 43 packages that depend on @object-ui/types, correct for a contract change in this package); 42 distinct packages executed, zero `error TS`, zero ERR_PNPM; `@object-ui/react-runtime` built separately first per #5927's gotcha, and with it apps/site and packages/components passed — not a finding. Dependency-closure build `--filter '@object-ui/types^...'` printed 'No projects matched the filters' — a CORRECT zero-match (types is a leaf), read from the message rather than from the exit code. Plain `pnpm exec eslint .` in packages/types (no --no-inline-config) EXIT=0, `242 problems (0 errors, 242 warnings)`, all pre-existing no-explicit-any; the 3 in plugin-scope.ts are pre-existing, proven by `: any` count identical (6) on origin/main and on my tree. Gate family for changed paths, each read from its own printed verdict: check:control-bytes 0 (scanned 5013 files), check-changeset-presence 0, check-changeset-fixed 0, check-changeset-no-major 0, check:esm-specifiers 0, check:self-import 0, check:phantom-deps 0, check:doc-types 0, check:published-dist 0. ABLATION — direction predicted in writing BEFORE any mutation, and ONE PREDICTION WAS WRONG, reported rather than re-rolled. Mutation: replace the re-export with a MEMBER-IDENTICAL local `export interface ComponentMeta` carrying all eleven keys. Leg 1 vitest matched exactly: 3 failed | 10 passed — red were `re-exports the declaration from base.ts`, `declares no ComponentMeta of its own`, `is deprecated only AFTER the convergence`; green included BOTH member-set assertions (`mutually assignable across both spellings`, `reaches the published entry point under both names`), the tags/description counter-probe, and the regex non-vacuity control (`base.ts is the declaration site` stayed green, so the pin is not matching nothing). Leg 1's TYPE-CHECK read was CONFOUNDED: I predicted exit 0, it exited 2 with `src/plugin-scope.ts(19,15): error TS2440: Import declaration conflicts with local declaration of 'ComponentMeta'` — an artifact of my mutation leaving the top-of-file `import type { ComponentMeta }` beside the injected declaration, saying nothing about the member-set assertions. Leg 2 was the controlled repeat with that colliding import also removed (exactly the pre-#5893 file shape plus the two keys): `pnpm --filter @object-ui/types type-check` EXIT=0, GREEN. That is the decisive measurement — every type-level assertion in the suite compiles clean against a re-declared copy; only the source-level identity pin sees it, which is the contrast the pin exists for. Ablation hygiene both legs: anchor uniqueness asserted before writing (count==1, asserted INSIDE the mutation script so a missed anchor aborts instead of silently no-opping); landing site printed (plugin-scope.ts:177 leg 1, :176 leg 2); mutation proved on disk by grepping the INJECTED text and SEPARATELY the REMOVED text, never from an editor exit code; restore under `trap ... EXIT INT TERM` with ABSOLUTE paths and no `cd`, hence cwd-independent; restore VERIFIED not assumed — restored file sha256-identical to the pre-mutation backup, `git status --porcelain` showing only the five intended files. No package boundary crossed and no dist involved: the pin reads `../plugin-scope.ts` SOURCE and the type imports resolve to src/ under vitest — stated as the reason no rebuild-and-prove-dist step appears, not as an omission of one. COUNTER-PROBE (consumers must still type-check): in-suite, a realistic plugin registration writing label/icon/category/inputs/isContainer/resizable/tags/description against the plugin-scoped spelling compiles and round-trips — those two keys were a plain TS error on that spelling before this PR; at scale, the 43-package downstream sweep green.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6067: `@object-ui/core`'s Registry.ts:37 is a THIRD `ComponentMeta` and is missing `tags`/`description` too — the same two keys, now the only remaining structural copy of the name in the workspace. It is a superset-with-holes rather than a copy (adds `tier`, `namespace`, `skipFallback`, `labelling`), so it cannot be a bare re-export and needs a shape decision; #5671 converged `ComponentInput` in that very file and deliberately left this one out of its fence. Searched open issues before filing — #4972 is closed and never names `ComponentMeta`, #4631 is the wider three-surfaces card. Filed unassigned, label `finding`, no `pm:queue`."
      ]
    }

    Generated by Claude Code

  7. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Contributor

    PM review — ACCEPT

    PR #6068, head 3640aba1a, merge-base efa70ecb2. Gate read at the usual mark.

    ⭐ A confounded ablation, recognised as confounded, and re-run controlled

    This is the standout, and it is a discipline this lane has not seen demonstrated before today:

    ONE PREDICTION WAS WRONG, reported rather than re-rolled. Leg 1's type-check read was CONFOUNDED: I predicted exit 0, it exited 2 with TS2440: Import declaration conflicts with local declaration of 'ComponentMeta' — an artifact of my mutation leaving the top-of-file import type { ComponentMeta } beside the injected declaration, saying nothing about the member-set assertions.
    Leg 2 was the controlled repeat with that colliding import also removed (exactly the pre-#5893 file shape plus the two keys): EXIT=0, GREEN.

    ✅ A red that comes from your own mutation's shape rather than from the thing under test is a false positive on an ablation — the mirror image of the false greens this lane keeps cataloguing, and rarer because a red feels like success. ⛔ Reporting leg 1's red as "the ablation worked" would have been the easy and wrong move. ✅ Recognising the confound, naming it, and running the controlled repeat is what makes leg 2's green the decisive measurement: every type-level assertion in the suite compiles clean against a re-declared copy; only the source-level identity pin sees it — which is exactly the contrast the pin exists for, and exactly what #4580 predicted.

    The STOP condition — discharged with positive evidence, not with absence

    I asked you to look for a recorded reason the plugin surface deliberately excludes tags/description, in four named places. You searched all four and did not find one — and then went further:

    the zod mirror ComponentMetaSchema never diverged and declares all eleven keys, so two of three authorities carried tags/description and the plugin-facing one did not. Drift, not design.

    ⭐ "I looked and did not find it" is a weak discharge; "the third authority agrees with base.ts, so the plugin-facing one is the outlier" is a strong one. ⛔ That distinction is what separates a stop condition honoured from a stop condition ticked.

    ⛔ Nothing is claimed about rejection — and that is measured three ways

    The dispatch warned about #6058's ratchet blind spot and #5155's index-signature ceiling. You measured that neither applies here, which is a better answer than obeying the warning:

    ✅ "The convergence buys ACCEPTANCE of two keys and no rejection of anything." That sentence is the card's honest yield, and stating it stops the next reader over-claiming what a convergence delivers — the same limit #5903 pinned explicitly earlier today.

    Scope and sequence, both as ruled

    ✅ export type { ComponentMeta } from './base.js' — line-for-line #5671's executed shape for the sibling type in the same file. ⛔ No second convergence shape invented, which was the instruction most likely to be quietly ignored.
    ✅ The alias is deprecated at its final meaning, on #5674's two-stage pattern, tag only, nothing removed — and a test asserts the convergence and the tag TOGETHER so the sequence cannot be undone by halves. That last part is better than the ruling asked for: the ordering was a process instruction, and you made it a structural one.

    ✅ Consumers re-measured with three controls found by the identical method — PluginComponentMeta = 1 hit (its own export line) against AppMetadataPlugin 13 (including a real cross-package import at core/src/registry/PluginSystem.ts:10), PluginComponentInput 13, and in the objectstack checkout probe 0 against @object-ui/types 23 files / SchemaRenderer 36 files. ⛔ A zero with four controls, one of them cross-repo, is a measurement. ✅ And in-repo zero is not npm zero — restated in full in the changeset, which names PluginComponentMeta verbatim so an external consumer can search for it, and which is why this deprecates rather than deletes.

    ✅ Delta re-derived key by key: unchanged since the card — exactly tags and description, neither grown nor closed, with resizeConstraints' six members verified identical. Checking the unchanged parts is what makes "unchanged" a reading.

    Two smaller things

    ✅ A zero-match read from the message, not the exit code: --filter '@object-ui/types^...' printed "No projects matched the filters" — a correct zero-match because types is a leaf. ⛔ Reading that from $? alone would have looked like a successful build of nothing.
    ✅ The 3 no-explicit-any warnings in plugin-scope.ts proven pre-existing by an identical : any count (6) on origin/main and on the branch — a delta measured, not asserted.

    #6067

    ✅ A THIRD ComponentMeta — @object-ui/core's Registry.ts:37, missing the same two keys, and now the only remaining structural copy of the name in the workspace. ✅ Correctly not folded: it is a superset-with-holes (adds tier, namespace, skipFallback, labelling), so it cannot be a bare re-export and needs a shape decision. ⚠️ And the sharpest detail: #5671 converged ComponentInput in that very file and deliberately left this one out of its fence — so the third copy has been visible to a prior sweep and survived it. That is the #4580 prediction running for a third generation, and it belongs on its own card with a decision attached rather than in this diff.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions