Skip to content

feat(conformance): add O006 DirectRocksdictImport rule - #3089

Merged
cmgrote merged 4 commits into
mainfrom
mrun/o005-rocksdict-conformance-check
Aug 10, 2026
Merged

feat(conformance): add O006 DirectRocksdictImport rule#3089
cmgrote merged 4 commits into
mainfrom
mrun/o005-rocksdict-conformance-check

Conversation

@AtMrun

@AtMrun AtMrun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Linked issue

https://linear.app/atlan-epd/issue/FND-163/parquet-nulllarge-string-typing-two-duplicate-sdk-fixes-still-leaking

Problem

atlan-thoughtspot-app and atlan-aws-smus-app each independently hand-rolled a RocksDB-backed DiskLookup class with an asymmetric JSON serialize/deserialize step:

def put(self, key, value):
    serialized_value = json.dumps(value) if not isinstance(value, str) else value
    self._db[str(key)] = serialized_value

def get(self, key, default=None):
    value = self._db[str(key)]
    try:
        return json.loads(value)          # unconditional — no matching str-shortcut
    except (json.JSONDecodeError, TypeError):
        return value

put() special-cases str and stores it raw; get() unconditionally tries json.loads(). A stored string that also happens to be valid bare JSON (a numeric-looking name, "true", "null") silently round-trips back as int/bool/None instead of str — corrupting output columns and, in ThoughtSpot's case, crashing the downstream parquet writer (CNCT-191; the Teradata/Presto sibling bug is CNCT-80).

Neither connector had a fleet-wide signal nudging it toward application_sdk.common.spillable_dict.SpillableDict, which already wraps the same rocksdict.Rdict and pickles values directly — no hand-rolled serialize/deserialize step to get wrong. Two connectors independently wrote the same bug because nothing pointed either one at the SDK utility that already solves it.

Fix

New O006 DirectRocksdictImport rule (WARN, scope=app), mirroring O004's import-anchored shape: flags any direct from rocksdict import ... / import rocksdict in app code, recommending SpillableDict instead. application_sdk.common.spillable_dict and application_sdk.common.incremental.storage.rocksdb_utils — the SDK's own intended callers of rocksdict — are excluded via the existing scope=APP mechanism (the SDK is the publisher of this seam, not a subject of the rule).

Numbered O006, not O005: #3094 (UnresolvedAppNamePlaceholder) claims O005 and merges first, so this rule takes the next free ID.

Not autofixable — SpillableDict's key type is restricted to str | int | float | bool | bytes and it has no custom rocksdict.Options tuning surface, so each site needs review before migrating. Suppressible with # conformance: ignore[O006] <reason>.

Test plan

  • check_o006 unit tests: from-import, aliased from-import, module import, submodule from-import, silent on SpillableDict/unrelated imports, inline suppression (test_asset_mapper.py)
  • Catalog tests updated: test_catalog_o_series_present, test_catalog_app_scoped_rules_are_the_expected_set
  • Regenerated docs/rules/optimizations.md via gen-rule-docs
  • Full packages/conformance suite: 2159 passed (1 pre-existing failure unrelated to this change — test_sdk_base_names_matches_templates_all needs a sibling application_sdk install not present in this standalone checkout)

🤖 Generated with Claude Code

Two connectors (atlan-thoughtspot-app, atlan-aws-smus-app) independently
hand-rolled a RocksDB-backed DiskLookup with an asymmetric JSON
serialize/deserialize step: put() special-cased str, get() unconditionally
ran json.loads() on every read. A stored string that also happened to be
valid bare JSON (a numeric-looking name, "true", "null") silently came back
as int/bool/None instead of str, corrupting output columns (CNCT-80,
CNCT-191). Neither connector had a fleet-wide signal nudging it toward the
SDK's existing application_sdk.common.spillable_dict.SpillableDict, which
wraps the same rocksdict.Rdict without the hand-rolled serialization step.

