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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
sdui-parser (renderer/preview copy): a attribute still overwrites the html-tier discriminator — objectstack now refuses it at parse #7235
Surfaced while implementing objectstack#13957 (objectstack PR #14274), which lands the maintainer's 2026-09-01 ruling on the objectstack side. Filed rather than folded in: that PR is packages/sdui-parser in objectstack, and this half is this repo's copy.
What objectstack just changed
On a kind:'html' page the tag name is the node's type, so an authored type= attribute is a name collision with the envelope's discriminator. objectstack's copy of the parser now refuses it at parse time with one forbidden-attr error naming both the tag and the attribute, and builds the node as { ...props, type: tag } instead of { type: tag, ...props }.
Before that, two outcomes: when the authored value named another registered type (a flex element carrying type="grid"), the tree carried the author's value as its discriminator, the manifest resolved it, every check passed, and a different component rendered with zero diagnostics; when it named nothing registered, unknown-component blamed the value and read as a missing plugin.
This repo's copy still does the old thing
packages/sdui-parser/src/parse.ts here builds the node as:
props spread after type: tag, and no refusal in parseAttr — so the renderer-side and live-preview copy still accepts the attribute and still lets it win the discriminator slot.
Why this is filed and not urgent-by-default
The direction of the divergence is the safe one: objectstack runs the save gate and is now the stricter of the two, so a refused page never reaches a renderer that would misread it. That is the opposite of the save-clean-renders-inert failure the lockstep exists to prevent (objectstack#12719). ⚠️ But the two copies now differ in accepted grammar, and the live edit preview in the console runs this copy — an author gets no diagnostic while typing and a refusal on save.
Note the refusal deliberately reuses the existing forbidden-attr code rather than minting a new one, precisely so check:sdui-lockstep stays green in both directions; a port here needs no new code either.
What a port would be
Refuse a type attribute in parseAttr with the existing forbidden-attr code, one diagnostic naming both the tag and the attribute.
Reverse the spread: markHtmlTierNode({ ...props, type: tag }) — defense in depth, correct only because of (1); reversing it alone trades a silent overwrite for a silent discard.
⛔ Do not carry the react tier's specType rescue (objectui#2880) into the html tier — the ruling declined it by name; that rescue stays where it lives.
After it lands, objectstack re-records its side with pnpm gen:sdui-lockstep when the .objectui-sha pin moves.
The ruled decision and its rationale are recorded as an append-only amendment on objectstack's ADR-0080.
domain:ui PM seat, session_01UzHd6hDYatoDn17BuwKxnZ, R16. Assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.
⚠️yes and not a close call: this narrows the accepted grammar — the parser begins refusing a type= attribute it accepts today. Narrowing is the direction the principle guards hardest, so the carrier is owed even though the maintainer has ruled the direction. Hang it on both sides with the draft PR and report the --pair exit code (PM_SWEEP_REPO=objectstack-ai/objectui, exit by redirect, ⛔ never after a pipe).
⭐ Your precondition is verified — the upstream half has landed
⛔ Do not take this from the card; I measured it, because a port whose reference does not exist is not a port. On objectstackorigin/maine03cb88:
packages/sdui-parser/src/parse.ts:137 const node: SchemaElement = { ...props, type: tag };
packages/sdui-parser/src/parse.ts:156 if (name === DISCRIMINATOR_ATTR) { this.error('forbidden-attr', …) }
and on objectuiorigin/main, the old shape is still live:
packages/sdui-parser/src/parse.ts:94 const node: SchemaElement = markHtmlTierNode({ type: tag, ...props });
forbidden-attr occurrences in that file: 1 ← the code you reuse already exists here
⇒ reference implementation present, defect present, no new diagnostic code needed. ⚠️ Line numbers are from 2026-09-11T07:1xZ and main is moving fast under this lane — re-derive them.
⚠️One real difference between the two copies, so you do not "fix" it by mistake: objectstack builds a bare { ...props, type: tag }; objectui wraps in markHtmlTierNode(...). Keep the wrapper. The port is markHtmlTierNode({ ...props, type: tag }) — reverse the spread inside the existing call.
ZONE 1 — the port, four steps, ⛔ exactly as ruled
Refuse a type attribute in parseAttr, with the existingforbidden-attr code — ⛔ do not mint a new one. One diagnostic naming both the tag and the attribute. objectstack's message is the model and says why: on this tier the tag name is the component, so <flex> already means type flex; the author should delete the attribute or write the tag they meant.
Reverse the spread:markHtmlTierNode({ ...props, type: tag }). ⚠️ Defense in depth, and correct only because of (1) — the card is explicit that reversing it alone trades a silent overwrite for a silent discard. Both steps or neither.
⛔ Do NOT carry the react tier's specType rescue (objectui#2880) into the html tier. The ruling declined it by name. That rescue stays where it lives.
⛔ Not yours: objectstack re-records its side with gen:sdui-lockstep when the .objectui-sha pin moves. Name it in the report; do not do it.
⭐ Reusing forbidden-attr rather than minting a code is what keeps check:sdui-lockstep green in both directions — that is deliberate upstream design, not an accident. ⛔ A new code would red the lockstep.
ZONE 2 — what today's defect actually is, so your pins aim at it
Two outcomes today, and the first is the dangerous one:
authored type naming another registered type (a flex element carrying type="grid") → the author's value wins the discriminator slot, the manifest resolves it, every check passes, and a different component renders with zero diagnostics;
authored type naming nothing registered → unknown-component blames the value, reading as a missing plugin rather than as a forbidden attribute.
⇒ a pin that only covers the second case misses the silent one entirely.
ZONE 3 — stop conditions, each stated as the harm it guards
⭐ Stated as harms deliberately. If a location I give is wrong but the harm is untouched, keep going and say so — PM claims are in scope for falsification, and a clean stop on a falsified PM premise is a full success. This lane's dispatch has been falsified four times today and every one improved the result; one of those was a "lit" control of mine that was lit for the wrong reason.
Stop if refusing type would reject a document this repo's own fixtures or examples author today. The harm is a narrowing that breaks existing authored content — sweep examples/** and the schema catalog before you finish, and report the sweep either way.
Stop if check:sdui-lockstep reds in either direction. The harm is the two parsers silently diverging in accepted grammar, which is the exact failure this card exists to close.
Report, don't stop, if objectui's copy differs from objectstack's in any way beyond the spread order and the missing refusal. The two are meant to be a lockstep pair; an unrecorded third difference is worth knowing.
ZONE 4 — acceptance
⭐ The silent case first: an html-tier <flex type="grid"> now produces a forbidden-attr diagnostic and builds as type flex — assert both, because either alone leaves half the defect. ⛔ Do not assert only that an error was raised.
The unknown-component case: an unregistered authored value now reports forbidden-attr naming the attribute, ⛔ not unknown-component naming the value.
A node with no type attribute is byte-identical to today (regression guard) — and ⭐ pin that markHtmlTierNode's provenance marking survives the spread reversal, since that is the objectui-only wrapper and the reversal is where it would be lost.
Ablate each of the two steps separately and show what each one alone leaves broken — the card's own claim is that step 2 alone converts a silent overwrite into a silent discard, so that ablation should demonstrate exactly that.
Restore and prove the tree clean by state (git diff HEAD empty + blob hash back to HEAD's), ⛔ never by an exit code.
(The report JSON is the fenced block below. GitHub appends an attribution footer after it on every write - that footer is NOT part of the report; read to the closing fence, not to end of body.)
{
"issue": 7235,
"status": "done",
"branch": "claude/issue-7235-html-tier-discriminator-attr",
"pr": "https://github.com/objectstack-ai/objectui/pull/9125",
"premise_still_valid": true,
"summary": "Ported objectstack#13957 into objectui's copy of packages/sdui-parser, exactly as ruled: parseAttr now refuses a `type` attribute with the EXISTING forbidden-attr code (one diagnostic naming both the tag and the attribute, objectstack's message verbatim), and the node is composed as markHtmlTierNode({ ...props, type: tag }) with the objectui-only provenance wrapper kept and the spread reversed INSIDE the call. The react tier's specType rescue (objectui#2880) was not carried over. Step 4 is NOT done here and is named for the objectstack side: it re-records with `pnpm gen:sdui-lockstep` when the .objectui-sha pin moves. PM precondition re-verified on both trees before any edit (objectstack parse.ts now at lines 137 / 156 after the file moved; objectui origin/main 567f370 line 94 as described). One PM ACCEPTANCE CRITERION IS FALSIFIED and is reported rather than papered over: a node with no `type` attribute is NOT byte-identical to today. Reversing the spread moves `type` from the FIRST key to the LAST, so JSON.stringify of a parsed tree emits the same pairs in a different order; provenance.test.ts pinned that order and went red (expected ['className','type'] to deeply equal ['type','className']). The key SET, every value, the children and the html-tier marker are unchanged (measured). The pin was updated to the new order with the reason written beside it rather than relaxed to a sorted comparison, and the changeset calls the reordering out for consumers that diff serialized trees. Assignee was set by the PM (os-tesla) and is untouched; no second claim posted.",
"tests": "All exit codes captured by REDIRECT before any pipe; every build/test went through scripts/pm/os-verify-lock.sh with a stable OS_VERIFY_LOCK_SLOT (one 99 queue-timeout was resumed on the same slot, never re-queued from the back). Evidence quoted at the final commit 400737e. (1) `pnpm exec vitest run packages/sdui-parser/` -> VERDICT command-exit 0, 'Test Files 15 passed (15) / Tests 204 passed (204)' (was 14/194 before this change). (2) Every html-tier CONSUMER test in the repo - the 39 test files outside packages/sdui-parser that reference kind:'html', parseJsx or @object-ui/sdui-parser - VERDICT command-exit 0, 'Test Files 39 passed (39) / Tests 744 passed (744)'. (3) type-check: @object-ui/sdui-parser exit 0 (`tsc --noEmit && tsc -p tsconfig.test.json`, so the new test file IS typechecked); @object-ui/components exit 0 AFTER building its dependency closure - the first run's 4 errors were all 'Cannot find module @object-ui/react-runtime' plus cascaded implicit-any, i.e. an unbuilt tree, not a regression. (4) builds: `pnpm --filter '@object-ui/sdui-parser^...' build` and the same for components, both exit 0. (5) repo gates: check:control-bytes exit 0 (7313 files scanned), check:new-line-citations exit 0 (0 new citations), check:upstream-port-parity exit 0 (parse.ts is NOT in scripts/upstream-port-pin.json - verified, the pin holds only 3 tooling files), check:vi-mock-specifiers exit 0, check-changeset-presence exit 0 ('3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'). NOT MEASURED: check:sdui-registration-pins exits 2 = PREREQUISITE NOT MET (no apps/console/dist/assets to weigh); it pins bundle-time registrations and this diff adds/removes none - left to CI rather than run against nothing. (6) eslint: 4 changed files, `--no-inline-config --format json`, exit 0, zero problems. NARROWING DECLARED with all three readings: population 4799 lintable files read from eslint's OWN config (ESLint#isPathIgnored over `git ls-files`, not a guess); files linted 4, read from the --format json result length; invariance - eslint.config.js declares no projectService and no parserOptions.project (0 occurrences), so linting is not type-aware and this diff cannot move the verdict on any file it does not contain. (7) ABLATIONS, the two steps separately, each mutation PROVEN ON DISK before any result was read (anchor occurrence counts + git hash-object) and each restore proven BY STATE (git diff HEAD empty AND blob back to HEAD's 834bf3b0484c), never by an exit code; a `trap restore EXIT INT TERM` with absolute paths guarded every leg and no probe file survives. BASELINE (refusal + reversed spread): a flex element carrying type=grid gives type=flex, codes=[\"forbidden-attr\"], vitest exit 0, 17/17 green. ABLATION A = STEP 2 ALONE (refusal removed, spread still reversed): type=flex, codes=[], json {\"type\":\"flex\",\"gap\":4} - the authored value is gone with ZERO diagnostics, i.e. a SILENT DISCARD, exactly the card's claim; vitest exit 1, 5 red. ABLATION B = STEP 1 ALONE (refusal kept, old spread order restored): type=flex, codes=[\"forbidden-attr\"] - indistinguishable from the shipped state through the public API, which is why the order fix is documented as defense in depth; vitest exit 1 with 1 red, and that one red is only the key-order pin. ABLATION C = BOTH REMOVED (origin/main behaviour): type=GRID, codes=[] - the SILENT OVERWRITE, a grid rendering where the author wrote a flex; vitest exit 1, 7 red. (8) STOP CONDITION check:sdui-lockstep - GREEN in both directions, measured, not assumed. The gate lives in objectstack and the reference checkout there has no node_modules at all, so a bare run exits 3 = PREREQUISITE NOT MET ('the dependency typescript is not installed'), which is NOT a reading; it was therefore run from a faithful scratch copy of that tree with a resolvable typescript: `--self-test` exit 0 and the gate exit 0, 'byte-identical to objectui@53ded82bf7a4 over 214 grammar line(s) [blob 0131f27cf86d] and agrees on all 24 diagnostic code(s)'. For the half that gate structurally cannot see (objectui moving after the record), objectstack's OWN fingerprintOf was run against this branch and against objectui origin/main: grammar-region blob 0131f27cf86dbabec09b4ae55ae131403667e7e9 BEFORE and AFTER, byte-identical and equal to the recorded value (all hunks land at lines 24-190; the region delimiter is at line 392 of 605, and 605-392+1 = 214 matches the record's line count), and the diagnostic-code list identical before and after - this PR adds NO code. (9) STOP CONDITION fixture/example sweep - CLEAN, reported either way. Population: the 65 files referencing kind:'html' / parseJsx / @object-ui/sdui-parser, plus examples/**, content/docs/**, skills/** and e2e/**. Exactly ONE html-tier document in the repo authors a `type` attribute: action-type-input-html-tier.test.tsx row 3, a deliberate NEGATIVE CONTROL asserting the spelling does not work - so the harm the stop condition names (a narrowing that breaks existing authored content) is untouched and I kept going. examples/**: 8 type= hits, 0 in html-tier source (all data-obj-type DOM assertions and a script type=module in an index.html); the schema catalog is the JSON tier, untouched. content/docs/**: 6 hits, all react-tier Block type= or MDX callouts, neither parsed by this parser. apps/console/src/sdui-tiers-preview.tsx, the repo's own html-tier authoring example whose test asserts zero error diagnostics: 0 hits. The sweep regex is not vacuous - it found the negative control, which pre-exists this change. (10) Clause-2 carrier: `PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9125` run from the objectstack checkout, exit captured by redirect = 0, 'the clause-2 declaration is readable in the fixed spelling and both carriers agree'. needs:contract-review was hung on PR 9125 with the additive REST endpoint (HTTP 200) and READ BACK: ['package: components','tests','needs:contract-review','package: sdui-parser'] - nothing missing from union(read, target). (11) Both GitHub bodies read back after writing: the filed issue is 4162 bytes with 0 tag-shaped fragments and 0 closing keywords near an issue reference; the PR body's only post-write divergence starts at exactly the sent length, i.e. append-only (the platform's own footer), with the session reference preserved as a prose code span and 0 tag-shaped fragments stored.",
"mcp_calls": "4 - three search_issues (dedup) and one issue_write (the filed finding). Everything else went through git, the REST channel (probed 200 on a repo-scoped read at the start of the run) and the zero-quota local tree.",
"open_questions": [
{
"question": "objectui AGENTS.md commandment #-1 says the codebase is English-only including code comments, but the dispatch contract says a quoted Chinese maintainer ruling must stay verbatim because rewriting the quote rewrites the ruling. The ported docblock carries objectstack's verbatim quote of the 2026-09-01 ruling. Which rule governs a VERBATIM RULING QUOTE inside objectui source? Reporting rather than silently picking a side, per the conflict clause - though I did have to write something, and the choice is stated below.",
"options": [
"A - keep the verbatim Chinese quote in the ported docblock (what I did). Byte-faithful with objectstack's copy, and measurement says #-1 does not describe the tree: 743 files under packages/** already carry CJK in comments (60233 CJK characters). Note my FIRST measurement of this was a FALSE ZERO - a grep -rP with a packages/*/src glob reported 0 files, and a proper walk reported 743; I nearly removed the quote on the strength of that zero.",
"B - render the ruling in English in objectui source and leave the verbatim quote to objectstack's ADR-0080 amendment. Complies with #-1 as written, and loses nothing that is not recorded elsewhere - but it is a translation of a ruling, which the dispatch contract forbids.",
"C - a maintainer amendment to #-1 carving out verbatim ruling quotes, after which either spelling is unambiguous."
],
"recommendation": "A, because the stronger constraint is the one whose violation is not recoverable by reading another document: a translated ruling can drift from what was actually decided, whereas an English gloss of it is recoverable from the ADR. C is the durable fix and is cheap - #-1 currently has 743 counterexamples in the tree, so it is already not the rule anyone is following."
}
],
"out_of_scope_findings": [
"filed as objectstack#17645: the sdui-parser lockstep has drifted again SINCE the record was taken - objectui origin/main carries 26 diagnostic codes to objectstack's 24 (objectui-only: inert-quick-add from objectui#8285 and member-type-mismatch from objectui#8067, plus the extra source file kanban-quick-add.ts), all landed after the record was taken on 2026-09-05, i.e. inside the blind spot check-sdui-lockstep's own header names. Category (b), a declared contract violated: objectstack#12719's 'both copies byte-agree on the accepted grammar and on diagnostic codes'. Measured with objectstack's OWN fingerprintOf; the control PRE-EXISTS this change and is the same kind as the subject - the same extractor on the same objectui repo at the PINNED revision 53ded82 returns 24 and the matching file list, so it is demonstrably sensitive to exactly this drift. Latent today (the gate compares objectstack against the record and is green, exit 0) and it will red at the next .objectui-sha bump plus gen:sdui-lockstep, with or without this PR. Dedup: three search_issues queries, all NON-EMPTY so self-validating; the closest matches are objectstack#12719 and #12814, both CLOSED and both about earlier codes - this is a third instance of a known class, not a duplicate.",
"noted, not filed: the two copies of parse.ts differ in two further places beyond the spread order, the missing refusal and the objectui-only markHtmlTierNode wrapper - both COMMENT-ONLY with no behavioural difference. objectui's whitespace-collapse rationale cites objectui#5661 and 'rendered as Axpage' where objectstack's cites 'triage option (b), shared verbatim with the downstream copy'; and objectstack's interpretBrace carries a LOCKSTEP banner ('Change this block only together with the objectui copy') that this copy lacks. Reported under the brief's report-don't-stop clause. Not filed: comment wording is a documentation nit, and the banner's absence changes nothing a gate or a reader acts on since the grammar region is byte-identical and mechanically checked. Successor: the next port of this file, which will read both docblocks side by side.",
"noted, not filed: action-type-input-html-tier.test.tsx carries a Chinese phrase in a code comment, which AGENTS.md #-1 forbids. Pre-existing, and one of 743 files under packages/** that do - a repo-wide convention gap, not a defect, and filing one instance of 743 would be noise. Successor: none, which is why it is stated here; it needs a ruling on whether #-1 still describes the tree (see open_questions), not a card. IN SCOPE and DONE, by contrast: the prose in that same file that THIS change falsified - it asserted the parser composes { type: tag, ...props } and that row 3 fails with unknown-component on the overwritten discriminator. Both are now false. The prose is corrected and row 3 now asserts WHICH diagnostic appears (forbidden-attr naming the attribute, and NOT unknown-component), because its old assertions still passed after the change for an entirely different reason - a test passing while measuring nothing.",
"noted, not filed: check:sdui-registration-pins cannot run without a console bundle and exits 2 by design. Not a finding, recorded so the gate list in this report is honest about what was and was not measured."
]
}
Activity
os-zhuang commentedon Sep 5, 2026
分诊 ·
domain:ui/priority:p2/pm:queueAnchor read, not guessed.
packages/sdui-parser/src/parse.ts⇒domain:ui。复核 objectuiorigin/maina472b07(2026-09-05T01:02:08Z):⇒
props在type: tag之后展开,原样保留 ✅ —— 作者写的type=仍然赢得判别符槽位。文件里forbidden-attr只有 1 处,不是给type用的 ⇒parseAttr里没有拒绝 ✅。定级 p2
⭐ 分歧方向确实是安全的那一边,卡面说得对,本席采纳:objectstack 跑保存闸,现在是两者中更严格的一方,所以一个被拒的页面永远到不了会误读它的渲染器。 这与 objectstack#12719 那条 lockstep 要防的「保存干净、渲染惰性」正好相反。
⛔ 但不是 p3,理由是卡面点出的那一条:
⇒ ⭐ 作者在编辑器里看到的和保存时得到的,是两套语法。 这正是「先让人写完、再告诉他不行」的最差反馈时序,而预览存在的全部意义就是提前告诉他。
flex元素带type="grid"),树携带作者的值作为判别符,manifest 解析它,每一道检查都通过,而一个不同的组件被渲染出来 —— 零诊断。边界测试 —— 收紧,但改变已发布解析器的接受语法
移植后,今天在预览里能通过的页面会开始被预览拒绝 ⇒ 收窄 accept surface。⚠️ 但它只是追上保存闸已经在做的事 —— ⇒ 不是新裁决,是把已裁决的规则搬到第二个副本。⭐ 裁决与理由已作为 append-only 修订记录在 objectstack 的 ADR-0080 上,⛔ 不需要重新裁。
⭐ 移植清单 —— 四条,原样保留,第 2、3 条最容易做错
parseAttr里拒绝type属性,复用既有的forbidden-attr码,一条同时点名标签与属性的诊断。markHtmlTierNode({ ...props, type: tag })—— 纵深防御,而且它只有在 (1) 存在时才是正确的。⛔ 单独反转会把一次静默覆盖换成一次静默丢弃。specType救援(objectui#2880)带进 html 层 —— 裁决按名字拒绝了它;那个救援留在它原来的地方。.objectui-shapin 移动时用pnpm gen:sdui-lockstep重记它那一侧。⭐ 而第 1 条刻意复用既有码而不新铸一个,正是为了让
check:sdui-lockstep在两个方向上都保持绿 —— ⇒ 这一侧的移植同样不需要新码。 ⛔ 谁接手都不要为了「更清楚」新铸一个错误码,那会把一个已经绿的 lockstep 弄红。⛔ 本席不认领、不派发、不代裁。
Generated by Claude Code
os-tesla commentedon Sep 11, 2026
Claim: branch
claude/issue-7235-html-tier-discriminator-attrClause-②: yes
domain:uiPM seat,session_01UzHd6hDYatoDn17BuwKxnZ, R16. Assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.yesand not a close call: this narrows the accepted grammar — the parser begins refusing atype=attribute it accepts today. Narrowing is the direction the principle guards hardest, so the carrier is owed even though the maintainer has ruled the direction. Hang it on both sides with the draft PR and report the--pairexit code (PM_SWEEP_REPO=objectstack-ai/objectui, exit by redirect, ⛔ never after a pipe).⭐ Your precondition is verified — the upstream half has landed
⛔ Do not take this from the card; I measured it, because a port whose reference does not exist is not a port. On
objectstackorigin/maine03cb88:and on
objectuiorigin/main, the old shape is still live:⇒ reference implementation present, defect present, no new diagnostic code needed.⚠️ Line numbers are from 2026-09-11T07:1xZ and
mainis moving fast under this lane — re-derive them.{ ...props, type: tag }; objectui wraps inmarkHtmlTierNode(...). Keep the wrapper. The port ismarkHtmlTierNode({ ...props, type: tag })— reverse the spread inside the existing call.ZONE 1 — the port, four steps, ⛔ exactly as ruled
typeattribute inparseAttr, with the existingforbidden-attrcode — ⛔ do not mint a new one. One diagnostic naming both the tag and the attribute. objectstack's message is the model and says why: on this tier the tag name is the component, so<flex>already means typeflex; the author should delete the attribute or write the tag they meant.markHtmlTierNode({ ...props, type: tag }).specTyperescue (objectui#2880) into the html tier. The ruling declined it by name. That rescue stays where it lives.gen:sdui-lockstepwhen the.objectui-shapin moves. Name it in the report; do not do it.⭐ Reusing
forbidden-attrrather than minting a code is what keepscheck:sdui-lockstepgreen in both directions — that is deliberate upstream design, not an accident. ⛔ A new code would red the lockstep.ZONE 2 — what today's defect actually is, so your pins aim at it
Two outcomes today, and the first is the dangerous one:
typenaming another registered type (aflexelement carryingtype="grid") → the author's value wins the discriminator slot, the manifest resolves it, every check passes, and a different component renders with zero diagnostics;typenaming nothing registered →unknown-componentblames the value, reading as a missing plugin rather than as a forbidden attribute.⇒ a pin that only covers the second case misses the silent one entirely.
ZONE 3 — stop conditions, each stated as the harm it guards
⭐ Stated as harms deliberately. If a location I give is wrong but the harm is untouched, keep going and say so — PM claims are in scope for falsification, and a clean stop on a falsified PM premise is a full success. This lane's dispatch has been falsified four times today and every one improved the result; one of those was a "lit" control of mine that was lit for the wrong reason.
typewould reject a document this repo's own fixtures or examples author today. The harm is a narrowing that breaks existing authored content — sweepexamples/**and the schema catalog before you finish, and report the sweep either way.check:sdui-lockstepreds in either direction. The harm is the two parsers silently diverging in accepted grammar, which is the exact failure this card exists to close.ZONE 4 — acceptance
<flex type="grid">now produces aforbidden-attrdiagnostic and builds as typeflex— assert both, because either alone leaves half the defect. ⛔ Do not assert only that an error was raised.unknown-componentcase: an unregistered authored value now reportsforbidden-attrnaming the attribute, ⛔ notunknown-componentnaming the value.typeattribute is byte-identical to today (regression guard) — and ⭐ pin thatmarkHtmlTierNode's provenance marking survives the spread reversal, since that is the objectui-only wrapper and the reversal is where it would be lost.git diff HEADempty + blob hash back to HEAD's), ⛔ never by an exit code.Generated by Claude Code
os-tesla commentedon Sep 11, 2026
os-dev-report
(The report JSON is the fenced block below. GitHub appends an attribution footer after it on every write - that footer is NOT part of the report; read to the closing fence, not to end of body.)
{ "issue": 7235, "status": "done", "branch": "claude/issue-7235-html-tier-discriminator-attr", "pr": "https://github.com/objectstack-ai/objectui/pull/9125", "premise_still_valid": true, "summary": "Ported objectstack#13957 into objectui's copy of packages/sdui-parser, exactly as ruled: parseAttr now refuses a `type` attribute with the EXISTING forbidden-attr code (one diagnostic naming both the tag and the attribute, objectstack's message verbatim), and the node is composed as markHtmlTierNode({ ...props, type: tag }) with the objectui-only provenance wrapper kept and the spread reversed INSIDE the call. The react tier's specType rescue (objectui#2880) was not carried over. Step 4 is NOT done here and is named for the objectstack side: it re-records with `pnpm gen:sdui-lockstep` when the .objectui-sha pin moves. PM precondition re-verified on both trees before any edit (objectstack parse.ts now at lines 137 / 156 after the file moved; objectui origin/main 567f370 line 94 as described). One PM ACCEPTANCE CRITERION IS FALSIFIED and is reported rather than papered over: a node with no `type` attribute is NOT byte-identical to today. Reversing the spread moves `type` from the FIRST key to the LAST, so JSON.stringify of a parsed tree emits the same pairs in a different order; provenance.test.ts pinned that order and went red (expected ['className','type'] to deeply equal ['type','className']). The key SET, every value, the children and the html-tier marker are unchanged (measured). The pin was updated to the new order with the reason written beside it rather than relaxed to a sorted comparison, and the changeset calls the reordering out for consumers that diff serialized trees. Assignee was set by the PM (os-tesla) and is untouched; no second claim posted.", "tests": "All exit codes captured by REDIRECT before any pipe; every build/test went through scripts/pm/os-verify-lock.sh with a stable OS_VERIFY_LOCK_SLOT (one 99 queue-timeout was resumed on the same slot, never re-queued from the back). Evidence quoted at the final commit 400737e. (1) `pnpm exec vitest run packages/sdui-parser/` -> VERDICT command-exit 0, 'Test Files 15 passed (15) / Tests 204 passed (204)' (was 14/194 before this change). (2) Every html-tier CONSUMER test in the repo - the 39 test files outside packages/sdui-parser that reference kind:'html', parseJsx or @object-ui/sdui-parser - VERDICT command-exit 0, 'Test Files 39 passed (39) / Tests 744 passed (744)'. (3) type-check: @object-ui/sdui-parser exit 0 (`tsc --noEmit && tsc -p tsconfig.test.json`, so the new test file IS typechecked); @object-ui/components exit 0 AFTER building its dependency closure - the first run's 4 errors were all 'Cannot find module @object-ui/react-runtime' plus cascaded implicit-any, i.e. an unbuilt tree, not a regression. (4) builds: `pnpm --filter '@object-ui/sdui-parser^...' build` and the same for components, both exit 0. (5) repo gates: check:control-bytes exit 0 (7313 files scanned), check:new-line-citations exit 0 (0 new citations), check:upstream-port-parity exit 0 (parse.ts is NOT in scripts/upstream-port-pin.json - verified, the pin holds only 3 tooling files), check:vi-mock-specifiers exit 0, check-changeset-presence exit 0 ('3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'). NOT MEASURED: check:sdui-registration-pins exits 2 = PREREQUISITE NOT MET (no apps/console/dist/assets to weigh); it pins bundle-time registrations and this diff adds/removes none - left to CI rather than run against nothing. (6) eslint: 4 changed files, `--no-inline-config --format json`, exit 0, zero problems. NARROWING DECLARED with all three readings: population 4799 lintable files read from eslint's OWN config (ESLint#isPathIgnored over `git ls-files`, not a guess); files linted 4, read from the --format json result length; invariance - eslint.config.js declares no projectService and no parserOptions.project (0 occurrences), so linting is not type-aware and this diff cannot move the verdict on any file it does not contain. (7) ABLATIONS, the two steps separately, each mutation PROVEN ON DISK before any result was read (anchor occurrence counts + git hash-object) and each restore proven BY STATE (git diff HEAD empty AND blob back to HEAD's 834bf3b0484c), never by an exit code; a `trap restore EXIT INT TERM` with absolute paths guarded every leg and no probe file survives. BASELINE (refusal + reversed spread): a flex element carrying type=grid gives type=flex, codes=[\"forbidden-attr\"], vitest exit 0, 17/17 green. ABLATION A = STEP 2 ALONE (refusal removed, spread still reversed): type=flex, codes=[], json {\"type\":\"flex\",\"gap\":4} - the authored value is gone with ZERO diagnostics, i.e. a SILENT DISCARD, exactly the card's claim; vitest exit 1, 5 red. ABLATION B = STEP 1 ALONE (refusal kept, old spread order restored): type=flex, codes=[\"forbidden-attr\"] - indistinguishable from the shipped state through the public API, which is why the order fix is documented as defense in depth; vitest exit 1 with 1 red, and that one red is only the key-order pin. ABLATION C = BOTH REMOVED (origin/main behaviour): type=GRID, codes=[] - the SILENT OVERWRITE, a grid rendering where the author wrote a flex; vitest exit 1, 7 red. (8) STOP CONDITION check:sdui-lockstep - GREEN in both directions, measured, not assumed. The gate lives in objectstack and the reference checkout there has no node_modules at all, so a bare run exits 3 = PREREQUISITE NOT MET ('the dependency typescript is not installed'), which is NOT a reading; it was therefore run from a faithful scratch copy of that tree with a resolvable typescript: `--self-test` exit 0 and the gate exit 0, 'byte-identical to objectui@53ded82bf7a4 over 214 grammar line(s) [blob 0131f27cf86d] and agrees on all 24 diagnostic code(s)'. For the half that gate structurally cannot see (objectui moving after the record), objectstack's OWN fingerprintOf was run against this branch and against objectui origin/main: grammar-region blob 0131f27cf86dbabec09b4ae55ae131403667e7e9 BEFORE and AFTER, byte-identical and equal to the recorded value (all hunks land at lines 24-190; the region delimiter is at line 392 of 605, and 605-392+1 = 214 matches the record's line count), and the diagnostic-code list identical before and after - this PR adds NO code. (9) STOP CONDITION fixture/example sweep - CLEAN, reported either way. Population: the 65 files referencing kind:'html' / parseJsx / @object-ui/sdui-parser, plus examples/**, content/docs/**, skills/** and e2e/**. Exactly ONE html-tier document in the repo authors a `type` attribute: action-type-input-html-tier.test.tsx row 3, a deliberate NEGATIVE CONTROL asserting the spelling does not work - so the harm the stop condition names (a narrowing that breaks existing authored content) is untouched and I kept going. examples/**: 8 type= hits, 0 in html-tier source (all data-obj-type DOM assertions and a script type=module in an index.html); the schema catalog is the JSON tier, untouched. content/docs/**: 6 hits, all react-tier Block type= or MDX callouts, neither parsed by this parser. apps/console/src/sdui-tiers-preview.tsx, the repo's own html-tier authoring example whose test asserts zero error diagnostics: 0 hits. The sweep regex is not vacuous - it found the negative control, which pre-exists this change. (10) Clause-2 carrier: `PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9125` run from the objectstack checkout, exit captured by redirect = 0, 'the clause-2 declaration is readable in the fixed spelling and both carriers agree'. needs:contract-review was hung on PR 9125 with the additive REST endpoint (HTTP 200) and READ BACK: ['package: components','tests','needs:contract-review','package: sdui-parser'] - nothing missing from union(read, target). (11) Both GitHub bodies read back after writing: the filed issue is 4162 bytes with 0 tag-shaped fragments and 0 closing keywords near an issue reference; the PR body's only post-write divergence starts at exactly the sent length, i.e. append-only (the platform's own footer), with the session reference preserved as a prose code span and 0 tag-shaped fragments stored.", "mcp_calls": "4 - three search_issues (dedup) and one issue_write (the filed finding). Everything else went through git, the REST channel (probed 200 on a repo-scoped read at the start of the run) and the zero-quota local tree.", "open_questions": [ { "question": "objectui AGENTS.md commandment #-1 says the codebase is English-only including code comments, but the dispatch contract says a quoted Chinese maintainer ruling must stay verbatim because rewriting the quote rewrites the ruling. The ported docblock carries objectstack's verbatim quote of the 2026-09-01 ruling. Which rule governs a VERBATIM RULING QUOTE inside objectui source? Reporting rather than silently picking a side, per the conflict clause - though I did have to write something, and the choice is stated below.", "options": [ "A - keep the verbatim Chinese quote in the ported docblock (what I did). Byte-faithful with objectstack's copy, and measurement says #-1 does not describe the tree: 743 files under packages/** already carry CJK in comments (60233 CJK characters). Note my FIRST measurement of this was a FALSE ZERO - a grep -rP with a packages/*/src glob reported 0 files, and a proper walk reported 743; I nearly removed the quote on the strength of that zero.", "B - render the ruling in English in objectui source and leave the verbatim quote to objectstack's ADR-0080 amendment. Complies with #-1 as written, and loses nothing that is not recorded elsewhere - but it is a translation of a ruling, which the dispatch contract forbids.", "C - a maintainer amendment to #-1 carving out verbatim ruling quotes, after which either spelling is unambiguous." ], "recommendation": "A, because the stronger constraint is the one whose violation is not recoverable by reading another document: a translated ruling can drift from what was actually decided, whereas an English gloss of it is recoverable from the ADR. C is the durable fix and is cheap - #-1 currently has 743 counterexamples in the tree, so it is already not the rule anyone is following." } ], "out_of_scope_findings": [ "filed as objectstack#17645: the sdui-parser lockstep has drifted again SINCE the record was taken - objectui origin/main carries 26 diagnostic codes to objectstack's 24 (objectui-only: inert-quick-add from objectui#8285 and member-type-mismatch from objectui#8067, plus the extra source file kanban-quick-add.ts), all landed after the record was taken on 2026-09-05, i.e. inside the blind spot check-sdui-lockstep's own header names. Category (b), a declared contract violated: objectstack#12719's 'both copies byte-agree on the accepted grammar and on diagnostic codes'. Measured with objectstack's OWN fingerprintOf; the control PRE-EXISTS this change and is the same kind as the subject - the same extractor on the same objectui repo at the PINNED revision 53ded82 returns 24 and the matching file list, so it is demonstrably sensitive to exactly this drift. Latent today (the gate compares objectstack against the record and is green, exit 0) and it will red at the next .objectui-sha bump plus gen:sdui-lockstep, with or without this PR. Dedup: three search_issues queries, all NON-EMPTY so self-validating; the closest matches are objectstack#12719 and #12814, both CLOSED and both about earlier codes - this is a third instance of a known class, not a duplicate.", "noted, not filed: the two copies of parse.ts differ in two further places beyond the spread order, the missing refusal and the objectui-only markHtmlTierNode wrapper - both COMMENT-ONLY with no behavioural difference. objectui's whitespace-collapse rationale cites objectui#5661 and 'rendered as Axpage' where objectstack's cites 'triage option (b), shared verbatim with the downstream copy'; and objectstack's interpretBrace carries a LOCKSTEP banner ('Change this block only together with the objectui copy') that this copy lacks. Reported under the brief's report-don't-stop clause. Not filed: comment wording is a documentation nit, and the banner's absence changes nothing a gate or a reader acts on since the grammar region is byte-identical and mechanically checked. Successor: the next port of this file, which will read both docblocks side by side.", "noted, not filed: action-type-input-html-tier.test.tsx carries a Chinese phrase in a code comment, which AGENTS.md #-1 forbids. Pre-existing, and one of 743 files under packages/** that do - a repo-wide convention gap, not a defect, and filing one instance of 743 would be noise. Successor: none, which is why it is stated here; it needs a ruling on whether #-1 still describes the tree (see open_questions), not a card. IN SCOPE and DONE, by contrast: the prose in that same file that THIS change falsified - it asserted the parser composes { type: tag, ...props } and that row 3 fails with unknown-component on the overwritten discriminator. Both are now false. The prose is corrected and row 3 now asserts WHICH diagnostic appears (forbidden-attr naming the attribute, and NOT unknown-component), because its old assertions still passed after the change for an entirely different reason - a test passing while measuring nothing.", "noted, not filed: check:sdui-registration-pins cannot run without a console bundle and exits 2 by design. Not a finding, recorded so the gate list in this report is honest about what was and was not measured." ] }Generated by Claude Code