Skip to content

feat(spec,lint): author-time rejection for unknown component types in spec-reserved namespaces; Phase-2 readiness read keeps both members - #13117

Merged
os-trump merged 1 commit into
mainfrom
claude/issue-12950-page-component-phase2
Aug 29, 2026
Merged

feat(spec,lint): author-time rejection for unknown component types in spec-reserved namespaces; Phase-2 readiness read keeps both members#13117
os-trump merged 1 commit into
mainfrom
claude/issue-12950-page-component-phase2

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#12950

Part of the #12183 decomposition (Phase 2) — #12183 remains open for Phase 1 and the renderer work.

The ruling this implements

Maintainer, 2026-08-26 (comment 5419723931 on #12183), verbatim: 「其他接受你的建议」 accepting:

Phase 2: global:search / global:notifications — read platform data-source readiness FIRST; a member with no data-source plan inside the horizon is retired per B (ADR-0087 channel, author-time loud rejection) rather than left as a declared-unrendered trap.
Rides with the ruling: PageComponentSchema.type's union accepts arbitrary strings at parse time, which is why an unimplemented type reaches the renderer as a plain string; the author-time rejection half gets handled alongside whichever phase touches the enum.

Work item 1 — readiness read (evidence faces, per member)

global:search — data source SHIPPED, member STAYS

FaceEvidence
What a renderer needsa cross-object search backend
ProtocolMetadataProtocol.searchAllpackages/metadata-protocol/src/protocol.ts line 10066: implemented cross-object search (hits with title/snippet/record, per-object caps, enable.searchable/apiEnabled opt-outs); capability advertised at line 5219 (search: typeof this.searchAll === 'function')
RESTGET /api/v1/searchpackages/rest/src/rest-route-ledger.ts line 297, with conformance coverage (search-clone-route-schema-conformance.test.ts drives the mount; metadata-protocol's search-clone-schema-conformance.test.ts parses the real searchAll producer)
SDKpackages/client/src/index.ts line 4617 — client search method, "501s on kernels without searchAll"
Sibling consumerobjectui packages/react/src/hooks/useRecordSearch.ts already reads /api/v1/search
Recorded plantargeted issue search returned the parent card itself (the search channel's positive control); the ruling plus this card are the recorded trail

global:notifications — data source SHIPPED with a live UI consumer, member STAYS

FaceEvidence
What a renderer needsa per-user notification feed with read-state
Recorded plan + landed convergenceADR-0012 (notification platform) and ADR-0030 (convergence): "the bell reads sys_inbox_message"
Storagesys_inbox_message L5 materialization (packages/services/service-messaging/src/objects/inbox-message.object.ts) + sys_notification_receipt read-state spine
Service + RESTMessagingService.listInbox backs GET /api/v1/notifications (messaging-service.ts lines 346-390; runtime/src/domains/notifications.ts binds the authenticated user), whole-inbox unreadCount badge semantics per the #6363 ruling; service-realtime pings online users
Sibling consumerobjectui Console bell — app-shellInboxPopover / useHomeInbox — reads the same feed today
Recorded plan searchtargeted issue search returned zero further cards; the in-session positive control (previous query returning the parent card) validates the channel, so the zero is a reading

Decision per the ruling: retirement B is conditioned on "no data-source plan inside the horizon". Both members have data sources that are not merely planned but shipped, REST-exposed, SDK-exposed, and consumed by the sibling repo's own console chrome. Both members STAY declared; this is reported back rather than retired. Neither face is ambiguous, so no needs_decision arm fires.

Report-back for PM routing: the renderer gap itself remains (both members still draw the placeholder until objectui lands renderers). With readiness now evidenced, the natural follow-up is a Phase-1-style renderer card for these two in objectui — routed by the spec seat that owns the #12183 decomposition, not filed unilaterally here.

Work item 2 — retirement per B: NO-OP, with evidence

No member retires (see above), so: enum UNCHANGED, no ADR-0087 conversion, no RETIRED_KEYS_BY_MAJOR entry, no liveness-ledger movement, no baseline row removed. The keep verdict is pinned in component-type-vocabulary.test.ts ("the Phase-2 members stay declared") so a later retirement is a conscious edit through the spec-property-retirement playbook.

Work item 3 — author-time rejection for unknown type strings

The measured extension story (why a bare union collapse is not viable)

A union collapse breaks all of the above. A blanket namespace constraint (reject any colon string outside the enum) breaks the three plugin widgets and record:line_items.

The mechanism the tree supports: a namespace-shaped validate-time gate

New gating rule component-type-unknown (packages/lint/src/validate-component-types.ts), on all three CLI commands via the shared authoring registry, severity error:

Contract section — exact accept-set movements

SurfaceBeforeAfter
PageComponentType enum33 membersUNCHANGED — no member retired
PageComponentSchema.type parseunion of enum and open stringUNCHANGED — pinned; only the .describe() prose now states the namespace claim
Author-time (os validate / os build / os lint)any string acceptedinside the 8 enum-derived namespaces, only enum ∪ map rows ∪ ledger accepted; violations are error-severity component-type-unknown findings with suggestions
Runtime publish doorno type-level gateUNCHANGED (deferred with written reason)
@objectstack/spec/ui exports+6 (vocabulary sets and predicates; api-surface/ui.json and export-origins/ui.json regenerated by tooling)

Evidence (all runs at head 63b361403, after the final commit)

  • Spec: full suite 443 files / 11728 tests passed (the one initial failure was the export-origins freshness guard demanding regeneration for the new exports; regenerated via check:generated --fix, re-run green 22/22). typecheck green on all three legs (src tsc, scripts, test layer via check:test-typecheck). --listFiles proof: component-type-vocabulary.ts + page.zod.ts in the src program; component-type-vocabulary.test.ts in the test program.
  • Lint: full suite 82 files / 2322 passed, 5 skipped (includes the wiring guard verifying the gating claim, barrel-export and input-tier ratchets). typecheck green. NOT MEASURED: validate-component-types.test.ts is excluded from lint's tsc program (tsconfig.json excludes all test files — pre-existing package posture, not introduced here); it is exercised by vitest at runtime.
  • Consumers of the enum/map (grep over packages): cloud-connection 27 files passed, mcp 23 files passed, platform-objects 33 files passed (after building their dependency closures; the first run's 17 file-level failures were unresolved @objectstack/core dist imports on the unbuilt worktree, 0 test failures).
  • Generated artifacts: check:generated verdict "3 of 14 artifacts stale" → regenerated exactly those (gen:api-surface, gen:export-origins, gen:docs) via --fix; re-run green. check:authorable-surface green (verdict line in the gate log); the enum and authorable keys did not move, consistent with the playbook's enum-grain visibility table.
  • Full derived gate family (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, derived from the real changeset at head): 58 gates run with per-gate exit capture before any pipe — 56 exit 0, including check:doc-authoring, check:adr-0087-registration, check:type-check-debt ("31 ledger entries re-measured, none above its recorded number"), check:type-check-coverage, check:query-options-erasure, check:nul-bytes ("scanned 7286 text files, no raw ASCII control bytes"). Two are prerequisite-not-met, not red: check-test-completeness.mjs (requires the CI turbo test log as an argument) and pm/check-half-states.mjs (exit 3 "no reading at all" — needs a real GitHub token this container does not hold); both are CI/PM-owned runs.
  • End-to-end dist proof through the shared registry (what the CLI consumes, not the unit tests' src path): global:serch → one error finding with the global:search suggestion; global:search, record:line_items, mcp:connect-agent, flex → accepted.
  • Both directions pinned in tests: five refusal cases (typo, far-from-anything, nested tab child, slot-mounted, name-keyed page map) and seventeen acceptance cases each naming the extension-story face it protects.

The hotCRM repro pages are untouched, and no renderer work rides here (Phase 1 is objectui#6661; the two never share files).

Generated by Claude Code


Generated by Claude Code

… spec-reserved namespaces
The type-vocabulary half of the Component Placeholder gap: PageComponentSchema.type
keeps its deliberate open string arm (custom/registered components are measured,
load-bearing consumers of it), but the spec now answers for its own namespaces —
a type inside them that neither the enum, nor ComponentPropsMap, nor the evidenced
string-arm ledger declares is refused at author time by the new gating rule
component-type-unknown, with closest-spelling suggestions. The parse is unchanged;
the refusal lands at os validate / os build / os lint.
Readiness read for the Phase-2 members (evidence in the PR): global:search and
global:notifications both have shipped platform data sources, so per the ruling
both STAY declared — no retirement rides this change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 17 documentable anchor(s). ⚠️4 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/deployment/validating-metadata.mdx(via AUTHORING_RULES (symbol))

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

  • content/docs/releases/v15.mdx(via PageComponentSchema (symbol))
  • content/docs/releases/v17.mdx(via AUTHORING_RULES (symbol))

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

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, …) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b15d260d100c78bb8a2c600e7c8263c99775591fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 6e4f5790f98211aee7f8bb6a6a9d389e67421cd5 — the merge of head 63b361403a81885f45a6a0b6bdfecb1d66233472 into base b15d260d100c78bb8a2c600e7c8263c99775591f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6e4f5790f98211aee7f8bb6a6a9d389e67421cd5 && git checkout 6e4f5790f98211aee7f8bb6a6a9d389e67421cd5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b15d260d100c78bb8a2c600e7c8263c99775591f 63b361403a81885f45a6a0b6bdfecb1d66233472 && git checkout -B drift-repro b15d260d100c78bb8a2c600e7c8263c99775591f && git merge --no-ff 63b361403a81885f45a6a0b6bdfecb1d66233472
node scripts/docs-audit/affected-docs.mjs --json b15d260d100c78bb8a2c600e7c8263c99775591f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b15d260d100c78bb8a2c600e7c8263c99775591f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:ui tests tooling labels Aug 29, 2026
@os-trump
os-trump marked this pull request as ready for review August 29, 2026 06:01
@os-trump
os-trump enabled auto-merge August 29, 2026 06:02
@os-trump
os-trump added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit 225e769Aug 29, 2026
44 checks passed
@os-trump
os-trump deleted the claude/issue-12950-page-component-phase2 branch August 29, 2026 06:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uiteststooling

Projects

None yet

2 participants

@os-trump@claude