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.
finding(data-objectstack): notifyDroppedFields asserts the wire entry into DroppedFieldsEvent without ever parsing reason against the spec enum #4934
Found while implementing objectui#3935 (replacing writeWarningToast's two-way conditional with an exhaustive reason table). Filed unassigned — recording only, not claiming.
Fact
packages/data-objectstack/src/index.ts — notifyDroppedFields filters the response's droppedFields entries on SHAPE only:
constvalid=dropped.filter((e): e is DroppedFieldsEvent=>!!e&&typeofe==='object'&&Array.isArray((easDroppedFieldsEvent).fields)&&(easDroppedFieldsEvent).fields.length>0,);
reason is never read, never compared against DroppedFieldsEventSchema.shape.reason, and the predicate is a hand-written type guard that asserts the entry into the spec type regardless. notifyBatchDroppedFields, a few lines down, does the same thing (const e = entry as DroppedFieldsEvent & { index?: number }).
So a value outside 'readonly' | 'readonly_when' | 'primary_key' — a server running ahead of this bundle's @objectstack/spec pin, which is the normal skew direction for a deployed client — reaches consumers typed as if it were inside the union. Declared is not enforced at the one boundary that reads the wire.
Why this is observation-class today
objectui has exactly one consumer of the dropped reason: a grep for .reason across packages/ and apps/ finds only writeWarningToast's two byReason lines. After objectui#3935 that consumer handles the off-union value explicitly — an exhaustive Record< DroppedFieldsEvent['reason'], … > for the declared arms, plus a documented cause-free line for anything else, pinned by a test. Nothing a user hits today.
What makes it worth recording is the direction of the gap: the interior is now typed to trust a union the boundary never checked. The next consumer to branch on reason gets an exhaustive-looking table and a type that lies to it, and no gate in the repo would say so.
Options, deliberately without a recommendation
Parse at the boundary with DroppedFieldsEventSchema.safeParse and keep only entries that pass. But a dropped entry means the user is told nothing about those fields, which is precisely the silence objectui#3484 exists to remove — so this is a behaviour ruling, not a mechanical hardening.
Keep the entry and widen the type at the boundary so the interior sees the unparsed reality (reason: string), pushing narrowing to consumers. That is the opposite of objectui#3160's ruling, which deliberately re-exported THE spec type rather than a hand-widened string; it needs that ruling revisited, not reversed quietly.
Leave it as is and require each consumer to document the skew case, as objectui#3935 did.
Triage first-touch — concentrated round #2 (triage seat, session session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19): promoted finding → pm:queue · Task, with a seat adjudication (auto-adjudicated, veto window open — mechanical-boundary test: bug-fix class, restores declared=enforced, no accept-set or public-surface change).
<!-- os-decision-facets -->
Platform long-term coherence: an interior typed to trust a union the boundary never checked is the exact inversion of contract-first; validating at the wire is the only site that can restore it.
AI-agent error-resistance: a type that lies invites the next exhaustive-looking table to break silently on server skew; loud, explicit skew representation at one boundary beats N per-consumer disciplines.
Startup scope discipline: small, one file, no new surface.
Ruling (threading the card's three options rather than picking a conflicting one): validate reason against the spec enum at the boundary, keep every entry (dropping entries would recreate the silence objectui#3484 removed — so not option 1's drop), and represent an off-union reason explicitly at the boundary type instead of asserting it into the union (not option 2's blanket string widening — the spec type remains the canonical arm per objectui#3160; the skew case becomes a named, documented arm the existing cause-free handling in writeWarningToast already covers). notifyBatchDroppedFields' cast goes the same way. Pin with a test feeding an off-union reason through the boundary. If implementation finds a consumer this breaks, stop and report the fork — do not silently revert to any of the three original options.
Confidence gap: whether any non-toast consumer of DroppedFieldsEvent exists outside the grepped surface (examples/docs) is unmeasured.
Maintainer ruling recorded (2026-08-22, decision-inbox digest with the skills seat, session_01ApyDuQY2fkunMCqXiqvBhR)
The full decision inbox (46 cards) was presented with per-card four-axis recommendations; the maintainer accepted the batch, verbatim: 「接受所有」.
Ruled: Option A — confirm the auto-adjudication. The triage seat's adjudicated ruling stands and its veto window closes: validate reason against the spec enum at the boundary in notifyDroppedFields (and notifyBatchDroppedFields), keep every entry (no drop — dropping would recreate the silence objectui#3484 removed), and represent an off-union reason explicitly at the boundary type as a named, documented skew arm rather than asserting it into the union or blanket-widening to string. Pin with a test feeding an off-union reason through the boundary; if implementation finds a consumer this breaks, stop and report the fork.
State: needs-user-decision removed → pm:queue (ready for dispatch).
Triage: domain:ui. The fix lands in packages/data-objectstack — notifyDroppedFields asserting the wire entry into DroppedFieldsEvent without parsing reason against the spec enum. The consumer-side parse is objectui's; the spec enum it should parse against is already published.
Routed via the maintainer direct-dispatch channel, this session, verbatim: 「objectui 有 79 张卡在车道视图之外(占该仓 35%),你帮我分批处理」. PM session session_0124Qg8rLvpXnQDwCmpKUmaJ. Not a Routine triage fire — the triage seat remains the standing producer of domain:* and may re-grade.
⛔ pm-state, assignee and scope untouched; the auto-adjudicated marker was preserved.
Claim: PM loop round R28
Session: session_013hfmP9hoMd3dJwTh85J4yB
Branch: claude/issue-4934-dropped-fields-reason-boundary
Worktree: objectui-issue-4934
Domain: domain:ui
File surface: packages/data-objectstack/src/index.ts (notifyDroppedFields + notifyBatchDroppedFields) + the package test dir (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus``
Clause-②: no — validating an existing wire value against an already-published spec enum restores declared = enforced; the accept set is unchanged and no public surface widens. ⚠️ If the named skew arm would have to be added to a PUBLISHED type rather than a boundary-local one, that is clause-② ⇒ stop and report.
Serial constraints cleared: packages/data-objectstack/** — PR #6828 (#6302, the analytics lowering) MERGED this session; no open PR touches the package.
{
"issue": 4934,
"status": "needs_decision",
"branch": "claude/issue-4934-dropped-fields-reason-boundary",
"pr": "https://github.com/objectstack-ai/objectui/pull/6884",
"premise_still_valid": true,
"summary": "The ruled boundary fix is implemented, pinned and self-green on the branch at 09af7e1: notifyDroppedFields and notifyBatchDroppedFields now parse `reason` against DroppedFieldsEventSchema.shape.reason (derived from the installed pin, not restated), keep EVERY entry, and route an unrecognized reason to a named skew arm UnrecognizedDropReasonEvent that carries UNRECOGNIZED_DROP_REASON plus the wire value verbatim in `unrecognizedReason`. WriteWarningEvent.droppedFields becomes the two-arm DroppedFieldsNotice; the spec type stays the canonical arm and is not widened to string. The shape filter's hand-written guard now asserts only what it checks (reason typed unknown), so the original lie is deleted rather than worked around. CLAUSE-2 DOES NOT FIRE: the skew arm landed on the adapter's own boundary types, nothing was added to the published @objectstack/spec type, and spec-symbol-batch6's pin that DroppedFieldsEvent IS the spec type still passes. BUT THE RULING'S STOP CONDITION DID FIRE. The sole consumer, packages/app-shell/src/providers/writeWarningToast.ts, keeps its runtime behaviour exactly (13/13 of its tests pass against the new boundary; the cause-free line is produced word for word) yet no longer COMPILES: app-shell type-check yields exactly 2 TS2345 errors, both the Map key and the lineFor parameter pinned to the spec union, and a full workspace build gives 39 of 40 packages successful with only app-shell failing on those same two lines. This is inherent to clause 3 rather than to my spelling of it: for those lines to compile the skew reason would have to be assignable to the spec union, which is the defect itself, so every faithful skew arm errors at the same two sites. The ruled fix is therefore NOT separable into two independently-green PRs, and the PR is deliberately left not-mergeable and marked `Part of` rather than a closing keyword. I did not apply the two-line consumer widening: packages/app-shell is outside the dispatched file surface and the maintainer-confirmed ruling asked for the fork to be reported rather than absorbed.",
"tests": "RED-FIRST (assertions written and run BEFORE the implementation, against the pre-fix boundary; no dist involved — the test imports ./index from source): `pnpm exec vitest run packages/data-objectstack/src/droppedFieldsReason.boundary.test.ts` gave `Tests 3 failed | 3 passed (6)`, the failures printing the lie itself — `AssertionError: expected 'some_future_reason' not to be 'some_future_reason'` and a diff showing received `\"reason\": 42` where the skew arm was expected. The 3 that PASSED pre-fix are the discrimination controls (in-union reasons already arrived untouched), so the pin bites only on the skew path. GREEN on the committed head 09af7e1: `pnpm exec vitest run packages/data-objectstack/` -> `Test Files 49 passed (49) / Tests 669 passed (669)` (662 before, 7 new); `pnpm --filter @object-ui/data-objectstack type-check` -> echoed `tsc --noEmit`, exit 0 (its tsconfig includes test files — verified by `tsc --listFiles`, 57 test files in the program — so the @ts-expect-error pinning that the skew arm is NOT assignable to DroppedFieldsEvent is enforced, not decorative); `pnpm --filter @object-ui/data-objectstack lint` -> exit 0, `406 problems (0 errors, 406 warnings)`, all warnings the pre-existing house no-explicit-any level. GATE UNION re-run on 09af7e1 AFTER the final commit: check:control-bytes, check:spec-symbols, check:phantom-deps, check:self-import, check:esm-specifiers, check:readme-exports, check:spec-floors, check-changeset-no-major, check-changeset-presence, check-changeset-fixed — all EXIT=0. IMPORTANT, reported as NOT MEASURED rather than green on their first run: check:readme-exports and check:spec-floors initially failed with `not on disk -- run pnpm build first` and `[no-artifact] ... produced no build output to judge`, which is an unbuilt workspace, not my diff; after `turbo run build` their real readings are readme-exports `0 unbuilt, 3282 export symbols read from 37 of 40 packages` and spec-floors `274 (subpath, symbol) pairs judged`, both exit 0 — so the new RUNTIME import of DroppedFieldsEventSchema from @objectstack/spec/data clears the declared ^17.2.0 floor. CONSUMER MEASUREMENT (the fork): `pnpm --filter @object-ui/app-shell type-check` -> exit 2 with exactly 2 errors, both `src/providers/writeWarningToast.ts` TS2345 saying the four-arm reason is not assignable to the three-arm spec union; `pnpm exec turbo run build --filter='!@object-ui/site' --concurrency=2` -> `Tasks: 39 successful, 40 total / Failed: @object-ui/app-shell#build` on the same two lines. That measurement used dist, so data-objectstack was rebuilt first and the marker was confirmed to have reached it (grep -c UNRECOGNIZED_DROP_REASON: dist/index.d.ts 5, dist/index.js 3), and app-shell's whole dependency closure was built beforehand — an earlier run of the same command produced only `Cannot find module` errors and was discarded as NOT MEASURED. Consumer RUNTIME unaffected: `pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts` -> `Tests 13 passed (13)`. LINT SCOPE narrowed to the affected package with the narrowing measured rather than assumed: population came from eslint's own selection (`--format json`, 55 files), and eslint.config.js declares no parserOptions.project or projectService, so type-aware linting is off and this diff cannot move the verdict on any untouched file; the repo-wide `eslint .` stays CI's. All heavy runs went through the shared verify lock (`os-verify-lock.sh`); every exit code was captured before any pipe.",
"mcp_calls": "5 — issue_read get, issue_read get_comments, issue_read get_sub_issues, add_issue_comment (claim), add_issue_comment (this report). Everything else went over repo-scoped REST, which answered 200 from this seat (open-PR list, PR create, PR body read-back).",
"open_questions": [
{
"question": "The ruled shape cannot land green without widening two type annotations in packages/app-shell/src/providers/writeWarningToast.ts, which is outside this task's file surface. Landing the boundary PR alone leaves main unbuildable (39/40 packages build; app-shell fails). Who applies the consumer edit, and does it ride in this same PR? ZONE 2 assumption 2 is CONFIRMED — the sole reason consumer still exists and still handles off-union explicitly at runtime — and it is exactly the fork the dispatch predicted.",
"options": [
"A — authorise the two-line widening in this same PR: `new Map(DroppedFieldsEvent['reason'] -> DroppedFieldsNotice['reason'])` for byReason, and the same swap on lineFor's parameter. Naming DroppedFieldsNotice['reason'] rather than string is deliberate: STRIPPED_LINE stays declared over the SPEC union, so objectui#3935's guarantee that a NEW SPEC ARM fails type-check unworded survives untouched. Zero runtime change (13/13 consumer tests already green). The file is uncontended — no open PR touches it or packages/data-objectstack (checked #6814, #6345, #6082, #5400, #4958).",
"B — revisit clause 3: keep droppedFields typed DroppedFieldsEvent[] and deliver skew entries on a separate named channel on WriteWarningEvent. Consumer compiles untouched, but the sole reader iterates droppedFields only, so skew entries would become invisible to the toast — recreating precisely the silence objectui#3484 removed, and doing it silently.",
"C — leave the boundary as it is (the card's option 3, per-consumer discipline). Already refused by the ruling; recorded only so the fork is presented whole."
],
"recommendation": "A, on all four axes. REAL BUSINESS NEED: measured pull is zero today — no off-union reason exists in practice and the one reader already copes at runtime — so this is hardening, exactly as triage classified it; the need served is structural, every future consumer, and A is the only option that actually delivers it. LONG-TERM SOUNDNESS: A is the only option that leaves declared = enforced at one boundary; B buys a green compile by making the data invisible, which is a worse defect than the one being fixed and is a workaround in the sense AGENTS.md 0.1 bans; C is the status quo the maintainer already declined. AI-ERROR RESISTANCE: A converts server skew from an N-consumer discipline into a tsc error at the one place the wire is read, which is what makes the next exhaustive-looking table impossible to write wrong — and it does so WITHOUT loosening the spec-union exhaustiveness #3935 established. STARTUP SCOPE DISCIPLINE: A is two type annotations in one uncontended file, no new API surface, no new package, no widening of any published spec type. The cost of A is the only thing genuinely at stake — a dev seat editing outside its dispatched surface — which is why it is being asked rather than done."
}
],
"out_of_scope_findings": []
}
{
"issue": 4934,
"status": "done",
"branch": "claude/issue-4934-dropped-fields-reason-boundary",
"pr": "https://github.com/objectstack-ai/objectui/pull/6884",
"premise_still_valid": true,
"summary": "Patch round on the existing branch, executing the 2026-08-30 option-A ruling. Exactly two type annotations changed in packages/app-shell/src/providers/writeWarningToast.ts (lineFor's parameter, and the byReason Map key) plus the DroppedFieldsNotice type import; STRIPPED_LINE keeps its Record declaration over the SPEC union at line 108, unmoved, so objectui#3935's guarantee survives. The changeset now states the blast radius and that the consumer's compile error is the intended signal; the bump level is unchanged at data-objectstack minor. The PR body now says plainly that clause 2 FIRES and the review is fable-tier, keeps the still-true narrow part (nothing added to the published spec type, spec-symbol-batch6's pin passes), and carries the evidence that the file already handled the skew value at runtime deliberately. Part of #4934 retained, no closing keyword. THREE CORRECTIONS TO THE ORDER. (1) The workspace build reading is 43 successful / 43 total, NOT 40 of 40. The '40' denominator existed only because the build was failing: app-shell#build has three direct dependents in the turbo graph (console, example-console-starter, example-byo-backend-console), and when it fails they are never attempted and never counted, so the previous seat's '39 successful, 40 total' plus 3 cancelled equals today's 43. Asking for 40/40 would have been asking for a number that cannot occur on a green tree. (2) The gate union owed more than the dispatched clue list: check-changeset-overwrite.mjs is a real gate in changeset-guard.yml and this round EDITS an existing changeset, and writeWarningToast.ts is a wording table that check:i18n-keys / i18n-drift / i18n-dead-keys read. All four extra gates run and are green; they were derived from the actual diff, not from the clue list. (3) This PR makes two sentences in writeWarningToast.ts stale, and the two-annotation bound means I report them rather than edit them: strippedLineUnknownReason's docstring still says the adapter asserts the entry without ever checking reason against the spec enum (the boundary now checks it), and lineFor's docstring opens 'The PARAMETER carries the spec union' (it no longer does; and that causal claim was already loose, since STRIPPED_LINE's exhaustiveness comes from its own Record declaration). Neither affects behaviour or any gate, and the sentence the ruling quotes as its evidence stays true. Both are written up in the PR body under 'Known drift this PR creates and deliberately does not fix'. No app-shell entry was added to the changeset: both edited sites are module-private (lineFor is not exported, byReason is a local), the emitted JavaScript is byte-identical, so app-shell has no user-visible change of its own; say the word if you want a patch entry anyway.",
"tests": "All readings on the final head c01e5a2, gate union re-run AFTER the last commit. Every exit code captured before any pipe; all heavy runs through os-verify-lock.sh. CONSUMER TYPE-CHECK (the thing that made this a fork): pnpm --filter @object-ui/app-shell type-check -- echoed the real script '@object-ui/app-shell@17.6.0 type-check' and 'tsc --noEmit && tsc -p tsconfig.test.json', lock VERDICT command-exit 0. The echo excludes a zero-match false green, and the second leg means the package's test files are in the program too. WORKSPACE BUILD: pnpm exec turbo run build --filter='!@object-ui/site' --concurrency=2 -- turbo's own summary line 'Tasks: 43 successful, 43 total', 'Cached: 39 cached, 43 total', zero Failed lines. No 'Cannot find module' shape appeared; the closure was built by this same run and data-objectstack's dist was confirmed to carry the marker (grep -c UNRECOGNIZED_DROP_REASON dist/index.d.ts dist/index.js gave 5 and 3, and dist/index.d.ts exports 'type DroppedFieldsNotice'). See summary correction (1) for why 43 and not 40. CONSUMER RUNTIME: pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts --reporter=verbose -- 'Test Files 1 passed (1)', 'Tests 13 passed (13)', exit 0, all 13 names printed including 'names the fields and claims no cause for a reason ahead of the spec pin (#3935)', which is the skew path. No message changed. ZERO RUNTIME CHANGE PROVEN, NOT ASSERTED: transpiled the file before and after the diff through the TypeScript API (ES2022/ESNext, 0 diagnostics); both emit 6686 bytes with sha256 6a5c5a957517a0489699ff85054a5edd7409f8809d5fe0084ad96f2a16655e9d, i.e. byte-identical JavaScript, while the source diff is non-empty (control printed). NOTE: my FIRST attempt at this used 'pnpm exec esbuild', which is not installed; both legs exited 254 and both wrote the same 113-byte pnpm error text, so the hashes matched and it read as a clean proof. That reading was DISCARDED as NOT MEASURED and redone -- reporting it because it is exactly the failure shape the order warns about. DATA-OBJECTSTACK UNMOVED: pnpm exec vitest run packages/data-objectstack/ -- 'Test Files 49 passed (49)', 'Tests 669 passed (669)', exit 0. Unchanged by this round, as expected. LINT: pnpm --filter @object-ui/app-shell lint -- '2798 problems (0 errors, 2798 warnings)', exit 0. eslint really did report on the edited file: --format json gives a population of 1022 files and packages/app-shell/src/providers/writeWarningToast.ts is in it with errorCount 0, warningCount 0; totals across the JSON are 0 errors / 2798 warnings, matching the script run. Narrowing to the package is measured, not assumed: eslint.config.js is 268 lines with 0 hits for projectService/parserOptions and 10 for 'rules' as a positive control, so type-aware linting is off and this diff cannot move the verdict on any untouched file; the repo-wide eslint . stays CI's. GATE UNION on c01e5a2, each exit captured individually: control-bytes 0, spec-symbols 0, phantom-deps 0, self-import 0, esm-specifiers 0, readme-exports 0, spec-floors 0, changeset-no-major 0, changeset-presence 0, changeset-fixed 0, changeset-overwrite 0, i18n-keys 0, i18n-drift 0, i18n-dead-keys 0, plus app-shell type-check 0 on the restored tree. The two dist-reading gates gave REAL readings, not no-artifact: readme-exports printed 'check-readme-exports: OK (... 3282 export symbol(s) read from 37 of 40 package(s) ... 0 unbuilt, 3 declare no types)' and spec-floors printed '274 (subpath, symbol) pair(s) judged' with 'Every consumer-facing @objectstack/spec floor carries the symbols its package's artifact references.' REVERSE VERIFICATION on the widening itself, committed first so the revert had a real restore point. PREDICTED BEFORE RUNNING: reverting only lineFor's parameter and leaving the Map key widened yields exactly one TS2345, at the CALL SITE inside emitWriteWarning rather than at the declaration, because byReason's key is by then the four-arm union. OBSERVED, exactly that: 'src/providers/writeWarningToast.ts(207,24): error TS2345: Argument of type \"readonly\" | \"readonly_when\" | \"primary_key\" | \"objectui:unrecognized-drop-reason\" is not assignable to parameter of type \"readonly\" | \"readonly_when\" | \"primary_key\".' with 'Exit status 2' and lock VERDICT command-exit 2; TS2345 count 1, total TS error lines 1. MUTATION CONFIRMED ON DISK before the run: injected text present exactly once, removed text absent (0), Map key still widened (1) as a control. RESTORE PROVEN BY OBSERVED STATE, not by an exit code: trap on EXIT/INT/TERM with an absolute path built from git rev-parse --show-toplevel, restoring with 'git checkout HEAD -- ABSPATH' (named ref, never bare, so the index cannot hand back the mutation); afterwards on-disk git hash-object equals git rev-parse HEAD:PATH at c4560fd222fd8f5d9d961ea3534793f57e45c165 (both non-empty, checked), git diff HEAD empty, git status --porcelain empty, and the restored annotation present once with the mutation text absent. app-shell type-check returns to exit 0 on the restored tree.",
"mcp_calls": "2 -- issue_read get_comments (the ruling), add_issue_comment (this report). Everything else went over repo-scoped REST, which answered 200 from this seat for both the PR read and the PR body PATCH; the card's own page was read through the zero-quota web payload first, which carried 15 of 22 timeline items and could not reach the ruling comment, hence the one targeted MCP read.",
"open_questions": [
{
"question": "Should the two now-stale docstring sentences in writeWarningToast.ts be corrected in this PR? The ruling bounded the round to two annotations and 'nothing else in packages/app-shell', so I left them and documented them in the PR body instead.",
"options": [
"A -- leave them; a follow-up card corrects both sentences together with any other prose drift this seam accumulates.",
"B -- authorise two prose edits in this PR: drop the stale mechanism clause from strippedLineUnknownReason's docstring, and rewrite lineFor's opening sentence to attribute exhaustiveness to STRIPPED_LINE's own Record declaration rather than to the parameter.",
"C -- correct them at review time as review feedback on this PR."
],
"recommendation": "B, narrowly. The cost is two sentences in a file already open in this diff, and the benefit is that the file stops describing a boundary that this very PR deletes -- a stale comment about a type contract is the same class of defect as the card itself, read by the next agent as though it were current. I did not do it unasked because the ruling's bound was explicit and because rewriting a docstring's causal explanation is prose authorship, not a mechanical fix, so it is not covered by any bounded-in-place exemption. If the answer is A, the follow-up card should be filed against this seam rather than left to memory."
}
],
"out_of_scope_findings": [
"NOT FILED as separate cards, deliberately -- both are drift this PR itself creates in the file it edits, so a separate card would duplicate the fable-tier review that is already required: (a) strippedLineUnknownReason's docstring in packages/app-shell/src/providers/writeWarningToast.ts still describes the pre-fix boundary ('asserts the entry ... without ever checking the value against the spec enum'); (b) lineFor's docstring opens 'The PARAMETER carries the spec union', which this diff makes false. Both are recorded in the PR body under 'Known drift this PR creates and deliberately does not fix' and raised as the open question above -- PM to decide whether they ride here or become a card."
]
}
{
"issue": 4934,
"status": "done",
"branch": "claude/issue-4934-dropped-fields-reason-boundary",
"pr": "https://github.com/objectstack-ai/objectui/pull/6884",
"premise_still_valid": true,
"summary": "Both sentences WERE stale against the current tree; I re-derived their exact text rather than trusting the paraphrases, and corrected them in commit 3ec17deb on the existing PR (no new PR). (1) strippedLineUnknownReason's docstring described the pre-fix mechanism ('asserts the entry into DroppedFieldsEvent without ever checking the value against the spec enum'); notifyDroppedFields now maps every wire entry through asDroppedFieldsNotice, which parses reason against a Set built from DroppedFieldsEventSchema.shape.reason.options and routes an unrecognized value to UNRECOGNIZED_DROP_REASON. The docstring now states that mechanism; its conclusion (the branch is still reachable) was true before and is true now, so it was preserved. (2) lineFor's docstring said the parameter carries the spec union (it now carries the two-arm notice union) and hung STRIPPED_LINE's exhaustiveness on that parameter; it now attributes exhaustiveness to the table's own declaration keyed by DroppedFieldsEvent['reason'], which is where it always lived. Bounds honoured: STRIPPED_LINE's declaration did not move (line 108); no code changed; nothing else under packages/app-shell; no changeset entry added and the existing changeset untouched. The protected option-A evidence sentence is byte-identical AND unmoved: I deliberately ended my replacement text at the exact byte where that sentence begins, so its three lines never entered the diff as changed lines, only as context. ONE CORRECTION TO THE RULING, which is why the emit proof is reported in two modes: the ruling's ground for no changeset was 'the emitted JavaScript is byte-identical ... the prose edits B adds cannot change that'. That is true only with comments STRIPPED. tsconfig.base.json line 22 sets removeComments:false and app-shell builds with a bare tsc, so docstring bytes do reach dist/index.js and dist/index.d.ts; the comment-preserving emit legitimately differs. The ruling's CONCLUSION survives on stronger ground than the phrase it used: the executable emit is proven identical, and every changed line of the comment-preserving emit is a comment line. Read it as 'byte-identical executable emit'. SECOND CORRECTION, to a standing lane note: the 2026-08-26 note says every PATCH edit downgrades the session-URL footer to the bare form. It did not here. I PATCHed the body via REST and the full read-back was byte-identical to what I sent, session-form footer intact (1 occurrence). Also: the previous round's '6686 bytes' is that same emit counted in UTF-16 code units, not bytes; this file carries CJK and em-dashes, so its byte length is 6726. The sha256 6a5c5a95... reproduced exactly, so the two rounds are measuring the same artifact.",
"tests": "All readings on head 3ec17debb with a clean tree (git status --porcelain empty at commit time), exit codes captured BEFORE any pipe, each quoting the gate's own verdict line. EMIT IDENTITY, two modes, each leg carrying a success control (0 diagnostics, real JS emitted, three known markers present) because a digest from a command that failed identically twice is a perfect false proof: comments-stripped BEFORE sha256 f9ff9b85b9cf39e28caf81a4bd99708bfc36b903b190af35fe56bca731e91bf8 / 2181 B, AFTER f9ff9b85... / 2181 B -> IDENTICAL, diff empty; comments-kept (removeComments:false, the real build) BEFORE 6a5c5a957517a0489699ff85054a5edd7409f8809d5fe0084ad96f2a16655e9d / 6726 B, AFTER 393f0180f8483dcfc1d106671185f5881cf96f3640442a237bce974d1503f1e3 / 6854 B -> DIFFERS, and filtering that emit diff for changed lines which are not comment lines returns nothing. The non-empty source diff is printed alongside: 8 insertions, 6 deletions, one file. NOTE ON MY OWN FIRST PROBE: its stripped-mode control failed (CONTROL_PASSED[strip] false) purely because I set the size floor at 3000 B and the stripped emit is 2181 B; I fixed the threshold, not the measurement, and re-ran both legs. TYPE-CHECK: pnpm --filter @object-ui/app-shell run type-check echoed 'tsc --noEmit && tsc -p tsconfig.test.json', os-verify-lock VERDICT command-exit 0, held 68s. Dependency closure was built FIRST (pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, VERDICT command-exit 0, held 114s) because a fresh worktree had no dist for any of the six referenced projects, so an unbuilt type-check would have been TS6305 = NOT MEASURED, not green. TESTS: pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts from the REPO ROOT (not --filter, which is objectui's documented false-green trap) -> 'Test Files 1 passed (1)' / 'Tests 13 passed (13)', messages unchanged. GATES, all exit 0: check:i18n-keys ('Every in-scope call-site key resolves against the en pack (2834 keys)'), check:i18n-drift ('0 en value(s) changed'), check:i18n-dead-keys (exit 0, and it declares of itself 'This is a REPORT, not a gate'), check:control-bytes ('scanned 5737 tracked text file(s)'), check-changeset-presence ('3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-fixed, check-changeset-no-major, check-changeset-overwrite ('1 changeset(s) added, 0 modified, 0 deleted' - proves I did not touch the existing changeset), check:shell-escape-residue, check-type-check-coverage, check-lint-coverage. Gate set derived from MY actual diff against objectui's own package.json and .github/workflows, NOT from the dispatch list and NOT from objectstack's dispatch-gates.mjs, which lives only in objectstack and would have answered about the wrong tree. BYTE DISCIPLINE: control-byte self-scan on the edited file returned grep exit 1 (no match); the only non-ASCII character introduced is U+2014 EM DASH, which the file already used. LINT is a DECLARED NARROWING, not a repo scan, and carries the three things a narrowing owes: population from eslint's own resolution (the file resolves a config and is linted, not ignored - 1 file in the JSON report); count from --format json (0 errors, 0 warnings); invariance because --print-config shows parserOptions with no project and no projectService, i.e. linting is not type-aware, so a comment-only edit in one module cannot move any untouched file's verdict. NOT RE-RUN, with reason: spec-symbols, spec-floors, readme-exports, phantom-deps, self-import, esm-specifiers, published-dist, vi-mock-specifiers - every one has the export surface, dist artifacts, module graph or registration tables as its subject, and the executable emit is proven unchanged. PROTECTED SENTENCE: its exact three-line block occurs exactly once in both HEAD and the edited tree, and appears in the diff only as context lines.",
"mcp_calls": "0 - a repo-scoped REST probe returned HTTP 200 on this seat, so every GitHub read (the ruling comment, the PR, the read-back) and the body PATCH went through REST. No MCP GitHub tool was called, and no duplicate-search was needed because no issue was filed.",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
fix(app-shell): word every dropped-fields reason on its own instead o…
os-support-ai commentedon Aug 19, 2026
Triage first-touch — concentrated round #2 (triage seat, session
session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19): promotedfinding→pm:queue· Task, with a seat adjudication (auto-adjudicated, veto window open — mechanical-boundary test: bug-fix class, restores declared=enforced, no accept-set or public-surface change).<!-- os-decision-facets -->
reasonconsumer (writeWarningToast branches onDroppedFieldsEvent.reasonwith a binary ternary, so every future reason is labelled "Read-only" #3935) already handles off-union explicitly; the exposure is every future consumer.Ruling (threading the card's three options rather than picking a conflicting one): validate
reasonagainst the spec enum at the boundary, keep every entry (dropping entries would recreate the silence objectui#3484 removed — so not option 1's drop), and represent an off-unionreasonexplicitly at the boundary type instead of asserting it into the union (not option 2's blanketstringwidening — the spec type remains the canonical arm per objectui#3160; the skew case becomes a named, documented arm the existing cause-free handling inwriteWarningToastalready covers).notifyBatchDroppedFields' cast goes the same way. Pin with a test feeding an off-unionreasonthrough the boundary. If implementation finds a consumer this breaks, stop and report the fork — do not silently revert to any of the three original options.Confidence gap: whether any non-toast consumer of
DroppedFieldsEventexists outside the grepped surface (examples/docs) is unmeasured.Size/model suggestion: S–M, opus.
Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Maintainer ruling recorded (2026-08-22, decision-inbox digest with the skills seat,
session_01ApyDuQY2fkunMCqXiqvBhR)The full decision inbox (46 cards) was presented with per-card four-axis recommendations; the maintainer accepted the batch, verbatim: 「接受所有」.
Ruled: Option A — confirm the auto-adjudication. The triage seat's adjudicated ruling stands and its veto window closes: validate
reasonagainst the spec enum at the boundary innotifyDroppedFields(andnotifyBatchDroppedFields), keep every entry (no drop — dropping would recreate the silence objectui#3484 removed), and represent an off-unionreasonexplicitly at the boundary type as a named, documented skew arm rather than asserting it into the union or blanket-widening tostring. Pin with a test feeding an off-unionreasonthrough the boundary; if implementation finds a consumer this breaks, stop and report the fork.State:
needs-user-decisionremoved →pm:queue(ready for dispatch).Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Triage:
domain:ui. The fix lands inpackages/data-objectstack—notifyDroppedFieldsasserting the wire entry intoDroppedFieldsEventwithout parsingreasonagainst the spec enum. The consumer-side parse is objectui's; the spec enum it should parse against is already published.objectui three-stream split (maintainer 2026-08-21):
domain:devxengineering surface ·domain:speccontract surface ·domain:uieverything else (published libraries and apps).Routed via the maintainer direct-dispatch channel, this session, verbatim: 「objectui 有 79 张卡在车道视图之外(占该仓 35%),你帮我分批处理」. PM session
session_0124Qg8rLvpXnQDwCmpKUmaJ. Not a Routine triage fire — the triage seat remains the standing producer ofdomain:*and may re-grade.⛔ pm-state, assignee and scope untouched; the
auto-adjudicatedmarker was preserved.Generated by Claude Code
claude commentedon Aug 30, 2026
Claim: PM loop round R28⚠️ If the named skew arm would have to be added to a PUBLISHED type rather than a boundary-local one, that is clause-② ⇒ stop and report.
Session:
session_013hfmP9hoMd3dJwTh85J4yBBranch:
claude/issue-4934-dropped-fields-reason-boundaryWorktree:
objectui-issue-4934Domain:
domain:uiFile surface:
packages/data-objectstack/src/index.ts(notifyDroppedFields+notifyBatchDroppedFields) + the package test dir (stop on breach; explain in the report)Container & model:
M,mode:subagent,model:opus``Clause-②: no — validating an existing wire value against an already-published spec enum restores declared = enforced; the accept set is unchanged and no public surface widens.
Serial constraints cleared:
packages/data-objectstack/**— PR #6828 (#6302, the analytics lowering) MERGED this session; no open PR touches the package.Generated by Claude Code
claude commentedon Aug 30, 2026
os-dev-report
{ "issue": 4934, "status": "needs_decision", "branch": "claude/issue-4934-dropped-fields-reason-boundary", "pr": "https://github.com/objectstack-ai/objectui/pull/6884", "premise_still_valid": true, "summary": "The ruled boundary fix is implemented, pinned and self-green on the branch at 09af7e1: notifyDroppedFields and notifyBatchDroppedFields now parse `reason` against DroppedFieldsEventSchema.shape.reason (derived from the installed pin, not restated), keep EVERY entry, and route an unrecognized reason to a named skew arm UnrecognizedDropReasonEvent that carries UNRECOGNIZED_DROP_REASON plus the wire value verbatim in `unrecognizedReason`. WriteWarningEvent.droppedFields becomes the two-arm DroppedFieldsNotice; the spec type stays the canonical arm and is not widened to string. The shape filter's hand-written guard now asserts only what it checks (reason typed unknown), so the original lie is deleted rather than worked around. CLAUSE-2 DOES NOT FIRE: the skew arm landed on the adapter's own boundary types, nothing was added to the published @objectstack/spec type, and spec-symbol-batch6's pin that DroppedFieldsEvent IS the spec type still passes. BUT THE RULING'S STOP CONDITION DID FIRE. The sole consumer, packages/app-shell/src/providers/writeWarningToast.ts, keeps its runtime behaviour exactly (13/13 of its tests pass against the new boundary; the cause-free line is produced word for word) yet no longer COMPILES: app-shell type-check yields exactly 2 TS2345 errors, both the Map key and the lineFor parameter pinned to the spec union, and a full workspace build gives 39 of 40 packages successful with only app-shell failing on those same two lines. This is inherent to clause 3 rather than to my spelling of it: for those lines to compile the skew reason would have to be assignable to the spec union, which is the defect itself, so every faithful skew arm errors at the same two sites. The ruled fix is therefore NOT separable into two independently-green PRs, and the PR is deliberately left not-mergeable and marked `Part of` rather than a closing keyword. I did not apply the two-line consumer widening: packages/app-shell is outside the dispatched file surface and the maintainer-confirmed ruling asked for the fork to be reported rather than absorbed.", "tests": "RED-FIRST (assertions written and run BEFORE the implementation, against the pre-fix boundary; no dist involved — the test imports ./index from source): `pnpm exec vitest run packages/data-objectstack/src/droppedFieldsReason.boundary.test.ts` gave `Tests 3 failed | 3 passed (6)`, the failures printing the lie itself — `AssertionError: expected 'some_future_reason' not to be 'some_future_reason'` and a diff showing received `\"reason\": 42` where the skew arm was expected. The 3 that PASSED pre-fix are the discrimination controls (in-union reasons already arrived untouched), so the pin bites only on the skew path. GREEN on the committed head 09af7e1: `pnpm exec vitest run packages/data-objectstack/` -> `Test Files 49 passed (49) / Tests 669 passed (669)` (662 before, 7 new); `pnpm --filter @object-ui/data-objectstack type-check` -> echoed `tsc --noEmit`, exit 0 (its tsconfig includes test files — verified by `tsc --listFiles`, 57 test files in the program — so the @ts-expect-error pinning that the skew arm is NOT assignable to DroppedFieldsEvent is enforced, not decorative); `pnpm --filter @object-ui/data-objectstack lint` -> exit 0, `406 problems (0 errors, 406 warnings)`, all warnings the pre-existing house no-explicit-any level. GATE UNION re-run on 09af7e1 AFTER the final commit: check:control-bytes, check:spec-symbols, check:phantom-deps, check:self-import, check:esm-specifiers, check:readme-exports, check:spec-floors, check-changeset-no-major, check-changeset-presence, check-changeset-fixed — all EXIT=0. IMPORTANT, reported as NOT MEASURED rather than green on their first run: check:readme-exports and check:spec-floors initially failed with `not on disk -- run pnpm build first` and `[no-artifact] ... produced no build output to judge`, which is an unbuilt workspace, not my diff; after `turbo run build` their real readings are readme-exports `0 unbuilt, 3282 export symbols read from 37 of 40 packages` and spec-floors `274 (subpath, symbol) pairs judged`, both exit 0 — so the new RUNTIME import of DroppedFieldsEventSchema from @objectstack/spec/data clears the declared ^17.2.0 floor. CONSUMER MEASUREMENT (the fork): `pnpm --filter @object-ui/app-shell type-check` -> exit 2 with exactly 2 errors, both `src/providers/writeWarningToast.ts` TS2345 saying the four-arm reason is not assignable to the three-arm spec union; `pnpm exec turbo run build --filter='!@object-ui/site' --concurrency=2` -> `Tasks: 39 successful, 40 total / Failed: @object-ui/app-shell#build` on the same two lines. That measurement used dist, so data-objectstack was rebuilt first and the marker was confirmed to have reached it (grep -c UNRECOGNIZED_DROP_REASON: dist/index.d.ts 5, dist/index.js 3), and app-shell's whole dependency closure was built beforehand — an earlier run of the same command produced only `Cannot find module` errors and was discarded as NOT MEASURED. Consumer RUNTIME unaffected: `pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts` -> `Tests 13 passed (13)`. LINT SCOPE narrowed to the affected package with the narrowing measured rather than assumed: population came from eslint's own selection (`--format json`, 55 files), and eslint.config.js declares no parserOptions.project or projectService, so type-aware linting is off and this diff cannot move the verdict on any untouched file; the repo-wide `eslint .` stays CI's. All heavy runs went through the shared verify lock (`os-verify-lock.sh`); every exit code was captured before any pipe.", "mcp_calls": "5 — issue_read get, issue_read get_comments, issue_read get_sub_issues, add_issue_comment (claim), add_issue_comment (this report). Everything else went over repo-scoped REST, which answered 200 from this seat (open-PR list, PR create, PR body read-back).", "open_questions": [ { "question": "The ruled shape cannot land green without widening two type annotations in packages/app-shell/src/providers/writeWarningToast.ts, which is outside this task's file surface. Landing the boundary PR alone leaves main unbuildable (39/40 packages build; app-shell fails). Who applies the consumer edit, and does it ride in this same PR? ZONE 2 assumption 2 is CONFIRMED — the sole reason consumer still exists and still handles off-union explicitly at runtime — and it is exactly the fork the dispatch predicted.", "options": [ "A — authorise the two-line widening in this same PR: `new Map(DroppedFieldsEvent['reason'] -> DroppedFieldsNotice['reason'])` for byReason, and the same swap on lineFor's parameter. Naming DroppedFieldsNotice['reason'] rather than string is deliberate: STRIPPED_LINE stays declared over the SPEC union, so objectui#3935's guarantee that a NEW SPEC ARM fails type-check unworded survives untouched. Zero runtime change (13/13 consumer tests already green). The file is uncontended — no open PR touches it or packages/data-objectstack (checked #6814, #6345, #6082, #5400, #4958).", "B — revisit clause 3: keep droppedFields typed DroppedFieldsEvent[] and deliver skew entries on a separate named channel on WriteWarningEvent. Consumer compiles untouched, but the sole reader iterates droppedFields only, so skew entries would become invisible to the toast — recreating precisely the silence objectui#3484 removed, and doing it silently.", "C — leave the boundary as it is (the card's option 3, per-consumer discipline). Already refused by the ruling; recorded only so the fork is presented whole." ], "recommendation": "A, on all four axes. REAL BUSINESS NEED: measured pull is zero today — no off-union reason exists in practice and the one reader already copes at runtime — so this is hardening, exactly as triage classified it; the need served is structural, every future consumer, and A is the only option that actually delivers it. LONG-TERM SOUNDNESS: A is the only option that leaves declared = enforced at one boundary; B buys a green compile by making the data invisible, which is a worse defect than the one being fixed and is a workaround in the sense AGENTS.md 0.1 bans; C is the status quo the maintainer already declined. AI-ERROR RESISTANCE: A converts server skew from an N-consumer discipline into a tsc error at the one place the wire is read, which is what makes the next exhaustive-looking table impossible to write wrong — and it does so WITHOUT loosening the spec-union exhaustiveness #3935 established. STARTUP SCOPE DISCIPLINE: A is two type annotations in one uncontended file, no new API surface, no new package, no widening of any published spec type. The cost of A is the only thing genuinely at stake — a dev seat editing outside its dispatched surface — which is why it is being asked rather than done." } ], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Aug 30, 2026
os-dev-report
{ "issue": 4934, "status": "done", "branch": "claude/issue-4934-dropped-fields-reason-boundary", "pr": "https://github.com/objectstack-ai/objectui/pull/6884", "premise_still_valid": true, "summary": "Patch round on the existing branch, executing the 2026-08-30 option-A ruling. Exactly two type annotations changed in packages/app-shell/src/providers/writeWarningToast.ts (lineFor's parameter, and the byReason Map key) plus the DroppedFieldsNotice type import; STRIPPED_LINE keeps its Record declaration over the SPEC union at line 108, unmoved, so objectui#3935's guarantee survives. The changeset now states the blast radius and that the consumer's compile error is the intended signal; the bump level is unchanged at data-objectstack minor. The PR body now says plainly that clause 2 FIRES and the review is fable-tier, keeps the still-true narrow part (nothing added to the published spec type, spec-symbol-batch6's pin passes), and carries the evidence that the file already handled the skew value at runtime deliberately. Part of #4934 retained, no closing keyword. THREE CORRECTIONS TO THE ORDER. (1) The workspace build reading is 43 successful / 43 total, NOT 40 of 40. The '40' denominator existed only because the build was failing: app-shell#build has three direct dependents in the turbo graph (console, example-console-starter, example-byo-backend-console), and when it fails they are never attempted and never counted, so the previous seat's '39 successful, 40 total' plus 3 cancelled equals today's 43. Asking for 40/40 would have been asking for a number that cannot occur on a green tree. (2) The gate union owed more than the dispatched clue list: check-changeset-overwrite.mjs is a real gate in changeset-guard.yml and this round EDITS an existing changeset, and writeWarningToast.ts is a wording table that check:i18n-keys / i18n-drift / i18n-dead-keys read. All four extra gates run and are green; they were derived from the actual diff, not from the clue list. (3) This PR makes two sentences in writeWarningToast.ts stale, and the two-annotation bound means I report them rather than edit them: strippedLineUnknownReason's docstring still says the adapter asserts the entry without ever checking reason against the spec enum (the boundary now checks it), and lineFor's docstring opens 'The PARAMETER carries the spec union' (it no longer does; and that causal claim was already loose, since STRIPPED_LINE's exhaustiveness comes from its own Record declaration). Neither affects behaviour or any gate, and the sentence the ruling quotes as its evidence stays true. Both are written up in the PR body under 'Known drift this PR creates and deliberately does not fix'. No app-shell entry was added to the changeset: both edited sites are module-private (lineFor is not exported, byReason is a local), the emitted JavaScript is byte-identical, so app-shell has no user-visible change of its own; say the word if you want a patch entry anyway.", "tests": "All readings on the final head c01e5a2, gate union re-run AFTER the last commit. Every exit code captured before any pipe; all heavy runs through os-verify-lock.sh. CONSUMER TYPE-CHECK (the thing that made this a fork): pnpm --filter @object-ui/app-shell type-check -- echoed the real script '@object-ui/app-shell@17.6.0 type-check' and 'tsc --noEmit && tsc -p tsconfig.test.json', lock VERDICT command-exit 0. The echo excludes a zero-match false green, and the second leg means the package's test files are in the program too. WORKSPACE BUILD: pnpm exec turbo run build --filter='!@object-ui/site' --concurrency=2 -- turbo's own summary line 'Tasks: 43 successful, 43 total', 'Cached: 39 cached, 43 total', zero Failed lines. No 'Cannot find module' shape appeared; the closure was built by this same run and data-objectstack's dist was confirmed to carry the marker (grep -c UNRECOGNIZED_DROP_REASON dist/index.d.ts dist/index.js gave 5 and 3, and dist/index.d.ts exports 'type DroppedFieldsNotice'). See summary correction (1) for why 43 and not 40. CONSUMER RUNTIME: pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts --reporter=verbose -- 'Test Files 1 passed (1)', 'Tests 13 passed (13)', exit 0, all 13 names printed including 'names the fields and claims no cause for a reason ahead of the spec pin (#3935)', which is the skew path. No message changed. ZERO RUNTIME CHANGE PROVEN, NOT ASSERTED: transpiled the file before and after the diff through the TypeScript API (ES2022/ESNext, 0 diagnostics); both emit 6686 bytes with sha256 6a5c5a957517a0489699ff85054a5edd7409f8809d5fe0084ad96f2a16655e9d, i.e. byte-identical JavaScript, while the source diff is non-empty (control printed). NOTE: my FIRST attempt at this used 'pnpm exec esbuild', which is not installed; both legs exited 254 and both wrote the same 113-byte pnpm error text, so the hashes matched and it read as a clean proof. That reading was DISCARDED as NOT MEASURED and redone -- reporting it because it is exactly the failure shape the order warns about. DATA-OBJECTSTACK UNMOVED: pnpm exec vitest run packages/data-objectstack/ -- 'Test Files 49 passed (49)', 'Tests 669 passed (669)', exit 0. Unchanged by this round, as expected. LINT: pnpm --filter @object-ui/app-shell lint -- '2798 problems (0 errors, 2798 warnings)', exit 0. eslint really did report on the edited file: --format json gives a population of 1022 files and packages/app-shell/src/providers/writeWarningToast.ts is in it with errorCount 0, warningCount 0; totals across the JSON are 0 errors / 2798 warnings, matching the script run. Narrowing to the package is measured, not assumed: eslint.config.js is 268 lines with 0 hits for projectService/parserOptions and 10 for 'rules' as a positive control, so type-aware linting is off and this diff cannot move the verdict on any untouched file; the repo-wide eslint . stays CI's. GATE UNION on c01e5a2, each exit captured individually: control-bytes 0, spec-symbols 0, phantom-deps 0, self-import 0, esm-specifiers 0, readme-exports 0, spec-floors 0, changeset-no-major 0, changeset-presence 0, changeset-fixed 0, changeset-overwrite 0, i18n-keys 0, i18n-drift 0, i18n-dead-keys 0, plus app-shell type-check 0 on the restored tree. The two dist-reading gates gave REAL readings, not no-artifact: readme-exports printed 'check-readme-exports: OK (... 3282 export symbol(s) read from 37 of 40 package(s) ... 0 unbuilt, 3 declare no types)' and spec-floors printed '274 (subpath, symbol) pair(s) judged' with 'Every consumer-facing @objectstack/spec floor carries the symbols its package's artifact references.' REVERSE VERIFICATION on the widening itself, committed first so the revert had a real restore point. PREDICTED BEFORE RUNNING: reverting only lineFor's parameter and leaving the Map key widened yields exactly one TS2345, at the CALL SITE inside emitWriteWarning rather than at the declaration, because byReason's key is by then the four-arm union. OBSERVED, exactly that: 'src/providers/writeWarningToast.ts(207,24): error TS2345: Argument of type \"readonly\" | \"readonly_when\" | \"primary_key\" | \"objectui:unrecognized-drop-reason\" is not assignable to parameter of type \"readonly\" | \"readonly_when\" | \"primary_key\".' with 'Exit status 2' and lock VERDICT command-exit 2; TS2345 count 1, total TS error lines 1. MUTATION CONFIRMED ON DISK before the run: injected text present exactly once, removed text absent (0), Map key still widened (1) as a control. RESTORE PROVEN BY OBSERVED STATE, not by an exit code: trap on EXIT/INT/TERM with an absolute path built from git rev-parse --show-toplevel, restoring with 'git checkout HEAD -- ABSPATH' (named ref, never bare, so the index cannot hand back the mutation); afterwards on-disk git hash-object equals git rev-parse HEAD:PATH at c4560fd222fd8f5d9d961ea3534793f57e45c165 (both non-empty, checked), git diff HEAD empty, git status --porcelain empty, and the restored annotation present once with the mutation text absent. app-shell type-check returns to exit 0 on the restored tree.", "mcp_calls": "2 -- issue_read get_comments (the ruling), add_issue_comment (this report). Everything else went over repo-scoped REST, which answered 200 from this seat for both the PR read and the PR body PATCH; the card's own page was read through the zero-quota web payload first, which carried 15 of 22 timeline items and could not reach the ruling comment, hence the one targeted MCP read.", "open_questions": [ { "question": "Should the two now-stale docstring sentences in writeWarningToast.ts be corrected in this PR? The ruling bounded the round to two annotations and 'nothing else in packages/app-shell', so I left them and documented them in the PR body instead.", "options": [ "A -- leave them; a follow-up card corrects both sentences together with any other prose drift this seam accumulates.", "B -- authorise two prose edits in this PR: drop the stale mechanism clause from strippedLineUnknownReason's docstring, and rewrite lineFor's opening sentence to attribute exhaustiveness to STRIPPED_LINE's own Record declaration rather than to the parameter.", "C -- correct them at review time as review feedback on this PR." ], "recommendation": "B, narrowly. The cost is two sentences in a file already open in this diff, and the benefit is that the file stops describing a boundary that this very PR deletes -- a stale comment about a type contract is the same class of defect as the card itself, read by the next agent as though it were current. I did not do it unasked because the ruling's bound was explicit and because rewriting a docstring's causal explanation is prose authorship, not a mechanical fix, so it is not covered by any bounded-in-place exemption. If the answer is A, the follow-up card should be filed against this seam rather than left to memory." } ], "out_of_scope_findings": [ "NOT FILED as separate cards, deliberately -- both are drift this PR itself creates in the file it edits, so a separate card would duplicate the fable-tier review that is already required: (a) strippedLineUnknownReason's docstring in packages/app-shell/src/providers/writeWarningToast.ts still describes the pre-fix boundary ('asserts the entry ... without ever checking the value against the spec enum'); (b) lineFor's docstring opens 'The PARAMETER carries the spec union', which this diff makes false. Both are recorded in the PR body under 'Known drift this PR creates and deliberately does not fix' and raised as the open question above -- PM to decide whether they ride here or become a card." ] }Session:
session_013hfmP9hoMd3dJwTh85J4yBGenerated by Claude Code
claude commentedon Aug 30, 2026
os-dev-report
{ "issue": 4934, "status": "done", "branch": "claude/issue-4934-dropped-fields-reason-boundary", "pr": "https://github.com/objectstack-ai/objectui/pull/6884", "premise_still_valid": true, "summary": "Both sentences WERE stale against the current tree; I re-derived their exact text rather than trusting the paraphrases, and corrected them in commit 3ec17deb on the existing PR (no new PR). (1) strippedLineUnknownReason's docstring described the pre-fix mechanism ('asserts the entry into DroppedFieldsEvent without ever checking the value against the spec enum'); notifyDroppedFields now maps every wire entry through asDroppedFieldsNotice, which parses reason against a Set built from DroppedFieldsEventSchema.shape.reason.options and routes an unrecognized value to UNRECOGNIZED_DROP_REASON. The docstring now states that mechanism; its conclusion (the branch is still reachable) was true before and is true now, so it was preserved. (2) lineFor's docstring said the parameter carries the spec union (it now carries the two-arm notice union) and hung STRIPPED_LINE's exhaustiveness on that parameter; it now attributes exhaustiveness to the table's own declaration keyed by DroppedFieldsEvent['reason'], which is where it always lived. Bounds honoured: STRIPPED_LINE's declaration did not move (line 108); no code changed; nothing else under packages/app-shell; no changeset entry added and the existing changeset untouched. The protected option-A evidence sentence is byte-identical AND unmoved: I deliberately ended my replacement text at the exact byte where that sentence begins, so its three lines never entered the diff as changed lines, only as context. ONE CORRECTION TO THE RULING, which is why the emit proof is reported in two modes: the ruling's ground for no changeset was 'the emitted JavaScript is byte-identical ... the prose edits B adds cannot change that'. That is true only with comments STRIPPED. tsconfig.base.json line 22 sets removeComments:false and app-shell builds with a bare tsc, so docstring bytes do reach dist/index.js and dist/index.d.ts; the comment-preserving emit legitimately differs. The ruling's CONCLUSION survives on stronger ground than the phrase it used: the executable emit is proven identical, and every changed line of the comment-preserving emit is a comment line. Read it as 'byte-identical executable emit'. SECOND CORRECTION, to a standing lane note: the 2026-08-26 note says every PATCH edit downgrades the session-URL footer to the bare form. It did not here. I PATCHed the body via REST and the full read-back was byte-identical to what I sent, session-form footer intact (1 occurrence). Also: the previous round's '6686 bytes' is that same emit counted in UTF-16 code units, not bytes; this file carries CJK and em-dashes, so its byte length is 6726. The sha256 6a5c5a95... reproduced exactly, so the two rounds are measuring the same artifact.", "tests": "All readings on head 3ec17debb with a clean tree (git status --porcelain empty at commit time), exit codes captured BEFORE any pipe, each quoting the gate's own verdict line. EMIT IDENTITY, two modes, each leg carrying a success control (0 diagnostics, real JS emitted, three known markers present) because a digest from a command that failed identically twice is a perfect false proof: comments-stripped BEFORE sha256 f9ff9b85b9cf39e28caf81a4bd99708bfc36b903b190af35fe56bca731e91bf8 / 2181 B, AFTER f9ff9b85... / 2181 B -> IDENTICAL, diff empty; comments-kept (removeComments:false, the real build) BEFORE 6a5c5a957517a0489699ff85054a5edd7409f8809d5fe0084ad96f2a16655e9d / 6726 B, AFTER 393f0180f8483dcfc1d106671185f5881cf96f3640442a237bce974d1503f1e3 / 6854 B -> DIFFERS, and filtering that emit diff for changed lines which are not comment lines returns nothing. The non-empty source diff is printed alongside: 8 insertions, 6 deletions, one file. NOTE ON MY OWN FIRST PROBE: its stripped-mode control failed (CONTROL_PASSED[strip] false) purely because I set the size floor at 3000 B and the stripped emit is 2181 B; I fixed the threshold, not the measurement, and re-ran both legs. TYPE-CHECK: pnpm --filter @object-ui/app-shell run type-check echoed 'tsc --noEmit && tsc -p tsconfig.test.json', os-verify-lock VERDICT command-exit 0, held 68s. Dependency closure was built FIRST (pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, VERDICT command-exit 0, held 114s) because a fresh worktree had no dist for any of the six referenced projects, so an unbuilt type-check would have been TS6305 = NOT MEASURED, not green. TESTS: pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts from the REPO ROOT (not --filter, which is objectui's documented false-green trap) -> 'Test Files 1 passed (1)' / 'Tests 13 passed (13)', messages unchanged. GATES, all exit 0: check:i18n-keys ('Every in-scope call-site key resolves against the en pack (2834 keys)'), check:i18n-drift ('0 en value(s) changed'), check:i18n-dead-keys (exit 0, and it declares of itself 'This is a REPORT, not a gate'), check:control-bytes ('scanned 5737 tracked text file(s)'), check-changeset-presence ('3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-fixed, check-changeset-no-major, check-changeset-overwrite ('1 changeset(s) added, 0 modified, 0 deleted' - proves I did not touch the existing changeset), check:shell-escape-residue, check-type-check-coverage, check-lint-coverage. Gate set derived from MY actual diff against objectui's own package.json and .github/workflows, NOT from the dispatch list and NOT from objectstack's dispatch-gates.mjs, which lives only in objectstack and would have answered about the wrong tree. BYTE DISCIPLINE: control-byte self-scan on the edited file returned grep exit 1 (no match); the only non-ASCII character introduced is U+2014 EM DASH, which the file already used. LINT is a DECLARED NARROWING, not a repo scan, and carries the three things a narrowing owes: population from eslint's own resolution (the file resolves a config and is linted, not ignored - 1 file in the JSON report); count from --format json (0 errors, 0 warnings); invariance because --print-config shows parserOptions with no project and no projectService, i.e. linting is not type-aware, so a comment-only edit in one module cannot move any untouched file's verdict. NOT RE-RUN, with reason: spec-symbols, spec-floors, readme-exports, phantom-deps, self-import, esm-specifiers, published-dist, vi-mock-specifiers - every one has the export surface, dist artifacts, module graph or registration tables as its subject, and the executable emit is proven unchanged. PROTECTED SENTENCE: its exact three-line block occurs exactly once in both HEAD and the edited tree, and appears in the diff only as context lines.", "mcp_calls": "0 - a repo-scoped REST probe returned HTTP 200 on this seat, so every GitHub read (the ruling comment, the PR, the read-back) and the body PATCH went through REST. No MCP GitHub tool was called, and no duplicate-search was needed because no issue was filed.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code