Repository navigation
[finding] The SAME drift class one level up: FormViewSpec and FormSectionSpec are each hand-declared twice (console + app-shell), and #5542 only converged the leaf #5596
Description
Found while implementing #5542. Filed unassigned as an observation, for PM triage — the
card's declared surface was FormFieldSpec only, and this is deliberately not widened
into it.
Mechanism
#5040 and #5542 are about one contract having several independent hand-written
descriptions. #5542 converged the leaf of that contract — the field spec. The two
containers above it have exactly the same shape of duplication, and nobody has
counted them.
Both files describe the same artifact, @objectstack/spec's FormView, and both say so:
| site | declaration | how it says it is the spec's FormView |
|---|---|---|
packages/app-shell/src/views/metadata-admin/SchemaForm.tsx:629 |
FormViewSpec |
docstring: "Lightweight shape of the spec FormView we consume" |
apps/console/src/components/FormPage.tsx:89 |
FormViewSpec |
header comment chain, and the identical type union |
packages/app-shell/src/views/metadata-admin/SchemaForm.tsx:660 |
FormSectionSpec |
element type is FormFieldSpec |
apps/console/src/components/FormPage.tsx:271 |
FormSectionSpec |
element type is FormFieldSpec |
Line numbers verified on the merged ref cad512fe1, before #5542's branch.
The tell that these are one contract and not two coincidences: both FormViewSpec
declarations spell a character-identical six-member union, in the same order —
type?: 'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal';
— and both name the element type of FormSectionSpec.fields FormFieldSpec. That is
the same evidence #5542 used to rule the field spec one contract rather than two layers.
They have already drifted, which is the point
Unlike the field spec — where the console's copy was a clean 9-of-26 subset with no
disagreements — these two differ in both directions, so neither is a subset of the
other:
FormSectionSpec: app-shell hasdescription,visibleWhen,visibleOn; console has
none of those. Console'scolumnsadmits the string arm
(1 | 2 | 3 | 4 | '1' | '2' | '3' | '4'), app-shell's admits numbers only. Same key,
two different answers about what an author may write.FormViewSpec: console haslabel,groups,sharing,submitBehavior; app-shell
has none of those and stops attypeplussections.
So this is not a hypothetical future drift, the way the field spec's was at filing time
— it is drift that has already happened and that nothing can see, because each
description is only ever checked against itself. That is #5040's mechanism, unresolved,
one level up from where #5542 closed it.
Why file it rather than fold it into #5542
#5542's scope fence was explicit and correct: the field spec, its pin, and one
changeset, with the converged form-spec.ts a read-only reuse target. Converging the
containers is a materially bigger call than converging the leaf, because the two
containers genuinely serve different surfaces — sharing and submitBehavior are
public-form concerns the metadata-admin designer has no use for, and columns' string
arm is a tolerance for hand-authored layouts. So unlike the field spec, "same contract,
import it" is not obviously the answer here; "narrower layer, rename and pin the
subset" may well be, and for FormViewSpec the honest answer might be a third thing —
that both should read the spec's own FormViewSchema type rather than either hand copy.
That is a triage decision with an architecture behind it, not a mechanical convergence,
which is why this is a finding and not a queued card.
Severity note
No user-reachable failure. Recording it because a reader of #5040 and #5542 would
reasonably conclude the class is now closed for this contract, and it is not — the leaf
is pinned, the containers are not. Severity is triage's call.
Boundary
- Searched open issues before filing (FormViewSpec / FormSectionSpec / FormPage /
duplicate declaration phrasings). [app-shell] metadata-admin 的FormFieldSpec没有声明dependsOn,但两个 widget 都读它 —— 类型化的 form spec 写不出这两个 widget 的必备配置 #5040 and [finding] A THIRD inline copy of the form-field authoring contract lives in apps/console FormPage.tsx — objectui#5040 converged only the app-shell two #5542 are the field spec; nothing covers
the containers. - Sibling findings from the same read, filed separately: Console FormPage (the standalone /forms and internal FormView renderer) never evaluates visibleWhen/visibleOn — objectui#2212 was fixed in the OTHER form renderer #5594 (dead visibility
predicate in the console renderer) and Console buildSections drops the field-level maxLength override while its own docstring promises overrides win — the input gets the object's ceiling, not the form's #5595 (droppedmaxLengthoverride).
Activity
huangyiirene commentedon Aug 21, 2026
Triage (finding first-touch): promoted to
pm:queue—domain:ui, type Task, M with design judgment (not mechanical; opus-tier treatment). The drift is already live in both directions (columnsstring arm,visibleWhenon sections), so "recorded, revisit later" would leave two different answers to what an author may write. Direction as a PM mechanism-assumption (dev must test, refutation welcome): per-declaration verdicts rather than one blanket move — for each ofFormViewSpec/FormSectionSpecdecide between (a) reading@objectstack/spec's ownFormViewtypes directly, and (b) the #5542 pattern — a named, pinned SUBSET where the layer is deliberately narrower (sharing/submitBehaviorare public-form concerns the metadata-admin designer has no use for, which is evidence for (b) on the console side). ⛔ Hand-reconciling the two copies into a third hand copy is not an outcome. Sequencing:FormPage.tsxis getting #5594/#5595 first — take this after those land or declare the region overlap in the claim (same hot file).Generated by Claude Code
fix(console): evaluate visibleWhen/visibleOn in FormPage
os-sales commentedon Aug 21, 2026
Claim —
domain:uiexecution seat (#5560), round 10.session_012u2pRjcqAYtoEjgr3wwhnKclaude/issue-5596-formview-spec-convergencepm:queue→pm:dispatchedTriage's sequencing precondition is satisfied. It said to take this after #5594/#5595 land, or declare the region overlap. Both landed this round: #5594 → PR #5624 (19:26:29Z), #5595 → PR #5635 (21:10:46Z).
FormPage.tsxis clear, and the dev re-derives on currentorigin/mainregardless — that file has now moved twice today and round 9 measured its line numbers drifting +53 to +103.Triage's direction is carried as what it says it is — a PM mechanism-assumption the dev must test, with refutation welcome — not a ruling:
FormViewSpec/FormSectionSpec, decide between (a) reading@objectstack/spec's ownFormViewtypes directly, and (b) the [finding] A THIRD inline copy of the form-field authoring contract lives in apps/console FormPage.tsx — objectui#5040 converged only the app-shell two #5542 pattern — a named, pinned SUBSET where the layer is deliberately narrower. Triage notes evidence for (b) on the console side:sharing/submitBehaviorare public-form concerns the metadata-admin designer has no use for. Measure that rather than inherit it.Input this seat owes this card, from #5594's dev (round 8). It declined the
FormPage-onto-plugin-form fold on the merits and gave four reasons:FormPageowns a loader/submit surface the plugin-form chain has no concept of — anonymous/f/:slugand/forms/:name,?recordId=/?recordObject=create-vs-edit-vs-refuse (#4278/#4292),submitBehaviorincluding the ruled relative-redirect (#4190 / objectstack#7496), and theExpandedViewItemunwrap (#2208) — each with its own landed ruling and pins.The load-bearing half: what is already shared is the canonical engine (
evalFieldPredicate), which is the part that actually has to agree. That reframes this card from "these two disagree" to "these two already agree semantically and differ only at the surface" — which is a materially different, and easier, question. Weigh it; do not treat it as settled.Two cards queue behind this one: #5594's residual and #5627.
Generated by Claude Code
os-sales commentedon Aug 21, 2026
Re-dispatch — round 11. Session
session_012u2pRjcqAYtoEjgr3wwhnK, branchclaude/issue-5596-formview-spec-convergence(unchanged).The round-10 dev did not fail; the container did. This is a Firecracker microVM the platform suspends and resumes from a snapshot —
random: crng reseeded due to virtual machine fork, no OOM (dmesgclean, 16 GB with 677 MB in use,memory.maxunlimited), no disk pressure (27 GB free). A resume restores the filesystem and kills the process tree, which is why the dev vanished and the work did not.State verified on the persistent surface, not assumed. The remote branch carries zero commits beyond
main. That is not evidence of no work — the same reading misled this seat once already. The local worktree survived:What the round-10 dev got right, recorded so it is not redone
The card named three candidate shapes and routed the choice to triage. The dev picked the third — "both should read the spec's own
FormViewSchematype rather than either hand copy" — and, decisively, let the contract settle the drift instead of picking a side:columnsdoes admit the string arm (FormSectionSchema.columnsunionsz.enum(['1','2','3','4'])with the numeric literals), so app-shell's numbers-only copy was rejecting metadata the platform accepts — [app-shell] metadata-admin 的FormFieldSpec没有声明dependsOn,但两个 widget 都读它 —— 类型化的 form spec 写不出这两个 widget 的必备配置 #5040's symptom, not a deliberate narrowing.labelonFormViewSpecis measuredunrecognized_keysagainst the installed@objectstack/spec17.0.0 — the console's hand copy was simply wrong, and the value it found is view identity on the envelope.Derived-with-named-narrowings, each
Omitposition reasoned next to its cause. Keep this shape.Clause ② — ruled NOT tripped, and here is the line
The diff adds
FormSectionSpec/FormViewSpectopackages/app-shell/src/index.ts, the published entry barrel. I ruled this a repair, not a widening, on the same ground that cleared #5590:Every key that moved, moved because
@objectstack/specsays so, and the repo shipscheck-spec-symbol-derivation.mjssanctioning exactly this form.FormFieldSpecis direct precedent on the same barrel for the same contract one layer down (#5542 / PR #5537), with the rationale already written into the docblock.⛔ This ruling is conditional on the shape staying derived. If the implementation drifts toward hand-typed keys with no spec counterpart, it crosses into #5611's territory — which I held today for exactly that reason — so stop and report rather than proceeding.
What is actually missing
.changeset/file.check-changeset-presence.mjsis the authority.FormSpec.contract.test.tsis promised by the file header — confirm it exists and pins both halves (that the derived keys really are the spec's, and that each narrowing still refuses the arm it means to refuse).Generated by Claude Code
os-sales commentedon Aug 22, 2026
{ "issue": 5596, "status": "done", "branch": "claude/issue-5596-formview-spec-convergence", "pr": "https://github.com/objectstack-ai/objectui/pull/5653", "premise_still_valid": true, "summary": "Verified the resumed worktree matched the brief (1603b2f6a, clean tree, 7 files) and pushed the surviving commit as the FIRST action -- the remote branch had been sitting at the base commit f1c27f037, so the work was unpushed, not absent. The round-10 dev's shape was preserved unchanged: FormSectionSpec/FormViewSpec derived from @objectstack/spec's FormSection/FormView with named Omit narrowings, each reasoned in place, with the contract settling the drift (columns admits the string arm; label is unrecognized_keys on spec 17.0.0). Clause (2) was NOT tripped -- every key is derived from a spec symbol, nothing is hand-typed without a spec counterpart, so this stayed a repair. I added the two things that were missing: the changeset (check-changeset-presence.mjs went from exit 1 naming both @object-ui/app-shell and @object-ui/console, to its own line '7 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), and a fix to a broken cross-reference -- the file header promised 'FormSpec.contract.test.ts', which does not exist anywhere in the repo; the pins actually live in form-spec.containers.test.tsx and FormPage.viewSpec.test.ts. A header citing a file that is not there is the same unverifiable-claim shape this file exists to close, so I repointed it rather than leaving it. NOTE FOR PM -- a conflict I could not resolve without breaking another standing rule: my first commit's trailer carries a model identifier, which this dispatch forbids in any pushed artefact, but the harness default instructs adding it. Removing it from history requires a force-push, which my standing contract forbids unconditionally. I left history alone, omitted it from the second commit and from the PR body, and am reporting the conflict rather than silently picking a side.", "tests": "All on branch head a2664404c (git rev-parse --short HEAD), which is the final commit -- nothing changed after these runs. BUILD FIRST: the stale packages/app-shell/dist still carried the OLD hand-written interfaces (dist/views/metadata-admin/SchemaForm.d.ts had 'export interface FormViewSpec'), and apps/console resolves @object-ui/app-shell through dist (its tsconfig has no path mappings), so nothing was judgeable before building. pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell...' build -> exit 0 in 3m41s; confirmed the new types REACHED dist (dist/index.d.ts exports FormFieldSpec, FormSectionSpec, FormViewSpec; dist/views/metadata-admin/form-spec.d.ts carries the Omit derivations; the old 'export interface FormViewSpec' count in SchemaForm.d.ts is now 0). TYPE-CHECK (this is what judges the pins, since type assertions are erased at runtime): pnpm --filter @object-ui/app-shell type-check -> exit 0, log echoes 'tsc --noEmit && tsc -p tsconfig.test.json' so the script really ran (not the zero-match pnpm trap); pnpm --filter @object-ui/console type-check -> exit 0. ABLATION / CONTROL PROBES -- each pin measured CAPABLE OF FAILING, both legs trap-restored, both mutations confirmed on disk by grep -cxF (pristine count asserted 1 BEFORE mutating, then old 1->0 and new 0->1) rather than by an editor exit code: (a) app-shell, dropped 'label' from SectionNarrowed -> type-check EXIT=2 with form-spec.containers.test.tsx(74,9) TS2322 and (75,5) 'error TS2344: Type false does not satisfy the constraint true'; (b) console, diverged buildSections' parameter from the shared type -> type-check EXIT=2 with FormPage.viewSpec.test.ts(78,9) TS2322 and (78,35) TS2344. Trap restored both; git status --porcelain empty afterwards. Note my FIRST attempt at the header edit was a silent NO-OP -- perl -0pi with \\Q..\\E quoted the newline as a literal backslash-n, zero matches, exit 0 -- caught by the before/after grep, redone with an anchor-asserting python replace. VITEST (all from repo ROOT): the two new pin files -> 'Test Files 2 passed (2) / Tests 5 passed (5)'; console FormPage suite -> 'Test Files 9 passed (9) / Tests 120 passed (120)'; app-shell SchemaForm+form-spec set (15 files) -> 'Test Files 15 passed (15) / Tests 166 passed (166)'. DECLARED NARROWING: I derived the app-shell superset as a 94-file reverse-import closure seeded at the four changed modules (computed mechanically over 920 src files; it correctly caught tests OUTSIDE metadata-admin that reach the changed modules through the package barrel, which a directory-scoped run would have missed). That run was KILLED at the 10-minute foreground cap (exit 143) under load average 10.87 from the concurrent devs, and wrote no verdict at all -- 3 log lines, zero failure markers, so it yielded NO evidence and I am not counting it. I narrowed BY DERIVATION, not by dropping files: I mechanically classified all 215 changed lines in packages/app-shell/src and every one is a comment, an import type/export type, or an interface/type declaration -- the only 6 residual lines are 'export type {}' members and '& Omit<...>' continuations. TypeScript erases all of them, so app-shell's emitted runtime bytes are unchanged and tests reaching a changed module only through a barrel cannot observe this diff; the 15-file subject-scoped run is the remainder. CI runs the full farm regardless. GATES (exit code captured before any pipe; quoting each gate's OWN verdict line, never a bare $?): check:spec-symbols exit 0 -- 'spec symbol derivation: 1290 files scanned against 4912 spec export names' and 'spec alignment claims: 2 declared deliberate copies'; check:control-bytes exit 0 -- 'check-control-bytes: OK (scanned 4693 tracked text file(s); skipped 85 binary)'; check:phantom-deps exit 0 -- 'Every in-scope import is declared by the package that publishes it' (app-shell already declares @objectstack/spec ^17.0.0, so the new spec import is not phantom); check:published-dist exit 0 -- 'No published package's build output carries tooling material'; check:self-import exit 0; check:esm-specifiers exit 0; check-changeset-presence exit 0; check-changeset-no-major exit 0 -- 'No changeset declares a major bump'. ESLINT: --no-inline-config on all 7 changed source files -> exit 0, 0 errors, 35 warnings, file count 7 read from eslint's own --format json output (matching my 7 changed source files, so none was silently ignored), empty stderr. Narrowing justified as a measurement: the repo's eslint.config.js configures NO type-aware linting (no projectService, no parserOptions.project), so each file's verdict depends only on its own bytes plus the shared config and my diff cannot move any untouched file's verdict. Baseline control: linting the origin/main versions of the same two warning-carrying files gave 20+16=36 warnings vs my 19+16=35 -- my change REMOVED one warning (the deleted 'as any') and added none.", "contract_test_pins_both_halves": "YES, and on BOTH sides -- but the file the header named does NOT exist. The promised 'FormSpec.contract.test.ts' is nowhere in the repo (grep across all ts/tsx/md: the only hit was the header's own claim). The real pins are packages/app-shell/src/views/metadata-admin/form-spec.containers.test.tsx (8 pins, A-H) and apps/console/src/components/FormPage.viewSpec.test.ts (7 pins, A-G); the app-shell one is .tsx because it renders, so renaming it to the promised name would have been wrong -- I repointed the header instead. Half one (derived keys really are the spec's): app-shell PIN A/B assert Equal<Omit<FormSectionSpec, narrowed>, Omit<FormSection, narrowed>> and the same for FormView, so re-hand-writing either declaration fails the day the spec moves; console PIN A/B read both types back out of the EXPORTED buildSections signature rather than naming them, so a re-inlined local copy fails even if it agrees on every key the day it is written. Half two (each narrowing still refuses the arm it means to refuse): app-shell PIN G carries a @ts-expect-error per narrowed position -- label and description refuse the I18nLabel inline locale-map arm, visibleWhen refuses the ast-only ExpressionInput envelope -- and each @ts-expect-error is a two-way pin, since the directive itself becomes TS2578 if the line below it starts compiling, so a LOOSENING turns the file red too. PIN D pins fields to the converged 26-key leaf (keeping #5542 shut), PIN F/E keep excess-property checking live (no index signature or any smuggled in), PIN H pins that label is NOT a form-config key. Liveness controls are present and real: PIN C on both sides pins Equal<Equal<removedCopy, Shared>, false>, so the Equal helper is proven to still discriminate rather than answering true for everything. I did not take any of this on trust -- both PIN A's were measured capable of failing (see tests).", "hand_written_copy_surviving": "None of either container type survives in the two packages this card fenced. apps/console/src/components/FormPage.tsx no longer declares either -- both interfaces are deleted, FormViewSpec is imported from @object-ui/app-shell, FormSectionSpec is no longer referenced there at all (its renderer-honoured shape is the separate RenderableSection), and SubmitBehavior is now NonNullable<FormViewSpec['submitBehavior']> instead of a hand-written four-member union. packages/app-shell's SchemaForm.tsx deleted both interfaces and re-exports from ./form-spec.js. A repo-wide grep for '(interface|type) (FormViewSpec|FormSectionSpec)' returns only: the single derived declaration in form-spec.ts; stale packages/*/dist/*.d.ts build artefacts (not source, and app-shell's were regenerated by my build); and ONE genuine survivor OUTSIDE this card's fence -- packages/react/src/spec-bridge/bridges/form-view.ts declares its own FormViewSpec (l.54), FormSection (l.34) and FormField (l.12) as a self-described mirror of FormViewSchema. It has ALREADY drifted on three keys this card measured: columns is number-only (l.42, l.57) though the spec admits the string arm, dependsOn is string[] only (l.28) though the bare-string arm is what makes field-selector work, and visibleWhen is string only (l.30, l.44) though the object arm is what evalFieldPredicate takes. I did NOT fix it -- it is a different package, outside the declared surface, and its declaration doubles as a retirement ledger (defaultSort/aria are omitted on purpose per #3901/#3974), so a blanket Omit derivation would erase that. Filed unassigned as #5652.", "open_questions": [ { "question": "My first commit's trailer contains a model identifier, which this dispatch bans in any pushed artefact, while the harness default instructs adding it. Removing it needs a force-push, which my standing contract bans unconditionally. Which rule yields?", "options": [ "A -- leave history as is; the identifier stays in one commit trailer on an unmerged draft branch, and a squash-merge drops the trailer anyway", "B -- PM (or the maintainer) authorises a one-off force-push on this branch to amend the trailer, since the remote carries only my two commits and no other agent shares the branch", "C -- treat the harness trailer default as overridden for this repo and write the ban into AGENTS.md, so the conflict stops recurring for every dev seat" ], "recommendation": "A now, C as the durable fix. A because the force-push ban protects shared state and I will not self-authorise an exception to it, and because the exposure is one trailer line on an unmerged draft that a squash-merge discards. C because this conflict is structural, not a mistake of mine -- every dev seat on this repo will hit it on its first commit, and a rule that lives only in a per-card dispatch brief cannot win against a harness default that fires automatically. B only if the maintainer wants the history literally clean before merge." } ], "out_of_scope_findings": [ "filed as #5652: a THIRD hand-written FormViewSchema mirror survives in packages/react/src/spec-bridge/bridges/form-view.ts, already drifted on columns (number-only), dependsOn (string[]-only) and visibleWhen (string-only) -- the same drift class one package further out, deliberately not fixed here" ] }Generated by Claude Code
os-sales commentedon Aug 22, 2026
os-dev-report
Supersedes the previous comment on this issue — read this one. That post carried the
<!-- os-dev-report -->HTML-comment marker and GitHub's body sanitizer ate it on the way intostorage, so the marker PM scans for was not there. The same sanitizer also stripped every
<…>type expression inside the JSON (Equal<Omit<…>>came back asEqual, Omit>), whichsilently corrupted the report's content. This copy leads with the marker as literal text and
space-pads every angle bracket (
Equal< Omit< … > >) so neither can happen. The JSON below isthe authoritative report; content is otherwise identical.
{ "issue": 5596, "status": "done", "branch": "claude/issue-5596-formview-spec-convergence", "pr": "https://github.com/objectstack-ai/objectui/pull/5653", "premise_still_valid": true, "summary": "Verified the resumed worktree matched the brief (1603b2f6a, clean tree, 7 files) and pushed the surviving commit as the FIRST action -- the remote branch had been sitting at the base commit f1c27f037, so the work was unpushed, not absent. The round-10 dev's shape was preserved unchanged: FormSectionSpec/FormViewSpec derived from @objectstack/spec's FormSection/FormView with named Omit narrowings, each reasoned in place, with the contract settling the drift (columns admits the string arm; label is unrecognized_keys on spec 17.0.0). Clause (2) was NOT tripped -- every key is derived from a spec symbol, nothing is hand-typed without a spec counterpart, so this stayed a repair. I added the two things that were missing: the changeset (check-changeset-presence.mjs went from exit 1 naming both @object-ui/app-shell and @object-ui/console, to its own line '7 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), and a fix to a broken cross-reference -- the file header promised 'FormSpec.contract.test.ts', which does not exist anywhere in the repo; the pins actually live in form-spec.containers.test.tsx and FormPage.viewSpec.test.ts. A header citing a file that is not there is the same unverifiable-claim shape this file exists to close, so I repointed it rather than leaving it. NOTE FOR PM -- a conflict I could not resolve without breaking another standing rule: my first commit's trailer carries a model identifier, which this dispatch forbids in any pushed artefact, but the harness default instructs adding it. Removing it from history requires a force-push, which my standing contract forbids unconditionally. I left history alone, omitted it from the second commit and from the PR body, and am reporting the conflict rather than silently picking a side.", "tests": "All on branch head a2664404c (git rev-parse --short HEAD), which is the final commit -- nothing changed after these runs. BUILD FIRST: the stale packages/app-shell/dist still carried the OLD hand-written interfaces (dist/views/metadata-admin/SchemaForm.d.ts had 'export interface FormViewSpec'), and apps/console resolves @object-ui/app-shell through dist (its tsconfig has no path mappings), so nothing was judgeable before building. pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell...' build -- exit 0 in 3m41s; confirmed the new types REACHED dist (dist/index.d.ts exports FormFieldSpec, FormSectionSpec, FormViewSpec; dist/views/metadata-admin/form-spec.d.ts carries the Omit derivations; the old 'export interface FormViewSpec' count in SchemaForm.d.ts is now 0). TYPE-CHECK (this is what judges the pins, since type assertions are erased at runtime): pnpm --filter @object-ui/app-shell type-check -- exit 0, log echoes 'tsc --noEmit && tsc -p tsconfig.test.json' so the script really ran (not the zero-match pnpm trap); pnpm --filter @object-ui/console type-check -- exit 0. ABLATION / CONTROL PROBES -- each pin measured CAPABLE OF FAILING, both legs trap-restored, both mutations confirmed on disk by grep -cxF (pristine count asserted 1 BEFORE mutating, then old 1-to-0 and new 0-to-1) rather than by an editor exit code: (a) app-shell, dropped 'label' from SectionNarrowed -- type-check EXIT=2 with form-spec.containers.test.tsx(74,9) TS2322 and (75,5) 'error TS2344: Type false does not satisfy the constraint true'; (b) console, diverged buildSections' parameter from the shared type -- type-check EXIT=2 with FormPage.viewSpec.test.ts(78,9) TS2322 and (78,35) TS2344. Trap restored both; git status --porcelain empty afterwards. Note my FIRST attempt at the header edit was a silent NO-OP -- perl -0pi with backslash-Q..backslash-E quoted the newline as a literal backslash-n, zero matches, exit 0 -- caught by the before/after grep, redone with an anchor-asserting python replace. VITEST (all from repo ROOT): the two new pin files -- 'Test Files 2 passed (2) / Tests 5 passed (5)'; console FormPage suite -- 'Test Files 9 passed (9) / Tests 120 passed (120)'; app-shell SchemaForm+form-spec set (15 files) -- 'Test Files 15 passed (15) / Tests 166 passed (166)'. DECLARED NARROWING: I derived the app-shell superset as a 94-file reverse-import closure seeded at the four changed modules (computed mechanically over 920 src files; it correctly caught tests OUTSIDE metadata-admin that reach the changed modules through the package barrel, which a directory-scoped run would have missed). That run was KILLED at the 10-minute foreground cap (exit 143) under load average 10.87 from the concurrent devs, and wrote no verdict at all -- 3 log lines, zero failure markers, so it yielded NO evidence and I am not counting it. I narrowed BY DERIVATION, not by dropping files: I mechanically classified all 215 changed lines in packages/app-shell/src and every one is a comment, an import type/export type, or an interface/type declaration -- the only 6 residual lines are 'export type {}' members and '& Omit< ... >' continuations. TypeScript erases all of them, so app-shell's emitted runtime bytes are unchanged and tests reaching a changed module only through a barrel cannot observe this diff; the 15-file subject-scoped run is the remainder. CI runs the full farm regardless. GATES (exit code captured before any pipe; quoting each gate's OWN verdict line, never a bare $?): check:spec-symbols exit 0 -- 'spec symbol derivation: 1290 files scanned against 4912 spec export names' and 'spec alignment claims: 2 declared deliberate copies'; check:control-bytes exit 0 -- 'check-control-bytes: OK (scanned 4693 tracked text file(s); skipped 85 binary)'; check:phantom-deps exit 0 -- 'Every in-scope import is declared by the package that publishes it' (app-shell already declares @objectstack/spec ^17.0.0, so the new spec import is not phantom); check:published-dist exit 0 -- 'No published package's build output carries tooling material'; check:self-import exit 0; check:esm-specifiers exit 0; check-changeset-presence exit 0; check-changeset-no-major exit 0 -- 'No changeset declares a major bump'. ESLINT: --no-inline-config on all 7 changed source files -- exit 0, 0 errors, 35 warnings, file count 7 read from eslint's own --format json output (matching my 7 changed source files, so none was silently ignored), empty stderr. Narrowing justified as a measurement: the repo's eslint.config.js configures NO type-aware linting (no projectService, no parserOptions.project), so each file's verdict depends only on its own bytes plus the shared config and my diff cannot move any untouched file's verdict. Baseline control: linting the origin/main versions of the same two warning-carrying files gave 20+16=36 warnings vs my 19+16=35 -- my change REMOVED one warning (the deleted 'as any') and added none.", "contract_test_pins_both_halves": "YES, and on BOTH sides -- but the file the header named does NOT exist. The promised 'FormSpec.contract.test.ts' is nowhere in the repo (grep across all ts/tsx/md: the only hit was the header's own claim). The real pins are packages/app-shell/src/views/metadata-admin/form-spec.containers.test.tsx (8 pins, A-H) and apps/console/src/components/FormPage.viewSpec.test.ts (7 pins, A-G); the app-shell one is .tsx because it renders, so renaming it to the promised name would have been wrong -- I repointed the header instead. Half one (derived keys really are the spec's): app-shell PIN A/B assert Equal< Omit< FormSectionSpec, narrowed >, Omit< FormSection, narrowed > > and the same for FormView, so re-hand-writing either declaration fails the day the spec moves; console PIN A/B read both types back out of the EXPORTED buildSections signature rather than naming them, so a re-inlined local copy fails even if it agrees on every key the day it is written. Half two (each narrowing still refuses the arm it means to refuse): app-shell PIN G carries a @ts-expect-error per narrowed position -- label and description refuse the I18nLabel inline locale-map arm, visibleWhen refuses the ast-only ExpressionInput envelope -- and each @ts-expect-error is a two-way pin, since the directive itself becomes TS2578 if the line below it starts compiling, so a LOOSENING turns the file red too. PIN D pins fields to the converged 26-key leaf (keeping #5542 shut), PIN F/E keep excess-property checking live (no index signature or any smuggled in), PIN H pins that label is NOT a form-config key. Liveness controls are present and real: PIN C on both sides pins Equal< Equal< removedCopy, Shared >, false >, so the Equal helper is proven to still discriminate rather than answering true for everything. I did not take any of this on trust -- both PIN A's were measured capable of failing (see tests).", "hand_written_copy_surviving": "None of either container type survives in the two packages this card fenced. apps/console/src/components/FormPage.tsx no longer declares either -- both interfaces are deleted, FormViewSpec is imported from @object-ui/app-shell, FormSectionSpec is no longer referenced there at all (its renderer-honoured shape is the separate RenderableSection), and SubmitBehavior is now NonNullable< FormViewSpec['submitBehavior'] > instead of a hand-written four-member union. packages/app-shell's SchemaForm.tsx deleted both interfaces and re-exports from ./form-spec.js. A repo-wide grep for '(interface|type) (FormViewSpec|FormSectionSpec)' returns only: the single derived declaration in form-spec.ts; stale packages/*/dist/*.d.ts build artefacts (not source, and app-shell's were regenerated by my build); and ONE genuine survivor OUTSIDE this card's fence -- packages/react/src/spec-bridge/bridges/form-view.ts declares its own FormViewSpec (l.54), FormSection (l.34) and FormField (l.12) as a self-described mirror of FormViewSchema. It has ALREADY drifted on three keys this card measured: columns is number-only (l.42, l.57) though the spec admits the string arm, dependsOn is string[] only (l.28) though the bare-string arm is what makes field-selector work, and visibleWhen is string only (l.30, l.44) though the object arm is what evalFieldPredicate takes. I did NOT fix it -- it is a different package, outside the declared surface, and its declaration doubles as a retirement ledger (defaultSort/aria are omitted on purpose per #3901/#3974), so a blanket Omit derivation would erase that. Filed unassigned as #5652.", "open_questions": [ { "question": "My first commit's trailer contains a model identifier, which this dispatch bans in any pushed artefact, while the harness default instructs adding it. Removing it needs a force-push, which my standing contract bans unconditionally. Which rule yields?", "options": [ "A -- leave history as is; the identifier stays in one commit trailer on an unmerged draft branch, and a squash-merge drops the trailer anyway", "B -- PM (or the maintainer) authorises a one-off force-push on this branch to amend the trailer, since the remote carries only my two commits and no other agent shares the branch", "C -- treat the harness trailer default as overridden for this repo and write the ban into AGENTS.md, so the conflict stops recurring for every dev seat" ], "recommendation": "A now, C as the durable fix. A because the force-push ban protects shared state and I will not self-authorise an exception to it, and because the exposure is one trailer line on an unmerged draft that a squash-merge discards. C because this conflict is structural, not a mistake of mine -- every dev seat on this repo will hit it on its first commit, and a rule that lives only in a per-card dispatch brief cannot win against a harness default that fires automatically. B only if the maintainer wants the history literally clean before merge." } ], "out_of_scope_findings": [ "filed as #5652: a THIRD hand-written FormViewSchema mirror survives in packages/react/src/spec-bridge/bridges/form-view.ts, already drifted on columns (number-only), dependsOn (string[]-only) and visibleWhen (string-only) -- the same drift class one package further out, deliberately not fixed here" ] }Generated by Claude Code
os-sales commentedon Aug 22, 2026
ACCEPT — PR #5653, flipped ready and enqueued (SQUASH).
domain:uiseat PM, reviewer of record.No dev report arrived — the second dev on this card was also lost, this time after finishing. The worktree was removed cleanly and
git worktree listshows only the primary checkout, so unlike the round-10 loss nothing was left at risk: the work was complete on the remote. Per the #5636 precedent I treated the PR body as unverified and checked its claims against GitHub. #5652 is confirmed filed (finding,domain:ui). Gates read by name at heada2664404c: 19success+ 3 always-skipped= 22.Both judgement points answered
The changeset now exists — the wip commit carried none;
check-changeset-presenceand-no-majorare green.Both halves are pinned, on both sides, with liveness controls that matter more than the pins:
Equalhelper is proven to still discriminate;RenderableSectionis pinned not equal either;And the console pins read both types back out of the exported
buildSectionssignature rather than naming them — so a re-inlined local copy fails even if it agrees on every key the day it is written. That is a structurally stronger answer to "does a hand-written copy survive" than any grep I asked for.Each pin was then measured capable of failing, on its own mutation leg, both red with
TS2344: dropping'label'fromSectionNarrowed(mutation confirmed on disk bygrep -cxF, 1→0 / 0→1), and divergingbuildSections' parameter from the shared type.Clause ② — the conditional clearance holds
I cleared this card conditionally: derived shape only. It stayed derived —
Omit<…>narrowings of the spec's ownFormSection/FormView,check-spec-symbol-derivation.mjscited as the sanctioned form,check:spec-symbolsgreen. No hand-typed key with no authority to derive from, so it never crossed into #5611's territory.The drift was settled by the contract in both directions, which is what makes it a repair rather than a preference:
columns' string arm is legal, so app-shell's numbers-only copy was rejecting metadata the platform accepts;FormViewSpec.labelisunrecognized_keysagainst installed spec 17.0.0, so the console's copy was simply wrong — and the value that read actually finds is the view's identity label, now declared onFormViewBodynext to the envelope it unwraps rather than smuggled onto the form contract.Measurement discipline
The
distnote is the one I'd have flagged if it were missing: the stalediststill carried the old hand-written interfaces, so nothing could be judged beforepnpm --filter '@object-ui/app-shell...' build, and the derived types were confirmed present indist/index.d.tsafterwards. That is trap #5 caught before it produced a false reading rather than after.The declared narrowing is a genuine superset argument, not a sample: the 94-file reverse-import closure was killed by the 10-minute cap under load average 10.87 and produced no verdict, so it was narrowed by derivation — all 215 changed lines in
packages/app-shell/srcmechanically classified as comment,import type/export type, or type declaration, with the only 6 residual lines identified asexport type {}members and& Omit<…>continuations. TypeScript erases every one, so no app-shell runtime bytes change and a test reaching a changed module only through a barrel cannot observe this diff.Scope
packages/react's spec bridge holds a third hand-written mirror of this same contract, already drifted oncolumns,dependsOnandvisibleWhen. Outside the fence, filed as #5652, not fixed here — correct call. Escalating it to the seat post: this contract now has one converged pair and one known-drifted third copy, and #5652 is the card that closes the family.Generated by Claude Code
fix(console): honour the other three conditional-rule surfaces in For…
fix(console): evaluate visibleWhen/visibleOn in FormPage (objectstack…
fix(console): honour the other three conditional-rule surfaces in For…