You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
html-tier page source: a type attribute overwrites the SDUI component discriminator — silently when the value names another registered type #13957
Found while auditing content/docs/ui/react-pages.mdx for #13737 (docs-only change, PR #13955). Not fixed there — this is a parser defect, not a docs one.
Mechanism
packages/sdui-parser/src/parse.ts, at the end of parseElement(), builds the node as:
constnode: SchemaElement={type: tag, ...props};
props is spread aftertype: tag, so an authored type attribute overwrites the component discriminator the tag name established. Nothing downstream restores it: compile() in index.ts returns parsed.tree as-is, and validateTree then reads manifest.components[node.type] — the value the author wrote, not the tag they wrote.
The react tier has an explicit rescue for exactly this collision (the author's value is preserved beside the discriminator as specType, objectui#2880). The html tier has none, and the html tier is the one whose whole point is that unreviewed and AI-authored source is safe to accept.
Why it is silent rather than loud
type is a member of BASE_PROPS in packages/sdui-parser/src/validate.ts:
/** Base props every node may carry (mirrors BaseSchema) -- never "unknown prop". */constBASE_PROPS=newSet(['type','id','className','style','visible','visibleOn','disabled','disabledOn','children']);
so the unknown-prop warning is skipped for it by design. Two outcomes, neither good:
the value names another registered type — e.g. a flex element carrying type="grid". validateTree finds grid in the manifest and every check passes. Zero diagnostics, and the page renders a grid where the author wrote a flex. This is the silent one.
the value names nothing registered — e.g. an object-chart element carrying type="bar", which is the shape a react-tier author would carry over. The diagnostic is unknown-component, message "bar" is not a known component. Loud, but it names the value and never says that a type attribute caused it, so it reads as a missing plugin rather than as a bad prop.
object-chart in the repo's sdui.manifest.json declares inputs objectName, data, filter, aggregate, drillDown — no type — so the second case is reachable purely by carrying a documented react-tier prop across tiers.
Evidence
Read from source at origin/main; I did not execute it, and no browser or app run is behind this report.
packages/sdui-parser/src/parse.ts — the spread, in parseElement()
packages/sdui-parser/src/validate.ts — BASE_PROPS includes type; visit() looks up manifest.components[node.type]
sdui.manifest.json — 57 component keys; object-chart declares no type input
Suggested direction (not a decision)
The contract-first shape is to refuse it at parse time rather than to alias it: when a type attribute is authored on an element, that is a name collision with the envelope's discriminator and the parser can say so — one diagnostic naming both the tag and the attribute. Whether the html tier should instead carry the react tier's specType rescue is a protocol question (ADR-0080), and someone who owns that decision should take it; a consumer-side accommodation in a renderer would be the wrong end.
Mechanism independently re-derived on origin/main — domain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. ⛔ Not a grading.domain:*, priority and type remain triage's field; this comment adds evidence only.
Both load-bearing claims hold:
claim
verdict
the spread order overwrites the discriminator
✅ packages/sdui-parser/src/parse.ts:85 — const node: SchemaElement = { type: tag, ...props }; — props is spread aftertype: tag
it is silent by design
✅ packages/sdui-parser/src/validate.ts:23-24 — BASE_PROPS contains 'type', so the unknown-prop warning is skipped for it
⚠️One reading I could not complete, stated rather than left implicit:git grep specType over packages/sdui-parser/src returns nothing. That is ⛔ not a contradiction of the card's react-tier rescue claim — the card cites objectui#2880, i.e. a different repository, which this seat cannot read from here. An absence measured in the wrong place is not evidence. ⇒ whoever grades this should confirm the rescue exists there before relying on the asymmetry.
Why this is offered as evidence rather than left as a bare observation
⭐ The two facts compose into a shape worth naming: the html tier is the one whose stated purpose is that unreviewed and AI-authored source can be accepted safely, and on that tier an authored attribute silently redirects which component renders — silently becausetype is on the never-warn list, which is correct for every other member of that list.
⇒ ⛔ Not asserting a severity, ⛔ not asserting a type. Recording only that the "silent" half is not an oversight to be patched at the warning layer: type is on that list deliberately, so a remedy that just removes it from BASE_PROPS would make every legitimate type-bearing node warn. ⚠️ Whoever picks this up should not read the fix as a one-line list edit.
Provenance
Surfaced by the #13737 dev while auditing content/docs/ui/react-pages.mdx for html-tier traps — an audit this seat ordered as a docs prerequisite, which found a parser defect instead. ⭐ Filed rather than folded in, correctly: PR #13955 is docs-only and this is packages/sdui-parser.
⚠️ The card arrived with no labels at all. finding added by this seat as the awaiting-first-grading marker, ⛔ nothing else.
Worktree: dedicated per-task worktree off origin/main (dev creates it)
Domain: domain:devx
File surface: packages/sdui-parser/src/parse.ts (+ tests) for the refusal + spread-order fix; a census-driven sweep of content/docs/** and examples/** html-tier sources carrying type= (each occurrence corrected in the same PR per ruling point 1); docs/adr/0080-* short amendment per ruling point 5 (governed limb ⇒ the whole PR is a DRAFT with human merge, never armed by an agent); changeset. ⛔ BASE_PROPS in validate.ts unchanged (ruling point 3). ⛔ If the fix turns out to need a BaseSchema change, that half is domain:spec's — stop and report, do not edit in place (triage note upheld by ruling point 5).
Container & model: mode:subagent, claude-opus-5. Tier readout (dispatch-gates --tier, derived): "Model tier — no path-derived mandate … floor sonnet · default opus · ceiling fable." PM judgment: opus for implementation; the clause-② gate below is the tier control.
Clause-②: content limb YES per ruling point 6 (html-tier accept-set narrows: pages that compile today with type= will be refused) ⇒ the PR carries needs:contract-review from creation, and per the ruling the director seat reviews and clears it (本席复审) — this seat will not strip that label itself.
{
"issue": 13957,
"status": "done",
"premise_still_valid": true,
"pr": "https://github.com/objectstack-ai/objectstack/pull/14274",
"branch": "claude/issue-13957-html-tier-type-collision",
"files_changed": [
"packages/sdui-parser/src/parse.ts",
"packages/sdui-parser/src/__tests__/type-attribute-collision.test.ts",
"content/docs/ui/react-pages.mdx",
"docs/adr/0080-ai-authored-ui-jsx-source.md",
"scripts/adr-anchors/packages__sdui-parser__src__parse.ts.json",
".changeset/sdui-parser-type-attribute-refusal.md"
],
"census": {
"population": 0,
"method": "TS-AST walk for kind:'html'|'jsx' pages with a source literal, plus a real open-tag reader (quotes and braces balanced) over every fenced snippet - not a line regex, which misses a type= on the 3rd line of a multi-line element",
"A_real_html_tier_sources": "0 of 3 html-tier pages carry a type= attribute: examples/app-showcase/src/ui/pages/capability-map.page.ts, command-center-jsx.page.ts, start-here.page.ts. No other kind:'html'|'jsx' source with a source literal exists under examples/**, content/**, packages/**, apps/**, docs/**, skills/**",
"B_content_docs_snippets": "11 hits, 0 html-tier - api/error-handling-client.mdx:182 (react), deployment/validating-metadata.mdx:221 (react ObjectChart), plugins/adding-a-metadata-type.mdx:168 (react), protocol/objectql/types.mdx:111 and :516 (plain HTML input illustration), protocol/objectui/concept.mdx:64, :163, :357 (react), protocol/objectui/index.mdx:147 (react), ui/react-pages.mdx:147 (react Block) and :445 (react ObjectChart)",
"C_supplementary_sweep": "8 hits outside content/docs, 0 html-tier - skills/objectstack-ui/SKILL.md:776 (explicitly react-page ObjectChart), packages/client-react/README.md x5 (react), content/blog/metadata-driven-architecture.mdx:425 (react), docs/adr/0048:71 (a MetaRef illustration)",
"corrections_made": "none needed in source. One documentation statement corrected: content/docs/ui/react-pages.mdx said the html tier's type attribute 'overwrites it', which this change makes false; it now names the refusal and the diagnostic"
},
"summary": "Premise re-verified on fresh origin/main before any edit: parse.ts still built the node as { type: tag, ...props } and validate.ts's BASE_PROPS still contained 'type'. parseAttr now refuses an authored type attribute with ONE forbidden-attr error naming both the tag and the attribute, replacing both prior branches (the silent one where the value named another registered type, and the misdirected unknown-component one). parseElement now builds { ...props, type: tag } as the ruled defense in depth. BASE_PROPS untouched, no specType alias, no warning grace period, no BaseSchema change - so nothing is handed to domain:spec. The refusal reuses the EXISTING forbidden-attr code rather than minting a new one, which is load-bearing: check:sdui-lockstep holds this copy's diagnostic-code set byte-equal to objectui's at the pinned revision, so a new code on one side only would red it with code-drift, and the remedy would be an objectui port plus a pin bump that this card does not own. The gate is green on the branch and its verdict line is quoted in the PR body. One deliberate refinement beyond the two ruled halves: the refused attribute is dropped rather than parked as the __forbidden_type sentinel the other refusals use, because that sentinel reaches validateTree and draws a second unknown-prop warning naming a key nobody wrote - which is the loud-but-misdirected species this card exists to remove, and the ruling asked for ONE diagnostic. The existing sentinel behaviour for ref/key/dangerouslySetInnerHTML/on* is unchanged. PR is a DRAFT (docs/adr/** governed limb) and carries needs:contract-review from creation per ruling point 6; the director seat clears it, not this seat.",
"adr_amendment": "docs/adr/0080-ai-authored-ui-jsx-source.md - one append-only section at end of file, 'Amendment (2026-09-01 - the `type` attribute is refused on the html tier)'. Quotes the ruling's point 2 verbatim and untranslated, records the three declined alternatives (specType on this tier, a warning grace period, a fix at the warning layer), the spread-order half and why it is correct only behind the refusal, and the zero-population census. Append-only and minimal on purpose: #13556's repo-wide ADR anchor migration is in flight on docs/adr/** concurrently. Also added scripts/adr-anchors/packages__sdui-parser__src__parse.ts.json so parse.ts names the decision it stands on (Prime Directive #13).",
"tests": "Union re-ran at final head 6dff387. (1) pnpm --filter @objectstack/sdui-parser exec vitest run - 7 files, 138 tests passed, 7 of them new (type= naming another registered type; type= naming nothing; a bare type; a nested element; a clean element with no regression; plus 3 spread-order tests). (2) ABLATION from the committed tree, mutation proved on disk each leg by blob id against HEAD:packages/sdui-parser/src/parse.ts and by anchored text counts, restore proved by git diff HEAD empty plus blob equality, absolute paths in an EXIT/INT/TERM trap. NO REBUILD IS INVOLVED and none is claimed: these tests import through relative source specifiers (../index.js, ../parse.js), not through the package exports, so nothing resolves to dist/. Leg 1 refusal removed, new spread order kept => 5 RED. Leg 2 refusal removed AND old spread order restored => 6 RED. Leg 3 refusal kept, old spread order restored => 8 GREEN. Leg 3 is reported as measured rather than smoothed over: the spread-order half is UNOBSERVABLE through the public API while the refusal stands, because the refused attribute never reaches props to be spread. That is the ruled relationship (拒绝使覆盖不可达,顺序修复是防御纵深) and it is now stated in the test file so a green suite is not misread as proof the order is load-bearing alone. (3) Consumer: pnpm --filter @objectstack/lint exec vitest run over validate-jsx-pages.test.ts, its production-witness, and page-walk-conversion-parity - 3 files, 17 tests passed. (4) pnpm --filter @objectstack/sdui-parser exec tsc --noEmit clean, and --listFiles confirms the new test file IS in the program (checked because a typecheck that excludes tests is a green over source nothing read). (5) pnpm lint - repo-wide eslint . --no-inline-config, FULL scan, green, 95s under the shared verify lock - not narrowed, so no narrowing needs declaring.",
"checks_run": {
"derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 68 families from the real change set (64 by path + 6 by kind, 2 overlapping); harvested with --commands and all 68 run. Exit codes captured BEFORE any pipe.",
"green": "61 of 68 green, including check:sdui-lockstep, check:adr-anchors, check:dispatcher-error-vocabulary, check:doc-authoring, check:doc-anchors, check:docs-single-h1, check:corpus-claim-drift, check:react-page-adapter-contract, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:published-files, check:objectui-changeset, check:pm-governed-merges, check:changeset-gate-self-tests. Plus the three the dispatch named directly: check:ratchet-remedy-authority (green), check:declared-population-live (green, 156 of 200 families reach the tree), check:nul-bytes (green, 7806 files, no raw control bytes) and a manual control-byte grep over the diff (no hits).",
"not_measured": "7 non-zero exits, every one a stated PREREQUISITE, none a finding - and read as NOT MEASURED, not as red: check-test-completeness (exit 3, needs a saved turbo run test log; the gate's own text names this branch NOT MEASURED), check:dual-build-cjs-loads (exit 3, needs the whole workspace built), check:type-check-debt (deferred, needs the built closure; sdui-parser carries NO debt-ledger entry so the ratchet has nothing to say about it), scripts/pm/check-half-states.mjs (exit 124, a network gate, timed out at 300s), and three that were re-run GREEN after building the consumer closure with turbo run build --filter=@objectstack/lint... : check:doc-formula-expressions, check:doc-security-posture, spec check:docs. check:skill-examples still exits 1 but only after reaching and clearing the 'skills + docs' surface this diff edits, stopping on an unbuilt client-react/dist. CI runs all of these with a real build.",
"final_head_rerun": "At final head 6dff387: check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:objectql-double-limit, check-keyed-text-bounds, check-comment-mask-adoption, check:doc-authoring, check:sdui-lockstep, check:adr-anchors, sdui-parser tsc --noEmit, and the full parser suite - all green."
},
"mcp_calls": "1 - one targeted mcp search_issues against objectstack-ai/objectui for the duplicate check. CHANNEL SWITCH DECLARED: repo-scoped REST reads work for this seat (200 on both repos) and carried the issue body, all 4 comments, the label write and its read-back, the PR creation and its full body read-back, and the objectui issue filing; the /search/issues REST endpoint answers 403, so the duplicate check went through one MCP call as the channel rules prescribe. It returned 8 items (non-empty), so no control-word probe was needed.",
"open_questions": [],
"followups_filed": [
"objectui#7235 - the renderer/preview copy of sdui-parser still builds markHtmlTierNode({ type: tag, ...props }) with no refusal, so the two copies now differ in accepted grammar. Filed unassigned, no labels, for that repo's triage. The direction is the SAFE one (objectstack runs the save gate and is now the stricter side, so a refused page never reaches a renderer that would misread it - the opposite of the save-clean-renders-inert failure the lockstep exists to prevent), but the console's live edit preview runs that copy, so an author currently sees no diagnostic while typing and a refusal on save. The issue carries the port recipe and notes that no new diagnostic code is needed on either side."
],
"out_of_scope_findings": [
"filed as objectui#7235: the objectui copy of sdui-parser needs the same parse-time refusal and spread-order fix"
]
}
PM review record (devx seat, session session_01WLJQhde67SeTccsmnBVarV) — PR #14274 verified against GitHub, ready for the two human gates:
CI converged green on head 6dff387: 38/38 check-runs, zero red (includes check:sdui-lockstep — the refusal reuses the existing forbidden-attr code precisely so the pinned diagnostic-code set stays byte-equal across repos).
Ruling compliance checked on the diff: census ran with an AST-grade reader — population 0 across all html-tier sources (3 pages) and every fenced snippet (19 hits, all react-tier or plain HTML illustration), so no in-repo corrections were owed; one diagnostic names tag + attribute and replaces both prior branches; spread order fixed as defense-in-depth; BASE_PROPS untouched; no specType, no grace period, no BaseSchema change (nothing handed to spec). The one refinement beyond the ruling's letter — dropping the refused attribute instead of parking the __forbidden_type sentinel — serves the ruling's "one diagnostic" clause (the sentinel would draw a second, misdirected unknown-prop warning) and is documented in the PR.
Sibling-copy gap filed as objectui#7235 (console preview copy still accepts the grammar; safe direction — the save gate is now the stricter side).
Remaining gates, neither this seat's: (1) needs:contract-review on the PR — the director seat reviews and clears per ruling point 6 (本席复审); (2) docs/adr/** governed limb — a human merges the draft. This seat will run the same-window close-out (label strip + origin/main probes) after the human merge.
Director seat, summon #9, session session_01WXyGTWPbbreqXow7Z2pZCk — the reviewer the ruling's point 6 named (本席复审). Tier fuse, both strings verbatim: served claude-fable-5-1, CONTRACT_REVIEW_TIER = claude-fable-5 (floor reading, named for veto in the round report). Reviewed from the PR diff and the parser source on the PR head; objectui-side claims read on objectui origin/main (a8d5c71).
The six ruling points (2026-09-01, comment 5494531584), each checked against the diff
Census first — population 0, measured with a TS-AST walk over every kind:'html' | 'jsx' page with a source literal (3 pages) plus an open-tag reader over every fenced snippet (docs 11 hits, supplementary 8 hits, all react-tier or plain-HTML illustration). One documentation sentence that stated the old behaviour corrected (content/docs/ui/react-pages.mdx). ✓
Loud refusal, one diagnostic naming tag + attribute, both branches replaced, no specType, no grace period — parse.ts:156-167: forbidden-attr with Attribute "type" is not allowed on <tag> — on this tier the tag name IS the component…; pinned: the previously silent case (flex carrying type="grid") now compiles ok:false with exactly one forbidden-attr, the previously misdirected case emits no unknown-component, a bare type and a nested element are covered. ✓
Spread order in the same PR, refusal at parse not at the warning layer — { ...props, type: tag } at parse.ts:137; validate.ts byte-identical to origin/main (diffstat empty), so BASE_PROPS keeps type. The ablation is reported honestly: the order is unobservable through the public API while the refusal stands, and the test file says so. ✓
Meta-criterion conflict recorded — in the ADR amendment. ✓
ADR-0080 amendment, short — one append-only section quoting the ruling verbatim and untranslated; scripts/adr-anchors/packages__sdui-parser__src__parse.ts.json added; no BaseSchema change, so nothing is handed to domain:spec. ✓
Clause-② yes, draft + needs:contract-review from creation, director clears — as delivered. ✓
① Derived judgments
change
judged
html-tier accept set narrows: any type= attribute on any element is refused (forbidden-attr, severity error)
correct — exactly the ruled narrowing, one payment, no staged window
the refused attribute is dropped rather than parked as the __forbidden_type sentinel the other refusals use
accepted refinement — the sentinel reaches validateTree and draws a second unknown-prop naming a key nobody wrote, the misdirected species this card exists to remove; ref / key / dangerouslySetInnerHTML / on* keep their sentinel behaviour unchanged
the existing forbidden-attr code is reused, no new code minted
correct and load-bearing — scripts/check-sdui-lockstep.mjs holds this copy's diagnostic-code set byte-equal to objectui's at the pinned revision (green at head); a new code on one side is the dialect split that gate exists to catch
the two copies now differ in strictness
safe direction — objectui's parse.ts:94 still builds markHtmlTierNode({ type: tag, ...props }) with no refusal (read on objectui a8d5c71), so the save gate is stricter than the renderer and a refused page never reaches a renderer that would misread it; objectui#7235 filed for the port
one fact the ruling's census did not cover
#14490 (filed in this stroke, finding): sdui.manifest.json has two components that declare an input named type — action:button and action:icon. On the html tier that input was never expressible (it overwrote the discriminator before), and it is now refused with a prescription — "write the tag of the component you meant" — that has no answer for those two. Not a defect in this PR under the ruling; a triage input (rename at source vs a component-aware prescription)
② semver
.changeset/sdui-parser-type-attribute-refusal.md — @objectstack/sdui-parser: minor, the narrowing stated in full, migration surface zero — launch-window convention. ✓ (Nit, not blocking: it does not spell BREAKING the way #14395's accept-set changeset does; nothing authorable is removed or renamed, so no ADR-0087 disposition is owed and the gate agrees.)
node scripts/pm/check-governed-merges.mjs --test on the final file list: docs/adr/** ×1 → the whole PR is human-merged. (1) This verdict. (2) PR stays draft — no seat flips it, enqueues it, or arms auto-merge. (3) needs:contract-review stripped on the PR in this stroke (the card never carried it); review requested from os-zhuang and hotlong with the draft bit preserved. (4) Listed under "awaiting a human merge" in the round report. CI at head: 38/38 green, mergeable_state: clean, no drift on the touched files since the PR's base. After the human merge the devx seat runs its same-window close-out as it recorded.
Close-out — PR #14274 merged to main as 0e68ed25c by os-zhuang at 12:23:55Z (governed terminal: docs/adr/** limb ⇒ human merge; contract review PASS by the director seat, comment 5507302283). Session session_01WLJQhde67SeTccsmnBVarV.
Close-out probes on origin/main996cb1d48 (verify worktree, 0e68ed25c an ancestor):
packages/sdui-parser/src/parse.ts: the docblock names the collision (:26 "An authored type= attribute is a NAME COLLISION with it, and the parser …"), the spread-order fix is recorded (:128), and the refusal diagnostic is live (:162 "… component, so already means type "tag". Delete the attribute, or …") — forbidden-attr, one diagnostic naming tag + attribute, no new code minted (check:sdui-lockstep green at the merged head).
packages/sdui-parser/src/__tests__/type-attribute-collision.test.ts present on origin/main (blob 40b9fa898).
Sibling-copy port tracked as objectui#7235; the action:button / action:icon manifest-input finding is #14490. This seat's part (dispatch, PM review 5499718658, same-window close-out) is complete.
pm:dispatched removed with this comment; the card closed via the PR's Fixes #13957.
Activity
claude commentedon Aug 31, 2026
Mechanism independently re-derived on
origin/main—domain:devxexecution PM seat (#6023), sessionsession_01Pk26oZ12t5N1hwGW1m1MgC. ⛔ Not a grading.domain:*, priority and type remain triage's field; this comment adds evidence only.Both load-bearing claims hold:
packages/sdui-parser/src/parse.ts:85—const node: SchemaElement = { type: tag, ...props };—propsis spread aftertype: tagpackages/sdui-parser/src/validate.ts:23-24—BASE_PROPScontains'type', so theunknown-propwarning is skipped for itgit grep specTypeoverpackages/sdui-parser/srcreturns nothing. That is ⛔ not a contradiction of the card's react-tier rescue claim — the card cites objectui#2880, i.e. a different repository, which this seat cannot read from here. An absence measured in the wrong place is not evidence. ⇒ whoever grades this should confirm the rescue exists there before relying on the asymmetry.Why this is offered as evidence rather than left as a bare observation
⭐ The two facts compose into a shape worth naming: the html tier is the one whose stated purpose is that unreviewed and AI-authored source can be accepted safely, and on that tier an authored attribute silently redirects which component renders — silently because
typeis on the never-warn list, which is correct for every other member of that list.⇒ ⛔ Not asserting a severity, ⛔ not asserting a type. Recording only that the "silent" half is not an oversight to be patched at the warning layer:⚠️ Whoever picks this up should not read the fix as a one-line list edit.
typeis on that list deliberately, so a remedy that just removes it fromBASE_PROPSwould make every legitimatetype-bearing node warn.Provenance
Surfaced by the #13737 dev while auditing
content/docs/ui/react-pages.mdxfor html-tier traps — an audit this seat ordered as a docs prerequisite, which found a parser defect instead. ⭐ Filed rather than folded in, correctly: PR #13955 is docs-only and this ispackages/sdui-parser.findingadded by this seat as the awaiting-first-grading marker, ⛔ nothing else.Generated by Claude Code
huangyiirene commentedon Sep 1, 2026
裁决:响亮拒绝 —— parse 期诊断,⛔ 不引
specType入 html tier;普查前置(维护者 2026-09-01,总监批 #24)项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 维护者对本批逐字:「同意」。content/docs/**与 examples 的 html-tier 源,量出type=属性的存量人口(定迁移面;非零则逐处修正随同一 PR);type=属性 = 与信封判别符的名字冲突,parser 当场一条诊断同时点名标签与属性(两支合一:值撞已注册组件的静默支、值撞不存在组件的错向支,都被这条诊断替代);⛔specType别名不引入 html tier(消费端容错、扩面到第二个 tier);⛔ 不设 warning 宽限期(不考虑渐进);{ type: tag, ...props }展开顺序修复(拒绝使覆盖不可达,顺序修复是防御纵深);BASE_PROPS里的type保留(对其余成员正确),拒绝在 parse 层做,⛔ 不动 warning 层;specType救援 ⇒ ⛔ 不改绑,两条原则并行不悖;BaseSchema本身,该半边转domain:spec唯一所有者,⛔ 不在 parser 侧就地改;type=页面将被拒)⇒ draft +needs:contract-review同笔,本席复审。状态转移(同笔)
needs-user-decision→pm:queue;priority:p2domain:devx不动(type 已记 Bug)。Generated by Claude Code
baozhoutao commentedon Sep 1, 2026
Claim:
session_01WLJQhde67SeTccsmnBVarV(devx execution seat, seat post [PM seat] domain:devx @ objectstack — ⏳ vacant #6023)claude/issue-13957-html-tier-type-collisionorigin/main(dev creates it)domain:devxpackages/sdui-parser/src/parse.ts(+ tests) for the refusal + spread-order fix; a census-driven sweep ofcontent/docs/**andexamples/**html-tier sources carryingtype=(each occurrence corrected in the same PR per ruling point 1);docs/adr/0080-*short amendment per ruling point 5 (governed limb ⇒ the whole PR is a DRAFT with human merge, never armed by an agent); changeset. ⛔BASE_PROPSinvalidate.tsunchanged (ruling point 3). ⛔ If the fix turns out to need aBaseSchemachange, that half isdomain:spec's — stop and report, do not edit in place (triage note upheld by ruling point 5).mode:subagent,claude-opus-5. Tier readout (dispatch-gates--tier, derived): "Model tier — no path-derived mandate … floor sonnet · default opus · ceiling fable." PM judgment: opus for implementation; the clause-② gate below is the tier control.type=will be refused) ⇒ the PR carriesneeds:contract-reviewfrom creation, and per the ruling the director seat reviews and clears it (本席复审) — this seat will not strip that label itself.packages/sdui-parser.sql-driver.tshave rotted — 4 of 4 sampled resolve to unrelated code, and the ADRs read as if they still point at the mechanism they name #13556's in-flight repo-wide ADR anchor migration (also a governed draft). Both are human-merged; whichever merges second may need a trivial rebase ondocs/adr/0080-*. The amendment is instructed to be minimal and append-only to keep that conflict surface near zero.Generated by Claude Code
fix(sdui-parser): refuse an authored `type` attribute on the html tier (
claude commentedon Sep 1, 2026
os-dev-report
{ "issue": 13957, "status": "done", "premise_still_valid": true, "pr": "https://github.com/objectstack-ai/objectstack/pull/14274", "branch": "claude/issue-13957-html-tier-type-collision", "files_changed": [ "packages/sdui-parser/src/parse.ts", "packages/sdui-parser/src/__tests__/type-attribute-collision.test.ts", "content/docs/ui/react-pages.mdx", "docs/adr/0080-ai-authored-ui-jsx-source.md", "scripts/adr-anchors/packages__sdui-parser__src__parse.ts.json", ".changeset/sdui-parser-type-attribute-refusal.md" ], "census": { "population": 0, "method": "TS-AST walk for kind:'html'|'jsx' pages with a source literal, plus a real open-tag reader (quotes and braces balanced) over every fenced snippet - not a line regex, which misses a type= on the 3rd line of a multi-line element", "A_real_html_tier_sources": "0 of 3 html-tier pages carry a type= attribute: examples/app-showcase/src/ui/pages/capability-map.page.ts, command-center-jsx.page.ts, start-here.page.ts. No other kind:'html'|'jsx' source with a source literal exists under examples/**, content/**, packages/**, apps/**, docs/**, skills/**", "B_content_docs_snippets": "11 hits, 0 html-tier - api/error-handling-client.mdx:182 (react), deployment/validating-metadata.mdx:221 (react ObjectChart), plugins/adding-a-metadata-type.mdx:168 (react), protocol/objectql/types.mdx:111 and :516 (plain HTML input illustration), protocol/objectui/concept.mdx:64, :163, :357 (react), protocol/objectui/index.mdx:147 (react), ui/react-pages.mdx:147 (react Block) and :445 (react ObjectChart)", "C_supplementary_sweep": "8 hits outside content/docs, 0 html-tier - skills/objectstack-ui/SKILL.md:776 (explicitly react-page ObjectChart), packages/client-react/README.md x5 (react), content/blog/metadata-driven-architecture.mdx:425 (react), docs/adr/0048:71 (a MetaRef illustration)", "corrections_made": "none needed in source. One documentation statement corrected: content/docs/ui/react-pages.mdx said the html tier's type attribute 'overwrites it', which this change makes false; it now names the refusal and the diagnostic" }, "summary": "Premise re-verified on fresh origin/main before any edit: parse.ts still built the node as { type: tag, ...props } and validate.ts's BASE_PROPS still contained 'type'. parseAttr now refuses an authored type attribute with ONE forbidden-attr error naming both the tag and the attribute, replacing both prior branches (the silent one where the value named another registered type, and the misdirected unknown-component one). parseElement now builds { ...props, type: tag } as the ruled defense in depth. BASE_PROPS untouched, no specType alias, no warning grace period, no BaseSchema change - so nothing is handed to domain:spec. The refusal reuses the EXISTING forbidden-attr code rather than minting a new one, which is load-bearing: check:sdui-lockstep holds this copy's diagnostic-code set byte-equal to objectui's at the pinned revision, so a new code on one side only would red it with code-drift, and the remedy would be an objectui port plus a pin bump that this card does not own. The gate is green on the branch and its verdict line is quoted in the PR body. One deliberate refinement beyond the two ruled halves: the refused attribute is dropped rather than parked as the __forbidden_type sentinel the other refusals use, because that sentinel reaches validateTree and draws a second unknown-prop warning naming a key nobody wrote - which is the loud-but-misdirected species this card exists to remove, and the ruling asked for ONE diagnostic. The existing sentinel behaviour for ref/key/dangerouslySetInnerHTML/on* is unchanged. PR is a DRAFT (docs/adr/** governed limb) and carries needs:contract-review from creation per ruling point 6; the director seat clears it, not this seat.", "adr_amendment": "docs/adr/0080-ai-authored-ui-jsx-source.md - one append-only section at end of file, 'Amendment (2026-09-01 - the `type` attribute is refused on the html tier)'. Quotes the ruling's point 2 verbatim and untranslated, records the three declined alternatives (specType on this tier, a warning grace period, a fix at the warning layer), the spread-order half and why it is correct only behind the refusal, and the zero-population census. Append-only and minimal on purpose: #13556's repo-wide ADR anchor migration is in flight on docs/adr/** concurrently. Also added scripts/adr-anchors/packages__sdui-parser__src__parse.ts.json so parse.ts names the decision it stands on (Prime Directive #13).", "tests": "Union re-ran at final head 6dff387. (1) pnpm --filter @objectstack/sdui-parser exec vitest run - 7 files, 138 tests passed, 7 of them new (type= naming another registered type; type= naming nothing; a bare type; a nested element; a clean element with no regression; plus 3 spread-order tests). (2) ABLATION from the committed tree, mutation proved on disk each leg by blob id against HEAD:packages/sdui-parser/src/parse.ts and by anchored text counts, restore proved by git diff HEAD empty plus blob equality, absolute paths in an EXIT/INT/TERM trap. NO REBUILD IS INVOLVED and none is claimed: these tests import through relative source specifiers (../index.js, ../parse.js), not through the package exports, so nothing resolves to dist/. Leg 1 refusal removed, new spread order kept => 5 RED. Leg 2 refusal removed AND old spread order restored => 6 RED. Leg 3 refusal kept, old spread order restored => 8 GREEN. Leg 3 is reported as measured rather than smoothed over: the spread-order half is UNOBSERVABLE through the public API while the refusal stands, because the refused attribute never reaches props to be spread. That is the ruled relationship (拒绝使覆盖不可达,顺序修复是防御纵深) and it is now stated in the test file so a green suite is not misread as proof the order is load-bearing alone. (3) Consumer: pnpm --filter @objectstack/lint exec vitest run over validate-jsx-pages.test.ts, its production-witness, and page-walk-conversion-parity - 3 files, 17 tests passed. (4) pnpm --filter @objectstack/sdui-parser exec tsc --noEmit clean, and --listFiles confirms the new test file IS in the program (checked because a typecheck that excludes tests is a green over source nothing read). (5) pnpm lint - repo-wide eslint . --no-inline-config, FULL scan, green, 95s under the shared verify lock - not narrowed, so no narrowing needs declaring.", "checks_run": { "derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 68 families from the real change set (64 by path + 6 by kind, 2 overlapping); harvested with --commands and all 68 run. Exit codes captured BEFORE any pipe.", "green": "61 of 68 green, including check:sdui-lockstep, check:adr-anchors, check:dispatcher-error-vocabulary, check:doc-authoring, check:doc-anchors, check:docs-single-h1, check:corpus-claim-drift, check:react-page-adapter-contract, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:published-files, check:objectui-changeset, check:pm-governed-merges, check:changeset-gate-self-tests. Plus the three the dispatch named directly: check:ratchet-remedy-authority (green), check:declared-population-live (green, 156 of 200 families reach the tree), check:nul-bytes (green, 7806 files, no raw control bytes) and a manual control-byte grep over the diff (no hits).", "not_measured": "7 non-zero exits, every one a stated PREREQUISITE, none a finding - and read as NOT MEASURED, not as red: check-test-completeness (exit 3, needs a saved turbo run test log; the gate's own text names this branch NOT MEASURED), check:dual-build-cjs-loads (exit 3, needs the whole workspace built), check:type-check-debt (deferred, needs the built closure; sdui-parser carries NO debt-ledger entry so the ratchet has nothing to say about it), scripts/pm/check-half-states.mjs (exit 124, a network gate, timed out at 300s), and three that were re-run GREEN after building the consumer closure with turbo run build --filter=@objectstack/lint... : check:doc-formula-expressions, check:doc-security-posture, spec check:docs. check:skill-examples still exits 1 but only after reaching and clearing the 'skills + docs' surface this diff edits, stopping on an unbuilt client-react/dist. CI runs all of these with a real build.", "final_head_rerun": "At final head 6dff387: check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:objectql-double-limit, check-keyed-text-bounds, check-comment-mask-adoption, check:doc-authoring, check:sdui-lockstep, check:adr-anchors, sdui-parser tsc --noEmit, and the full parser suite - all green." }, "mcp_calls": "1 - one targeted mcp search_issues against objectstack-ai/objectui for the duplicate check. CHANNEL SWITCH DECLARED: repo-scoped REST reads work for this seat (200 on both repos) and carried the issue body, all 4 comments, the label write and its read-back, the PR creation and its full body read-back, and the objectui issue filing; the /search/issues REST endpoint answers 403, so the duplicate check went through one MCP call as the channel rules prescribe. It returned 8 items (non-empty), so no control-word probe was needed.", "open_questions": [], "followups_filed": [ "objectui#7235 - the renderer/preview copy of sdui-parser still builds markHtmlTierNode({ type: tag, ...props }) with no refusal, so the two copies now differ in accepted grammar. Filed unassigned, no labels, for that repo's triage. The direction is the SAFE one (objectstack runs the save gate and is now the stricter side, so a refused page never reaches a renderer that would misread it - the opposite of the save-clean-renders-inert failure the lockstep exists to prevent), but the console's live edit preview runs that copy, so an author currently sees no diagnostic while typing and a refusal on save. The issue carries the port recipe and notes that no new diagnostic code is needed on either side." ], "out_of_scope_findings": [ "filed as objectui#7235: the objectui copy of sdui-parser needs the same parse-time refusal and spread-order fix" ] }Generated by Claude Code
baozhoutao commentedon Sep 1, 2026
PM review record (devx seat, session
session_01WLJQhde67SeTccsmnBVarV) — PR #14274 verified against GitHub, ready for the two human gates:6dff387: 38/38 check-runs, zero red (includescheck:sdui-lockstep— the refusal reuses the existingforbidden-attrcode precisely so the pinned diagnostic-code set stays byte-equal across repos).BASE_PROPSuntouched; nospecType, no grace period, noBaseSchemachange (nothing handed to spec). The one refinement beyond the ruling's letter — dropping the refused attribute instead of parking the__forbidden_typesentinel — serves the ruling's "one diagnostic" clause (the sentinel would draw a second, misdirectedunknown-propwarning) and is documented in the PR.sql-driver.tshave rotted — 4 of 4 sampled resolve to unrelated code, and the ADRs read as if they still point at the mechanism they name #13556's in-flight anchor migration);scripts/adr-anchors/mapping added soparse.tsnames its decision.Remaining gates, neither this seat's: (1)
needs:contract-reviewon the PR — the director seat reviews and clears per ruling point 6 (本席复审); (2)docs/adr/**governed limb — a human merges the draft. This seat will run the same-window close-out (label strip + origin/main probes) after the human merge.Generated by Claude Code
hotlong commentedon Sep 2, 2026
Contract review — PR #14274 @
6dff387193— PASS (governed surface: stays draft, human merge)Director seat, summon #9, session
session_01WXyGTWPbbreqXow7Z2pZCk— the reviewer the ruling's point 6 named (本席复审). Tier fuse, both strings verbatim: servedclaude-fable-5-1,CONTRACT_REVIEW_TIER=claude-fable-5(floor reading, named for veto in the round report). Reviewed from the PR diff and the parser source on the PR head; objectui-side claims read on objectuiorigin/main(a8d5c71).The six ruling points (2026-09-01, comment
5494531584), each checked against the diffkind:'html' | 'jsx'page with a source literal (3 pages) plus an open-tag reader over every fenced snippet (docs 11 hits, supplementary 8 hits, all react-tier or plain-HTML illustration). One documentation sentence that stated the old behaviour corrected (content/docs/ui/react-pages.mdx). ✓specType, no grace period —parse.ts:156-167:forbidden-attrwith Attribute "type" is not allowed on <tag> — on this tier the tag name IS the component…; pinned: the previously silent case (flexcarryingtype="grid") now compilesok:falsewith exactly oneforbidden-attr, the previously misdirected case emits nounknown-component, a baretypeand a nested element are covered. ✓{ ...props, type: tag }atparse.ts:137;validate.tsbyte-identical toorigin/main(diffstat empty), soBASE_PROPSkeepstype. The ablation is reported honestly: the order is unobservable through the public API while the refusal stands, and the test file says so. ✓scripts/adr-anchors/packages__sdui-parser__src__parse.ts.jsonadded; noBaseSchemachange, so nothing is handed todomain:spec. ✓needs:contract-reviewfrom creation, director clears — as delivered. ✓① Derived judgments
type=attribute on any element is refused (forbidden-attr, severity error)__forbidden_typesentinel the other refusals usevalidateTreeand draws a secondunknown-propnaming a key nobody wrote, the misdirected species this card exists to remove;ref/key/dangerouslySetInnerHTML/on*keep their sentinel behaviour unchangedforbidden-attrcode is reused, no new code mintedscripts/check-sdui-lockstep.mjsholds this copy's diagnostic-code set byte-equal to objectui's at the pinned revision (green at head); a new code on one side is the dialect split that gate exists to catchparse.ts:94still buildsmarkHtmlTierNode({ type: tag, ...props })with no refusal (read on objectuia8d5c71), so the save gate is stricter than the renderer and a refused page never reaches a renderer that would misread it; objectui#7235 filed for the portfinding):sdui.manifest.jsonhas two components that declare an input namedtype—action:buttonandaction:icon. On the html tier that input was never expressible (it overwrote the discriminator before), and it is now refused with a prescription — "write the tag of the component you meant" — that has no answer for those two. Not a defect in this PR under the ruling; a triage input (rename at source vs a component-aware prescription)② semver
.changeset/sdui-parser-type-attribute-refusal.md—@objectstack/sdui-parser: minor, the narrowing stated in full, migration surface zero — launch-window convention. ✓ (Nit, not blocking: it does not spell BREAKING the way#14395's accept-set changeset does; nothing authorable is removed or renamed, so no ADR-0087 disposition is owed and the gate agrees.)③ Boundary flags
open_questions: []; follow-up objectui#7235 exists. ✓Landing — governed terminal, four pieces
node scripts/pm/check-governed-merges.mjs --teston the final file list:docs/adr/**×1 → the whole PR is human-merged. (1) This verdict. (2) PR stays draft — no seat flips it, enqueues it, or arms auto-merge. (3)needs:contract-reviewstripped on the PR in this stroke (the card never carried it); review requested from os-zhuang and hotlong with the draft bit preserved. (4) Listed under "awaiting a human merge" in the round report. CI at head: 38/38 green,mergeable_state: clean, no drift on the touched files since the PR's base. After the human merge the devx seat runs its same-window close-out as it recorded.Generated by Claude Code
claude commentedon Sep 2, 2026
Close-out — PR #14274 merged to
mainas0e68ed25cby os-zhuang at 12:23:55Z (governed terminal:docs/adr/**limb ⇒ human merge; contract review PASS by the director seat, comment 5507302283). Sessionsession_01WLJQhde67SeTccsmnBVarV.Close-out probes on
origin/main996cb1d48(verify worktree,0e68ed25can ancestor):packages/sdui-parser/src/parse.ts: the docblock names the collision (:26"An authoredtype=attribute is a NAME COLLISION with it, and the parser …"), the spread-order fix is recorded (:128), and the refusal diagnostic is live (:162"… component, so already means type "tag". Delete the attribute, or …") —forbidden-attr, one diagnostic naming tag + attribute, no new code minted (check:sdui-lockstepgreen at the merged head).packages/sdui-parser/src/__tests__/type-attribute-collision.test.tspresent onorigin/main(blob40b9fa898)..changeset/sdui-parser-type-attribute-refusal.md(minor),content/docs/ui/react-pages.mdx,docs/adr/0080-ai-authored-ui-jsx-source.md(append-only amendment), the test,parse.ts,scripts/adr-anchors/packages__sdui-parser__src__parse.ts.json.Sibling-copy port tracked as objectui#7235; the
action:button/action:iconmanifest-input finding is #14490. This seat's part (dispatch, PM review 5499718658, same-window close-out) is complete.pm:dispatchedremoved with this comment; the card closed via the PR'sFixes #13957.Generated by Claude Code