O005 flags any direct `rocksdict` import in app code, mirroring O004's
import-anchored shape and scope=APP posture (the SDK's own spillable_dict.py
and rocksdb_utils.py are the intended callers of rocksdict and are excluded).
O005 is claimed by UnresolvedAppNamePlaceholder (#3094), which will merge
first. Renumber this rule to O006 so the two land without an ID collision.
@linear

linear Bot commented Aug 10, 2026

Copy link
Copy Markdown

FND-163

@cmgrote cmgrote changed the title feat(conformance): add O005 DirectRocksdictImport rule feat(conformance): add O006 DirectRocksdictImport rule Aug 10, 2026
cmgrote added a commit that referenced this pull request Aug 10, 2026
O005 fired five times on the module that implements the behaviour it is
meant to protect. Run against PR #3101 (FND-195):

  common/task_queue.py:86   APP_NAME_TOKEN = "{app_name}"
  common/task_queue.py:239  attribute docstring naming the token
  common/task_queue.py:243  attribute docstring naming the token
  handler/service.py:2009   logger.error(... unresolved {app_name} ...)
  handler/service.py:2023   logger.warning(... unbaked {app_name} ...)

None can freeze into an identifier. A rule that flags the canonical fix,
the docs describing it, and the logs diagnosing it is a rule people
suppress — which costs the true positives too.

Detection now anchors on the token reaching a value. Three exclusions
added, each narrow:

* documentation — the value of any bare string expression statement.
  The previous check excluded only body[0] of Module/ClassDef/FunctionDef,
  so a PEP 257 attribute docstring (a bare string after a field
  annotation) was flagged. A string bound to nothing cannot be
  dispatched.
* diagnostic text — inside a logging call, warnings.warn, or a raise.
  Reporting an unresolved token requires quoting it.
* token sentinels and message constants — bound to an ALL_CAPS name
  where the literal is exactly the token (its own definition) or the
  name reads as prose (_MESSAGE, RATIONALE).

Kept narrow deliberately: TASK_QUEUE = "atlan-{app_name}-prod" is
ALL_CAPS but neither bare-token nor prose-named, so it still fires. New
tests pin that, plus keyword arguments, values at any depth in a DAG
literal, a returned template, and a bare token bound to a lowercase name
— the shapes an over-broad exclusion would have swallowed. All ten
original tests pass unchanged.

Also realigned the rule metadata, which predated FND-195 and claimed no
canonical helper exists. application_sdk.common.task_queue now provides
derive_task_queue and resolve_manifest_tokens, so remediation has one
target. Detection stays shape-anchored rather than import-anchored on
purpose: the writers most worth catching are hand-authored templates
outside the SDK that import nothing at all.

The checker's own _MESSAGE is now built from _TOKEN rather than spelling
the token inline, so this module does not depend on its own exclusions to
avoid self-flagging.

Rule docs regenerated. Note for a follow-up, not addressed here: O005 is
claimed by both this PR and #3089 (DirectRocksdictImport), both targeting
0.18.0 — whichever merges second needs renumbering, including the
{#o005} docs anchor. Deciding which yields is the two authors' call.
@cmgrote

cmgrote commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

@sdk-resolve

@github-actions

Copy link
Copy Markdown
Contributor

🤖 SDK Resolve started. Driving this PR toward merge-ready — fixing CI + every @sdk-review finding, then requesting human review.

Follow progress →

This runs out-of-band and can take several minutes; I'll comment here when it finishes.

@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@sdk-review

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
Earlier @sdk-review trigger (click to expand)

🔍 SDK Review (mothership) triggered by @mothership-ai[bot] at 2026-08-10T20:25:08.050Z.

Watch the workflow run live — the review summary will appear as a separate comment when complete (typical: 5–30 min, hard cap 2h).


Completed — status completed, cost $4.160823999999999, duration 17m 46s.

@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SDK Review (mothership): PR #3089 — feat(conformance): add O006 DirectRocksdictImport rule

Verdict: NEEDS FIXES

The detector, catalog registration, scope, tier, tests, and generated docs are all correct and verified at runtime — this is a well-constructed rule that faithfully mirrors O004's proven shape. The one gap is the pairing the conformance suite treats as load-bearing: O006 is registered for detection but has no prescription in the O-series remediation program, so any app finding it produces reaches the remediate loop with no rule-specific guidance. Add the O006 section to optimizations.prose.md and this is ready.


Findings

packages/conformance/conformance/programs/areas/optimizations.prose.md

  • Important [REMEDIATION] L138 — O006 is detected, catalogued, documented, and tested, but the active O-series remediation program (the file remediate-finding dispatches every finding.area == "optimizations" result to) prescribes fixes only for O001–O004. An O006 finding routed here has no rule-specific guidance for judging SpillableDict compatibility, a deliberate custom rocksdict.Options, an out-of-range key type, or the association-list (append_to_key) case the rule's own docs call out as a legitimate suppression. Path: immediate fix — add an O006 DirectRocksdictImport prescription covering the migrate-to-SpillableDict judgment (pickled values, str|int|float|bool|bytes keys, no custom Options surface) and the justified # conformance: ignore[O006] <reason> path, mirroring how O004 documents its intentional-legacy-pin suppression.

Strengths

  • Detector is import-anchored and correctly scoped: verified it matches from rocksdict import Rdict, aliased/submodule/multiline forms, and import rocksdict, while rejecting lookalikes (rocksdictx, my.rocksdict). Runtime-probed, not just read.
  • Correct WARN-tier + scope=APP: the SDK's own intended rocksdict callers (spillable_dict.py, rocksdb_utils.py) are excluded by the runner's scope filter, so the rule is dogfood-safe against this repo — confirmed by green CI conformance legs on this HEAD.
  • Catalog wiring is consistent end-to-end: RULES now [O001..O004, O006], both catalog tests updated, generated docs regenerated. The O005 skip is deliberate and documented (PR feat(conformance): add O005 UnresolvedAppNamePlaceholder #3094 holds O005).
  • Positive, negative, and inline-suppression behavior tests all pass (55/55 green locally).
  • since="0.18.0" matches the existing next-version convention already on main (contract_toolkit.py).

CI: all passing (no failing checks on this HEAD)
Models: Claude (review) + adversarial skipped (budget >70% at Phase 2 per ORCHESTRATION §2b)
Cross-model agreement: 1/1 findings kept under the no-adversarial rule (Opus confidence ≥ 0.80)
Run: view workflow logs + cost

@cmgrote

cmgrote commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

@sdk-resolve

@github-actions

Copy link
Copy Markdown
Contributor

🤖 SDK Resolve started. Driving this PR toward merge-ready — fixing CI + every @sdk-review finding, then requesting human review.

Follow progress →

This runs out-of-band and can take several minutes; I'll comment here when it finishes.

@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🤖 SDK Resolve — round 3. Picked up the latest review: 1 open finding (0 blocking, 1 nit). Verdict is READY_TO_MERGE and CI is green, but I don't stop on an open nit — applying its concrete Path: fix (add the explicit suppression template to the Keys carve-out), then I'll push and re-run @sdk-review automatically. I keep looping until every finding (nits included) is fixed + green CI + READY_TO_MERGE. Progress: https://github.com/atlanhq/application-sdk/actions/runs/31430735054

…ption

The O-series remediation program prescribed fixes only for O001-O004, so
an O006 finding routed to remediate-finding had no rule-specific guidance.
Add an O006 section covering the migrate-to-SpillableDict judgment (pickled
values, str|int|float|bool|bytes keys, no custom Options surface) and the
justified-suppression paths (deliberate custom Options, out-of-range key
types, native merge/append_to_key semantics SpillableDict does not provide),
mirroring how O004 documents its intentional-legacy-pin suppression.
@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@sdk-review

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
Earlier @sdk-review trigger (click to expand)

🔍 SDK Review (mothership) triggered by @mothership-ai[bot] at 2026-08-10T20:58:07.954Z.

Watch the workflow run live — the review summary will appear as a separate comment when complete (typical: 5–30 min, hard cap 2h).


Completed — status completed, cost $4.468274, duration 10m 21s.

@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SDK Re-review (mothership): PR #3089 — feat(conformance): add O006 DirectRocksdictImport rule

Verdict: READY TO MERGE

The single prior finding — O006 registered for detection with no prescription in the O-series remediation program — is resolved. The delta adds a complete O006 block to optimizations.prose.md covering the migrate-to-SpillableDict judgment, all three suppression carve-outs, and classification="judgment" residue routing. I verified every technical claim in the new prose against application_sdk/common/spillable_dict.py (pickled values, the str|int|float|bool|bytes key restriction, the internal Options surface, and append_to_key's non-atomic O(K²) semantics) — all accurate. Dispatch is series-letter → area file, so O006 routes to this file automatically with no dispatch-table edit. One non-blocking nit remains on the Keys carve-out phrasing.


Delta from previous review

  • Resolved (1): O006 prescription added to packages/conformance/conformance/programs/areas/optimizations.prose.md — the remediate-finding loop now has rule-specific guidance for the SpillableDict migration judgment and the justified-suppression paths.
  • Still present (0): none.
  • New (1): one Nit on the Keys carve-out (below).
  • Downgraded (0): none.

Findings

packages/conformance/conformance/programs/areas/optimizations.prose.md

  • Nit [REMEDIATION] L154-157 — The Keys carve-out ends "either reshape the key into a supported primitive or suppress (below)", but the two explicit # conformance: ignore[O006] <reason> templates that follow are each scoped to their own case ("naming the tuning it depends on" for Options, "naming the merge semantics" for append/merge). Neither names the unsupported-key-type justification, so the Keys case has no literal suppression template of its own. The remediate loop is shown the syntax twice and told to suppress, and every O006 outcome is classification="judgment" → routed to residue for human confirmation, so this is a completeness gap in the prose, not a correctness break. Path: optional cleanup — add one template line to the Keys bullet, e.g. "…or suppress with # conformance: ignore[O006] <reason naming the non-primitive key type> (routed to residue)", so all three carve-outs carry an explicit suppression form.

Holistic Recommendations

  • None. This PR treats the cause (no fleet-wide signal nudging connectors off hand-rolled RocksDB wrappers) rather than a symptom, and pairs the detector with its remediation prescription in the same area file.

Strengths

  • Prior round's verdict addressed cleanly and completely — the delta is exactly the prescription the prior review asked for, nothing more.
  • The four technical claims in the O006 prose are all verified accurate against the SpillableDict source: value pickling, the str | int | float | bool | bytes key restriction with TypeError, the internally-built Options()/BlockBasedOptions() with no caller tuning surface, and append_to_key's read-modify-write / non-atomic / O(K²) semantics.
  • The three suppression carve-outs (custom Options, out-of-range key type, native merge/append_to_key semantics) mirror how O004 documents its intentional-legacy-pin suppression — consistent with the suite's conventions.
  • Dogfood-safe: detect --repo . --series O exits 0 with only pre-existing O001 warnings; O006's scope=APP correctly excludes the SDK's own intended rocksdict callers (spillable_dict.py, rocksdb_utils.py).
  • 55/55 conformance tests pass on this HEAD (test_asset_mapper.py, test_catalog.py).

CI: all passing (no failing checks on this HEAD; sdk-review status set by this run)
Models: Claude (review) + adversarial skipped (review_scope=conformance-only per ORCHESTRATION §2b)
Cross-model agreement: 1/1 findings kept under the no-adversarial rule (Opus confidence ≥ 0.80)
Run: view workflow logs + cost

Comment thread packages/conformance/conformance/programs/areas/optimizations.prose.md Outdated
atlan-ci
atlan-ci previously approved these changes Aug 10, 2026

@atlan-ci atlan-ci left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SDK reviewer's verdict: READY TO MERGE.

Full review summary is in the comment posted on this PR.

@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@sdk-review

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🔍 SDK Review (mothership) triggered by @mothership-ai[bot] at 2026-08-10T21:18:08.360Z.

Watch the workflow run live — the review summary will appear as a separate comment when complete (typical: 5–30 min, hard cap 2h).


Completed — status completed, cost $3.8318229999999995, duration 11m 40s.

@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SDK Re-review (mothership): PR #3089 — feat(conformance): add O006 DirectRocksdictImport rule

Verdict: READY TO MERGE

The single prior-round nit — the Keys carve-out in the remediation prose lacked its own explicit # conformance: ignore[O006] suppression template — is resolved: the Keys bullet now carries <reason naming the non-primitive key type>, matching the Options and merge-semantics carve-outs. This round I independently re-verified the whole rule end-to-end rather than trusting the prior pass: the detector logic, catalog registration, scope correctness, dogfood safety, doc freshness, and all four technical claims in the prose against the SpillableDict source. Everything checks out.


Delta from previous review

  • Resolved (1): Keys carve-out in optimizations.prose.md now carries its own explicit suppression template (# conformance: ignore[O006] <reason naming the non-primitive key type>), so all three justified-suppression paths have a literal template.
  • Still present (0): none.
  • New (0): none.
  • Downgraded (0): none.

Findings

None. No Critical, Important, or Nit findings this round.

Holistic Recommendations

  • None. The rule treats the cause (no fleet-wide signal steering connectors off hand-rolled RocksDB wrappers) rather than a symptom, and ships detector + catalog registration + remediation prescription + tests together in one PR — the pairing the conformance suite expects.

Strengths (independently verified this round)

  • Detector correctnesscheck_o006 matches all three import forms (from rocksdict import Rdict, aliased from-import, import rocksdict) plus submodule from-imports (from rocksdict.options import Options) via the module == "rocksdict" or startswith("rocksdict.") anchor, mirroring O004's shape. It does not over-match: application_sdk.common.spillable_dict and unrelated imports (sqlite3) are silent. Confirmed by the 7 O006 tests in test_asset_mapper.py, all passing.
  • Scope is correct and dogfood-safescope=RuleScope.APP. I ran the real suite runner on this HEAD: python -m conformance.suite.runner --repo . --series O → gate passed, rule counts: {'O001': 2} (both pre-existing), zero O006 findings_rule_in_scope(APP, SDK) is False, so the SDK's own intended rocksdict callers (spillable_dict.py, rocksdb_utils.py) are correctly excluded at the runner level even though the raw detector fires on them.
  • All four prose claims verified accurate against application_sdk/common/spillable_dict.py: values pickled on write/unpickled on read; keys restricted to str | int | float | bool | bytes with TypeError (SpillableKey TypeAlias, _validate_key); Options()/BlockBasedOptions() built internally with no caller tuning surface; append_to_key is a read-modify-write list append (non-atomic, O(K²)), not RocksDB's native merge.
  • Catalog + docs in syncgen-rule-docs --check passes ("All rule catalog docs are up-to-date"); doc table bumped to 5 rules; test_catalog.py asserts O006 in both the app-scoped set and the O-series presence set. since="0.18.0" is the next conformance package version (current 0.17.0), consistent with convention. O005 is a reserved ID (absent everywhere), not a gap.
  • Remediation dispatch needs no edit — routing is finding.area (series-letter) → area file, so O006 routes to optimizations.prose.md automatically.
  • 55/55 conformance tests pass on this HEAD (test_asset_mapper.py, test_catalog.py).

CI: all passing (no failing checks on this HEAD; sdk-review status set by this run)
Models: Claude (review) + adversarial skipped (review_scope=conformance-only per ORCHESTRATION §2b)
Cross-model agreement: no findings to challenge (0-finding review)
Run: view workflow logs + cost

@atlan-ci atlan-ci left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SDK reviewer's verdict: READY TO MERGE.

Full review summary is in the comment posted on this PR.

@mothership-ai

mothership-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🤖 SDK Resolve — merge-ready. @cmgrote this PR is now yours to review and merge.

Outcome: MERGE-READY — green CI + every @sdk-review finding fixed (nits included) + verdict READY_TO_MERGE. I did not merge; that's the human gate.

  • Rounds: 3 review rounds this run.
  • Findings fixed: 2 — (1) an Important/REMEDIATION finding: the O006 rule was registered for detection but had no remediation prescription in optimizations.prose.md; I added the full O006 block (migrate-to-SpillableDict judgment + the three justified-suppression carve-outs). (2) a Nit: the Keys carve-out lacked its own explicit # conformance: ignore[O006] suppression template; I added it so all three suppression paths carry a literal template.
  • Findings dismissed: 0 — nothing was disputed or shipped over.
  • Final CI: green (all required checks pass on HEAD c2f2e33f; only the optional Endor Labs scan still queued).
  • Final verdict: READY_TO_MERGE (re-review run https://github.com/atlanhq/application-sdk/actions/runs/31433165613) — zero findings, nits included.

Reviewer assignment: NO reviewer list is configured (vars.SDK_RESOLVE_REVIEWERS is unset), so I could not auto-request reviewers — a human needs to assign one. Progress: https://github.com/atlanhq/application-sdk/actions/runs/31430735054

@cmgrote
cmgrote added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 70fe46e Aug 10, 2026
45 checks passed
@cmgrote
cmgrote deleted the mrun/o005-rocksdict-conformance-check branch August 10, 2026 21:38
mothership-ai Bot pushed a commit that referenced this pull request Aug 10, 2026
O005 fired five times on the module that implements the behaviour it is
meant to protect. Run against PR #3101 (FND-195):

  common/task_queue.py:86   APP_NAME_TOKEN = "{app_name}"
  common/task_queue.py:239  attribute docstring naming the token
  common/task_queue.py:243  attribute docstring naming the token
  handler/service.py:2009   logger.error(... unresolved {app_name} ...)
  handler/service.py:2023   logger.warning(... unbaked {app_name} ...)

None can freeze into an identifier. A rule that flags the canonical fix,
the docs describing it, and the logs diagnosing it is a rule people
suppress — which costs the true positives too.

Detection now anchors on the token reaching a value. Three exclusions
added, each narrow:

* documentation — the value of any bare string expression statement.
  The previous check excluded only body[0] of Module/ClassDef/FunctionDef,
  so a PEP 257 attribute docstring (a bare string after a field
  annotation) was flagged. A string bound to nothing cannot be
  dispatched.
* diagnostic text — inside a logging call, warnings.warn, or a raise.
  Reporting an unresolved token requires quoting it.
* token sentinels and message constants — bound to an ALL_CAPS name
  where the literal is exactly the token (its own definition) or the
  name reads as prose (_MESSAGE, RATIONALE).

Kept narrow deliberately: TASK_QUEUE = "atlan-{app_name}-prod" is
ALL_CAPS but neither bare-token nor prose-named, so it still fires. New
tests pin that, plus keyword arguments, values at any depth in a DAG
literal, a returned template, and a bare token bound to a lowercase name
— the shapes an over-broad exclusion would have swallowed. All ten
original tests pass unchanged.

Also realigned the rule metadata, which predated FND-195 and claimed no
canonical helper exists. application_sdk.common.task_queue now provides
derive_task_queue and resolve_manifest_tokens, so remediation has one
target. Detection stays shape-anchored rather than import-anchored on
purpose: the writers most worth catching are hand-authored templates
outside the SDK that import nothing at all.

The checker's own _MESSAGE is now built from _TOKEN rather than spelling
the token inline, so this module does not depend on its own exclusions to
avoid self-flagging.

Rule docs regenerated. Note for a follow-up, not addressed here: O005 is
claimed by both this PR and #3089 (DirectRocksdictImport), both targeting
0.18.0 — whichever merges second needs renumbering, including the
{#o005} docs anchor. Deciding which yields is the two authors' call.
mothership-ai Bot added a commit that referenced this pull request Aug 10, 2026
Regenerated via gen-rule-docs so the catalog page carries both O005
(this PR) and O006 (#3089, landed on main) after the rebase.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants