Skip to content

Commit 1ab8752

Browse files
os-zhuangclaude
andauthored
ADR-0076 D11 前置验证:HTTP 端口一致性门(packages/qa/http-conformance)+ OQ#9/10 结论 (#2462) (#3037)
* feat(transport): prove the IHttpServer port is Hono-free — second adapter on raw node:http (ADR-0076 D11/OQ#10, #2462) Multi-adapter was designed but unproven: only the Hono adapter existed, and ADR-0076 D11 flagged 'the normalized context shows Hono-isms' as a caveat blocking the transport decomposition. This adds the validation: - New @objectstack/plugin-node-server: a thin IHttpServer implementation on raw node:http with ZERO dependencies beyond @objectstack/core — :param + trailing-* routing (registration-order first-match, same as the primary adapter), eager JSON/urlencoded body parse with lazy rawBody() for binary, SSE via native res.write/end, 404/405-with-Allow semantics, EADDRINUSE retry, graceful drain on close. Deliberately no getRawApp()/mount(). - Cross-adapter conformance suite: boots the dispatcher bridge AND the REST route generator (with ObjectQL + memory driver) on BOTH adapters over real sockets — /data CRUD roundtrip, /meta reads, /ready, /health, discovery, :param routing, 404/405 parity, plus a probe-for-probe response-shape parity matrix between the two adapters. 40 assertions, all green. Findings recorded in ADR-0076 (OQ#9/OQ#10/OQ#11 resolution notes): - The port has NO hard Hono-isms. The Host-header backfill in the Hono adapter is a Fetch-API artifact local to that adapter, not a port leak. - All remaining Hono coupling is confined to the feature-detected getRawApp() escape hatch (metadata HMR, cloud-connection routes, static/SPA + CORS + Server-Timing) which degrades gracefully. - Follow-up for the D11 window: codify res.write/res.end (SSE) and getPort() — extensions consumers already rely on — plus 404/405 semantics into the IHttpServer contract. - OQ#9 audit verdict (delineate, don't merge; only discovery + packages are genuinely double-mounted) recorded in the ADR open-questions list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * fix(plugin-node-server): harden query/params against prototype pollution (CodeQL) CodeQL flagged remote property injection: query-param keys come straight from the request URL, so building the map on a plain object literal let '?__proto__=…' write through the prototype chain. Use null-prototype objects for query and params and drop the dangerous keys (__proto__/constructor/prototype) outright; regression test pins that '?__proto__=polluted' is dropped and Object.prototype stays clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * fix(plugin-node-server): eliminate the dynamic property-write sink CodeQL flags The blacklist guard added in the previous commit was not recognized as a sanitizer by CodeQL's js/remote-property-injection rule — the sink is the computed write itself (obj[userKey] = …). Build query and params via Object.fromEntries (own data properties only, no prototype-chain walk) so the sink no longer exists; the __proto__/constructor/prototype drop stays as defense in depth. Regression test unchanged and green (41/41). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * refactor(qa): reposition the port-conformance adapter as a private QA gate — packages/qa/http-conformance Maintainer review: the node:http adapter is a validation instrument, not a product server, so it must not ship as a publishable plugin- package. Reposition it accordingly: - Move packages/plugins/plugin-node-server → packages/qa/http-conformance; rename @objectstack/plugin-node-server → @objectstack/http-conformance, private: true, test-only (no build/publish), matching the existing verification-gate packages (dogfood, downstream-contract). - packages/qa/ becomes the unified home for non-published verification gates (new pnpm-workspace glob); migrating dogfood/downstream-contract there is proposed as a follow-up. - Drop the changeset + fixed-group entry (private packages are not versioned); update the ADR-0076 OQ#10 note to the new name. Behavior unchanged: same adapter, same 41-assertion cross-adapter conformance suite, all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * refactor(qa): migrate dogfood + downstream-contract into packages/qa/ (unified verification-gate home) Completes the packages/qa/ consolidation started with http-conformance: all three non-published verification gates (dogfood regression gate, downstream-contract compatibility gate, http-conformance port gate) now live under one directory. Mechanical path migration — no behavior change: - git mv packages/{dogfood,downstream-contract} → packages/qa/ - rewrite every 'packages/dogfood' / 'packages/downstream-contract' breadcrumb: spec liveness proof registry + JSON ledgers, spec-liveness-check.yml path trigger, eslint ignore, docs/ADRs, source comments - bump the repo-root traversal one level in the four dogfood conformance tests that resolve REPO_ROOT from __dirname Verified from the new locations: dogfood 273 passed / 3 skipped, downstream-contract typecheck + 14 tests, spec check:liveness resolves all bound proofs, http-conformance 41/41. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * chore: retrigger CI — Actions did not fire on dc6507c Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * docs(changeset): fix dogfood path breadcrumb after packages/qa migration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * fix(ci): build the examples' dependency closure explicitly in the typecheck job + correct stale ADR-0076 D12 status Two follow-ups after merging latest main: - lint.yml: the example-app typecheck step built only ./packages/* (direct children). The connector packages the showcase imports were built purely by accident — through dogfood's dependency chain — which broke when dogfood moved to packages/qa/. Request the examples' dependency closure explicitly (--filter='./examples/*^...') so the step no longer depends on which package happens to live at the top level. - ADR-0076: the round-2 status verification merged from main (#3061) was read against a pre-#3028 snapshot — svcAvailable no longer hardcodes 'available'; the D12 framework side shipped in #3028. Correct the status line and note OQ#9/OQ#10 resolution (#3037). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * fix(ci): cap turbo test concurrency at the runner's core count (OOM mitigation) Test Core started failing deterministically on the hosted runner — plugin-audit#test dies with ZERO vitest output (kernel OOM-kill signature) while passing locally on Node 20 and 22 — first on main @4f8c2d1, then twice on this PR (initial run + job re-run). Onset correlates with the task graph growing past ~100 tasks (connector packages + qa gates): turbo's default concurrency (10) schedules that many vitest workers + tsup DTS builds onto a 4-vCPU runner and peak memory tips over. Cap test-step concurrency at 4 (PR affected-tests + push full-run). CPU-bound workload, so wall-clock cost is minimal; peak memory becomes bounded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu * fix(qa): bump repo-relative path in the newly-merged declarative-mcp dogfood test + regen lockfile after merge showcase-declarative-mcp.dogfood.test.ts landed on main (#3062) against the old packages/dogfood location; after the packages/qa migration its '../../../examples/app-showcase' URL resolved inside packages/. One more level, same as the other conformance tests. Lockfile regenerated after taking main's side in the merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BtxNeUaGmvUYr8FwtUNu --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a1f6816 commit 1ab8752

118 files changed

Lines changed: 1308 additions & 132 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/owner-id-anchor-and-bulk-write-scoping.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ row-level scoping keys off to decide who may update/delete a record.
5555
now correctly affects zero rows instead of all of them.
5656

5757
Proven end-to-end on the real showcase app
58-
(`packages/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts`) and pinned
58+
(`packages/qa/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts`) and pinned
5959
in the ADR-0096 authz-conformance ledger (`ownership-anchor-guard`,
6060
`bulk-write-owner-scoping`).

.github/workflows/ci.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,26 @@ jobs:
107107
# spec sits at the root of the dependency graph, so spec-touching PRs
108108
# still run (close to) everything — but the many PRs that don't touch
109109
# spec skip the bulk of the 75-package matrix.
110+
# --concurrency=4: turbo's default (10) oversubscribes the 4-vCPU
111+
# hosted runner. As the task graph grew past ~100 tasks (connector
112+
# packages + qa gates), parallel vitest workers + tsup DTS builds
113+
# started exhausting runner memory: plugin-audit#test died with ZERO
114+
# output (kernel OOM-kill signature) deterministically on the runner
115+
# while passing everywhere else — first seen on main @4f8c2d1,
116+
# reproduced twice on #3037. Matching concurrency to the core count
117+
# bounds peak memory; the job is CPU-bound anyway.
110118
- name: Run affected tests (PR)
111119
if: github.event_name == 'pull_request'
112120
env:
113121
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
114-
run: pnpm turbo run test --affected
122+
run: pnpm turbo run test --affected --concurrency=4
115123

116124
# Push to main: full run, but exclude spec's plain test task — the
117125
# coverage step below executes the exact same 250-file spec suite once,
118126
# with coverage. Previously the spec suite ran twice per push.
119127
- name: Run all tests (push)
120128
if: github.event_name == 'push'
121-
run: pnpm turbo run test --filter=!@objectstack/spec
129+
run: pnpm turbo run test --filter=!@objectstack/spec --concurrency=4
122130

123131
- name: Generate coverage report
124132
if: github.event_name == 'push'

.github/workflows/lint.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,15 @@ jobs:
123123
# bad signal to copy from. tsup transpiles them without a full typecheck,
124124
# so build alone will not catch type drift — typecheck them explicitly.
125125
# They import from built workspace packages, so the packages must be built
126-
# first for cross-package type resolution to succeed.
126+
# first for cross-package type resolution to succeed. The examples'
127+
# dependency closure is requested EXPLICITLY (`./examples/*^...` = deps
128+
# of the examples, not the examples themselves): the bare `./packages/*`
129+
# glob only matches direct children, and the connector packages the
130+
# showcase imports were previously built only by accident — through
131+
# dogfood's dependency chain, which broke when dogfood moved to
132+
# packages/qa/ (#3037).
127133
- name: Build workspace packages
128-
run: pnpm exec turbo run build --filter='./packages/*'
134+
run: pnpm exec turbo run build --filter='./packages/*' --filter='./examples/*^...'
129135

130136
- name: Type check example apps
131137
run: pnpm --filter './examples/*' run typecheck

.github/workflows/spec-liveness-check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ name: Spec Liveness Check
99
#
1010
# ADR-0054 (prove-it-runs): bound high-risk 'live' properties must carry a `proof`
1111
# pointing to a dogfood test that declares the matching `@proof:` tag. The gate also
12-
# triggers on packages/dogfood/** so deleting/renaming a proof re-runs this check and
12+
# triggers on packages/qa/dogfood/** so deleting/renaming a proof re-runs this check and
1313
# the dangling reference is caught (the proof files live outside packages/spec/).
1414

1515
on:
1616
pull_request:
1717
types: [opened, synchronize, reopened]
1818
paths:
1919
- 'packages/spec/**'
20-
- 'packages/dogfood/**'
20+
- 'packages/qa/dogfood/**'
2121

2222
permissions:
2323
contents: read

content/docs/permissions/authorization.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ Five mechanisms — four CI-time, one runtime — make the security posture a
318318
`--update-access-matrix` — the snapshot's git diff is the review artifact.
319319
Opt-in, format, and workflow: **[Access-Matrix Snapshot Gate](/docs/permissions/access-matrix)**.
320320

321-
- **Conformance matrix** (`packages/dogfood/test/authz-conformance.matrix.ts`,
321+
- **Conformance matrix** (`packages/qa/dogfood/test/authz-conformance.matrix.ts`,
322322
ADR-0056 D10): every authorization primitive sits in exactly one honest
323323
state — `enforced` (must name its enforcement site; high-risk rows must
324324
reference an end-to-end dogfood proof), `experimental`, or `removed`. A new

content/docs/permissions/delegated-administration.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Writes to the governed tables (`sys_user_position`,
6767
| No scope, no admin | Holders of plain CRUD on the RBAC tables with **no** scope are refused: administration is a scoped capability now, not a side effect of table access |
6868

6969
Every rule above is exercised end-to-end by the showcase permission zoo
70-
(`packages/dogfood/test/showcase-permission-zoo.dogfood.test.ts`): the
70+
(`packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts`): the
7171
in-subtree allowlisted assignment passes with a `granted_by` stamp; the
7272
out-of-subtree anchor, the off-allowlist grant (to self), and the
7373
`manageBindings: false` binding write are each refused.

docs/adr/0054-runtime-proof-for-authorable-surface.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ misbehaves at runtime.
2929

3030
A metadata-driven platform whose authors are AI cannot ship unproven liveness
3131
for the primitives that matter. The static pointer must be upgradable to a
32-
**runtime proof** — a [`@objectstack/dogfood`](../../packages/dogfood) test that
32+
**runtime proof** — a [`@objectstack/dogfood`](../../packages/qa/dogfood) test that
3333
authors the property against the real, in-process stack and asserts the runtime
3434
result.
3535

docs/adr/0060-conformance-ledger-platform-pattern.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,6 @@ indirection.
202202
## References
203203

204204
ADRs 0020, 0049, 0054, 0056 (D10), 0058 (D7). Issue #1887. Existing instances:
205-
`packages/dogfood/test/authz-conformance.{matrix,test}.ts`,
206-
`packages/dogfood/test/expression-conformance.{ledger,test}.ts`. Helper home:
205+
`packages/qa/dogfood/test/authz-conformance.{matrix,test}.ts`,
206+
`packages/qa/dogfood/test/expression-conformance.{ledger,test}.ts`. Helper home:
207207
`packages/verify/`. Target surface: `packages/spec/src/data/validation.zod.ts`.

docs/adr/0076-objectql-core-tiering.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0076: objectql is the data engine — relocate metadata management (protocol) out of it; enforce the boundary; defer the engine repo-split
22

3-
**Status**: Proposed (2026-06-28, rev. 9) — D1–D12 below. D9 step-1 (interface segmentation) shipped in #2429; OQ#7 resolved (keep `metadata-protocol` name). rev.9 adds **D12 (honest capabilities** — discovery must not report stub/fallback services as real; the analytics fallback + dev stubs are marked honestly, not deleted) and corrects the D10 analytics note (deliberate fallback + `replaceService`, not a collision). — v12 assessment. Verified 2026-07-16: D1 (metadata-protocol extraction + back-compat re-export), D2 (core-boundary ratchet test), D9-step1 (segmented protocol interfaces) confirmed in code; D3 capability/profile contract unbuilt; D12 schema half landed but runtime enforcement missing (`http-dispatcher.ts` `svcAvailable` still hardcodes `status:'available'` for every service); D7/D10/D11-decomposition deferred as designed.
3+
**Status**: Proposed (2026-06-28, rev. 9) — D1–D12 below. D9 step-1 (interface segmentation) shipped in #2429; OQ#7 resolved (keep `metadata-protocol` name). rev.9 adds **D12 (honest capabilities** — discovery must not report stub/fallback services as real; the analytics fallback + dev stubs are marked honestly, not deleted) and corrects the D10 analytics note (deliberate fallback + `replaceService`, not a collision). — v12 assessment. Verified 2026-07-16: D1 (metadata-protocol extraction + back-compat re-export), D2 (core-boundary ratchet test), D9-step1 (segmented protocol interfaces) confirmed in code; D3 capability/profile contract unbuilt; D12 framework side landed in #3028 (standardized `__serviceInfo` marker — OQ#11 — honored by BOTH discovery builders; `svcAvailable` no longer hardcodes `available`; analytics fallback reports `degraded`, `/realtime` no longer advertised) — the console-side consumer update ("trust only `handlerReady:true`") remains for the cross-repo window; D7/D10/D11-decomposition deferred as designed (OQ#9/OQ#10 resolved below, #3037).
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (sys_metadata overlay substrate), [ADR-0025](./0025-plugin-package-distribution.md) (plugin package distribution), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (open-core boundary), [ADR-0048](./0048-cross-package-metadata-collision.md) (package id is the addressing unit), [ADR-0066](./0066-unified-authorization-model.md) (secure-by-default, posture-gated bypass)
66
**Consumers**: **new** `@objectstack/metadata-protocol` (receives `protocol` + `sys-metadata-repository` + `metadata-diagnostics`), `@objectstack/objectql` (loses protocol → becomes a lean data engine; keeps a back-compat re-export), `@objectstack/metadata-core` (gains the `SysMetadataEngine` interface), `@objectstack/plugin-security`, `@objectstack/plugin-sharing`, `@objectstack/spec`, and out-of-tree embedders — notably `../objectbase` (its `gateway`).
@@ -204,6 +204,6 @@ import { SecurityPlugin } from '@objectstack/plugin-security';
204204
6. **Data-facade home** — does the `DataProtocol` impl live in the engine-adjacent transport layer / `rest`, or a small `@objectstack/protocol-data`? (It is thin and transport-shaped.)
205205
7. **Metadata package name (post-segmentation)****Resolved: keep `@objectstack/metadata-protocol`** (already published; renaming has ~0 benefit and real churn). The `protocol` suffix is a low-cost naming exception — the contract is in `@objectstack/spec/api`; a README note in the package should clarify impl-vs-contract.
206206
8. **Per-domain versioning** — once segmented, do capability protocols get independent version markers / a `getCapabilities()` discovery method?
207-
9. **dispatcher vs rest-server overlap**are `runtime/http-dispatcher` (~3.8k) and `rest/rest-server` (~5.1k) redundant central transport layers? Consolidate or delineate (D11).
208-
10. **Validate multi-adapter**write a second `IHttpServer` adapter (thin Workers/Express) to prove the port is free of Hono-isms before relying on it (D11).
209-
11. **D12 stub marker**standardise the self-identifying marker: reuse `_dev`, introduce `__stub: true`, or a richer per-service capability descriptor? (Decide when D12 is implemented.)
207+
9. **dispatcher vs rest-server overlap****Resolved (audit, #2462): delineate, do not merge.** They partition the API surface by domain rather than duplicating it: in a standard boot REST owns data/meta/batch/import-export/search/email/forms/sharing/reports/approvals/security, the dispatcher bridge owns mcp/ai/graphql/keys/automation/actions/storage/i18n/analytics/health/ready. Only **discovery** and **packages** are genuinely double-mounted (first-registration-wins — the D11 smell to fix), and the dispatcher's `/data`/`/meta`/`/ui`/`/notifications`/`/share-links`/`/security` `dispatch()` branches are **not mounted** in a standard boot (they are live only when `HttpDispatcher` is used directly as a callable by out-of-tree adapters). D11 worklist: (1) kill or delegate the dead callable branches; (2) give packages + discovery a single owner; (3) extract per-domain handlers; (4) unify env-resolution on the injected `KernelResolver` seam.
208+
10. **Validate multi-adapter****Resolved (validated, #2462): the port is free of hard Hono-isms.** A zero-dependency `node:http` reference adapter (`@objectstack/http-conformance`, private QA gate under `packages/qa/`) runs the dispatcher bridge + REST generator unchanged; a cross-adapter conformance suite (40 assertions incl. `/data` CRUD roundtrip, `:param` routing, 404/405 semantics, SSE streaming, discovery) passes identically on both adapters. Findings: the Hono adapter's Host-header backfill is adapter-local (a Fetch-API artifact, not a port leak); all remaining Hono coupling is confined to the `getRawApp()` escape hatch (metadata HMR, cloud-connection/marketplace routes, static/SPA + CORS + Server-Timing), whose consumers feature-detect and degrade. Follow-up for D11: codify the two soft extensions consumers already rely on (`res.write`/`res.end` for SSE, `getPort()`) and 404/405 semantics into the `IHttpServer` contract.
209+
11. **D12 stub marker****Resolved (#2462): standard `__serviceInfo` self-descriptor** (`ServiceSelfInfoSchema` in `spec/api/discovery.zod.ts`, read via `readServiceSelfInfo()`), with plugin-dev's legacy `_dev: true` normalized to `{ status: 'stub', handlerReady: false }`. Both discovery builders honor it; the analytics fallback self-identifies as `degraded`.

docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,5 @@ Execution landed across both repos; differences from the proposal are noted inli
113113
- **§6 guardrails**`@objectstack/lint` `validate-semantic-roles`: undeclared `Field.group` reference, declared-but-unreferenced group, dangling `stageField`/`highlightFields` pointers; plus `field-group-shadowed` (added by the #2548 follow-up): a group whose every visible member is hoisted into the detail highlight strip (or is the record title) renders on forms but never on detail pages.
114114
- **PR4 (legacy-path deletion)**objectui#2546 removed the monolith `DetailView` branch in `RecordDetailView` together with the `detail.renderViaSchema` kill-switch and the `?renderViaSchema=0` debug param; schema-driven is the only path.
115115
- **Cross-surface consumption** — kanban default card fields ride `highlightFields` (objectui#2541), alongside grids/lists/detail strip.
116-
- **Verification** — parse: `@objectstack/spec` suite; served pipeline: `packages/dogfood/test/semantic-roles.dogfood.test.ts`; real-backend browser pass over the four detail shapes (grouped / ungrouped / `stageField: false` / related-heavy): framework#3019, runbook + results in `docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md`, and a permanent Playwright spec in `examples/app-showcase/e2e/detail-shapes.spec.ts`.
116+
- **Verification** — parse: `@objectstack/spec` suite; served pipeline: `packages/qa/dogfood/test/semantic-roles.dogfood.test.ts`; real-backend browser pass over the four detail shapes (grouped / ungrouped / `stageField: false` / related-heavy): framework#3019, runbook + results in `docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md`, and a permanent Playwright spec in `examples/app-showcase/e2e/detail-shapes.spec.ts`.
117117
- **Consequence surfaced by the browser pass** — because detail bodies hide strip fields, a fully-highlighted group silently disappears from detail pages. Judged working-as-intended (one curated list, every surface) but author-surprising — hence the `field-group-shadowed` warning and the semantic-zoo fixture keeping one non-highlighted member per group.

0 commit comments

Comments
 (0)