Skip to content

fix(plugin-timeline,plugin-calendar,plugin-kanban): a failed load no longer keeps a data view on its error screen after a later load succeeds (objectui#10663) - #10680

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10663-view-error-clears
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10663-view-error-clears

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10663

Clause-②: no

A data view that fails one load no longer stays on its error screen after a later load succeeds. No export, schema, prop or accept set moves. Dispatched by the domain:ui seat 5, session https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy.

What changed

The card's census found three data views with the objectui#10578 defect. Each set error in its fetch catch, cleared it nowhere, and returned the error screen early whenever error was set. Since objectui#10623 (timeline) and objectui#10572 (calendar, kanban), each also re-reads on every data-invalidation event, so one failed background re-read was enough to keep a healthy view on its error screen until a remount.

Each now takes the objectui#10578 shape: the current run clears error when it commits rows. The clear is not made when a run starts, so the report stays until rows land.

  • ObjectTimeline (@object-ui/plugin-timeline): the fetch effect had no run guard. A fetchSeqRef run sequence now scopes both error writes to the current run: the clear on commit, and the failure write in the catch. A superseded run can no longer clear the current run's error, or raise the error screen over the current run's rows. The rows write itself is untouched (see Acceptance notes).
  • ObjectCalendar (@object-ui/plugin-calendar): the clear sits on each commit branch of the fetch effect (inline provider, object, api), inside the run's existing isMounted guard. It also sits in the host-data effect, beside the row-ceiling reset that is already there for the same reason: the handed-over rows are not the query that failed.
  • ObjectKanban (@object-ui/plugin-kanban): the clear sits on the fetch commit, inside the run's existing isMounted guard. Host, bound and inline cards are read straight from props, so there is no other commit site.

Does a failed background re-read keep the last good rows? No, for all three. None has a silent mode (ObjectGantt and ObjectMap do). The failure is reported, the rows stay in state but are not drawn, and the next re-read that succeeds takes the screen back. Each pin file pins this.

Files: packages/plugin-timeline/src/ObjectTimeline.tsx (+24/-2), packages/plugin-calendar/src/ObjectCalendar.tsx (+21/-1), packages/plugin-kanban/src/ObjectKanban.tsx (+11/-0). Three new pin files beside them. Three patch changesets, one per package.

The boundary with this seat's parallel objectui#10664 holds: no changed line in the three sources touches an effect's dependency list, a useEffect( or a useCallback( (grep over the diff: 0 hits). The removed lines are the timeline's react import line, its bare catch write and the calendar's one-line api commit.

Census: every data view under packages/plugin-*/src and packages/components/src/renderers that keeps an error state from a fetch

The population was enumerated from two sides: every set…Error( writer, and every useState error slot or object-shaped error write. Each writer was read. A third side, every file that calls find( or aggregate( (67 files), confirms that the views with no error state have no error screen to stick.

View Reading on base Here
ObjectTimeline never clears fixed
ObjectCalendar never clears fixed
ObjectKanban never clears fixed
ObjectGantt clears on commit (objectui#10578) control
ObjectMap clears on commit (PR objectui#10649) control
ObjectGrid, ListView (loadError), ObjectTree, ObjectChart clear at run start clear already
ObjectDataTable, ObjectMetricWidget, ObjectPivotTable clear at run start clear already
DatasetWidget, DatasetReportRenderer whole-state replace at run start clear already
data-list, record-picker, the elements aggregate clear at run start clear already
LineItemsPanel (plugin-form) a successful load never clears (a save start does); a banner, not an early return not touched: the file is under the domain:ui#4 seat's live claim, PR objectui#10650. Probe reproduced; reported to the seat

Outside the data-view population, listed so the census is complete:

  • Record forms: ObjectForm, DrawerForm, ModalForm, SplitForm, TabbedForm, WizardForm never clear error either. They are record-page renderers, not data views, by the product's own split in anchors.ts ("List / Interface page = a data view ... Record page renders one object record."). Each also runs two fetches (the object schema and the record) with no run guard, so "clear on a successful commit" has no single home: a record read that succeeds over a stale schema would clear a schema failure. A probe reproduced the defect on ObjectForm through its objectui#10572 bus re-read. Reported to the seat, not changed here.
  • react-page's loadError guards a dynamic import of the code runtime, not a data fetch.
  • The plugin-chatbot hooks, Mermaid, ImportWizard and the designer metadata pages are not data views, and each clears anyway.
  • saveError, submitError, addError, exportError and mutationError are write errors, not load errors.

Pins: red on base, green on head

Each pin file mounts its view through the real SchemaRenderer and its package's own registration, and holds every find open by hand. The second load is a data-invalidation re-read (notifyDataChanged).

Base leg: the three sources were swapped to their base blobs (4758b33) on the committed head 4f1d532, and the three pin files ran with the two controls. Result: Tests 8 failed | 21 passed (29), with both controls green. The restore was proven by blob equals HEAD for each file and an empty git diff HEAD. Head leg, same five files: Tests 29 passed (29). The six pin and source blobs are identical at the final head 5abe386.

Pin timeline calendar kanban
fail, then a bus re-read succeeds: error screen gone, rows drawn RED / green RED / green RED / green
a failed BACKGROUND re-read over good rows is reported; the next success takes the screen back RED / green RED / green RED / green
control: fail, then fail again shows the newer failure green / green green / green green / green
a SUPERSEDED run that succeeds does not clear the current error green / green (A1) green / green (A3) green / green (A4)
a SUPERSEDED run that fails does not raise the error over current rows RED / green (A2)
host data after a failed own fetch clears it RED / green

Controls: ObjectGantt.errorClears-10578.test.tsx and ObjectMap.busReread-10623.test.tsx were green on base and on head in both legs.

Ablations

The superseded-success pins cannot fail on base, because base never clears. Their red legs are ablations on the committed head, each through the objectstack ablation-replace.mjs in WRAP mode. The anchor had to hit once, and each landing is proven by an anchor count of 1 to 0 and a blob change. Each restore is proven by blob equals HEAD and an empty git diff HEAD.

  • A1, the timeline's commit clear moved outside isCurrent(): 1 failed | 4 passed, the superseded-success pin.
  • A2, the timeline's failure write moved outside isCurrent(): 1 failed | 4 passed, the superseded-failure pin.
  • A3, the calendar's object-arm clear moved outside isMounted: 1 failed | 4 passed, the superseded-success pin.
  • A4, the kanban's clear moved outside isMounted: 1 failed | 3 passed, the superseded-success pin.

Local verification (HEAD 5abe386, after merging origin/main at 7baede3)

  • Dependency closure first: pnpm turbo run build with --filter='PKG^...' for each of the three packages, --concurrency=2: Tasks: 13 successful, 13 total.
  • pnpm exec vitest run packages/plugin-timeline/ packages/plugin-calendar/ packages/plugin-kanban/ plus the two control files, at 5abe386: Test Files 143 passed (143), Tests 1109 passed (1109). The same three suites alone at 2d03929: Test Files 141 passed (141), Tests 1094 passed (1094).
  • pnpm --filter @object-ui/PKG type-check for each of the three packages, at 2d03929: exit 0, with the script name echoed. The last merge (2d03929 to 5abe386) brought one apps/console test and one changeset, outside all three packages' dependency closures. Each new pin file is in its package's tsconfig.test.json program (--listFilesOnly: 1 hit each).
  • Gates at 5abe386, each exit 0: check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:shell-escape-residue, check:unreferenced-sources, check:i18n-keys, type-check:coverage, lint:coverage, check:pending-changeset-literals, check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed.
    • check:new-line-citations: "VERDICT new-cross-file-line-citations: 0 new citation(s)".
    • check-changeset-presence: "6 source file(s) of 3 released package(s) changed, and this change declares 3 changeset(s)".
    • check-governed-queue-guard --test over the nine paths: NOT GOVERNED.
    • check:changeset-claims (report-only): details under Acceptance notes.
  • NOT MEASURED: check:sdui-registration-pins, reason: it exits 2 with "No console build to weigh at apps/console/dist/assets" (a missing prerequisite, not a verdict). This diff touches no registration and no sideEffects field; CI builds the console and runs it.
  • Lint was narrowed to the six changed files. eslint.config.js lints **/*.{ts,tsx} and declares no parserOptions.project or projectService, so no type-aware rule runs and this diff cannot move any untouched file's verdict. The --format json output counted 6 files, 0 errors. The source files carry 26, 30 and 30 warnings on head, the same as their base blobs. The pin files carry 6, 9 and 6 warnings, the same any pattern the sibling pins use.
  • Docs and READMEs: none describes these views' error lifecycle, so no doc gate is reached.

Acceptance notes

  • Pre-existing, reported to the seat and not changed here (rows lifecycle, not error): the timeline's rows write is unguarded. A superseded answer that lands late overwrites the current run's rows. Probe: currentShown=false supersededShown=true. Its finally also releases loading for any run. The same race on ObjectMap was repaired in PR objectui#10649 (its case B). This PR's guard covers the two error writes only; the rows write is outside this card's ruled scope.
  • Consistency across the family: ObjectGantt and ObjectMap keep the last good rows on a failed background re-read. The timeline, calendar and kanban report it. This PR states and pins the second behaviour and does not add a silent mode.
  • The timeline and kanban read host data, bound rows and authored items straight from props and commit nothing for them. If a view flips from fetching for itself to host rows after a failure, the error stays until the next successful fetch of its own. The calendar commits host rows to state, so it clears there.
  • The calendar re-enters its loading placeholder on every re-read, because its fetch always calls setLoading(true) and loading is checked before error. That is pre-existing, and this PR does not change it.
  • check:changeset-claims lists 12 pending changesets that name ObjectCalendar.tsx or ObjectKanban.tsx. None of them describes the error lifecycle (grep: the only hit is a @ts-expect-error), so all still hold.
  • LineItemsPanel and the six record forms are reported to the seat with probe readings, as above.

Generated by Claude Code

…ws (objectui#10663)

ObjectTimeline set `error` only in its fetch catch and cleared it nowhere, so
one failed load kept the error screen up after later loads succeeded. Since
objectui#10623 a failed data-invalidation re-read was enough to get there.

The current run of the fetch effect now clears `error` when it commits rows,
and only the current run writes `error` at all (a run sequence, the
objectui#10578 shape). A failed background re-read is still reported: this
block has no silent mode.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…run commits rows (objectui#10663)

ObjectCalendar and ObjectKanban set `error` only in their fetch catch and
cleared it nowhere, the same defect objectui#10578 fixed on ObjectGantt. Since
objectui#10572 a failed data-invalidation re-read was enough to keep either
block on its error screen until a remount.

Each now clears `error` when the current run commits rows, inside the fetch
effect's existing `isMounted` guard. The calendar also clears it where host
`data` is committed, beside the ceiling reset that sits there for the same
reason. A failed background re-read is still reported: neither block has a
silent mode.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…ars (objectui#10663)

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 12 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7313-object-calendar-record-source.md

  • names plugin-calendar/src/ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    ObjectCalendar resolves its records through the shared ladder (resolveRecordSourceConfig in @object-ui/core, called from plugin-calendar/src/ObjectCalendar.tsx): data first, then staticData, then objectName. The published TypeScript interface REQUIRED objectName and declared neither data nor staticData; the published Zod mirror did the same. So an object-calendar node authored on staticData — the route the plugin page documents twice — rendered correctly and was refused by safeValidateSchema, and could not be annotated with its own type (TS2741: Property 'objectName' is missing).

.changeset/7322-object-kanban-component-props.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    Inside ObjectKanban.tsx, three schema-key reads drop their as any: titleField (two sites, now honest because the object-kanban arm declares it) and cardFields / cardTitle (already declared; the casts were redundant). (schema as any).navigation stays — navigation is declared on neither face, so removing the cast would change nothing but the spelling of an index-signature read.

.changeset/7322-object-kanban-group-by-limit.md

  • names packages/plugin-kanban/src/ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    What was measured, on this branch's base (53ded82b). packages/plugin-kanban/src/ObjectKanban.tsx reads schema.groupBy at thirteen sites (lane materialisation at :601 / :625 / :640, card moves at :747 / :865, and their effect deps) and schema.limit at two (:264, $top: schema.limit ?? DEFAULT_KANBAN_LIMIT, and the effect deps at :291). groupField has ZERO read sites anywhere under packages/plugin-kanban/ — against a control of those thirteen groupBy reads in the same query, so the zero is a reading, not a blind grep. Yet the declaration REQUIRED groupField and declared neither groupBy nor limit. Measured from source: the documented, tested, working shape — { type: 'object-kanban', objectName, groupBy, limit } — failed ObjectKanbanSchema.safeParse and safeValidateSchema on the missing groupField, and only ever reached the renderer through BaseSchema's [key: string]: any and .passthrough(), admitted unexamined. An author who followed the declaration wrote groupField and got a board that grouped nothing, with no diagnostic on either face.

.changeset/7712-kanban-calendar-filter-input.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    ObjectKanban.tsx sends the authored key to the query as $filter: schema.filter and ObjectCalendar.tsx does the same, and @objectstack/spec's ComponentPropsMap declares filter on both blocks (measured: safeParse accepts it, and refuses an undeclared key by name on the same call). But none of the four registrations that publish those two renderers listed filter in inputs, and sdui-parser's validateTree reports unknown-prop for every key no inputs entry claims. So an author writing the one spelling that WORKS was told it was unknown — objectui#6678's shape, where a correct write draws the same diagnostic as a write that does nothing. That is worse than an inert key: it actively punishes the correct behaviour, and the honest response to it is to delete working metadata.

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    ObjectKanban.tsx sends the authored key to the query as $filter: schema.filter and ObjectCalendar.tsx does the same, and @objectstack/spec's ComponentPropsMap declares filter on both blocks (measured: safeParse accepts it, and refuses an undeclared key by name on the same call). But none of the four registrations that publish those two renderers listed filter in inputs, and sdui-parser's validateTree reports unknown-prop for every key no inputs entry claims. So an author writing the one spelling that WORKS was told it was unknown — objectui#6678's shape, where a correct write draws the same diagnostic as a write that does nothing. That is worse than an inert key: it actively punishes the correct behaviour, and the honest response to it is to delete working metadata.

.changeset/7772-page-block-kanban-group-by-control.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    What was measured, on this branch's base (0c8dbc49). BLOCK_CONFIG['object-kanban'] offered exactly four controls — objectName / groupField / titleField / cardFields. ObjectKanbanSchema.groupBy is REQUIRED on both published faces (objectql.ts:2765 groupBy: string, no ?; objectql.zod.ts:1001 groupBy: z.string(), no .optional()) and had no control at all, while groupField — the only control able to set grouping — has been a retirementTombstone() on the Zod face and ?: never on the TS face since objectui#7322. ObjectKanban.tsx reads schema.groupBy at thirteen sites and groupField at zero (control: those same thirteen hits in the one query, so the zero is a reading).

.changeset/7773-kanban-adapter-groupfield-write.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    The object-kanban renderer never read it: groupField has ZERO hits anywhere under packages/plugin-kanban/, against a control of thirteen schema.groupBy read sites in ObjectKanban.tsx from the same query — so the zero is a reading, not a blind grep. The write was inert; the board grouped by groupBy and groupField rode along unread.

.changeset/7780-object-kanban-record-source.md

  • names packages/plugin-kanban/src/ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    packages/plugin-kanban/src/ObjectKanban.tsx resolves a board's rows in four steps: the pre-fetched data PROP a parent passes (hasExternalData), then bind through useDataScope(schema.bind), then the inline ROW ARRAY on schema.data, and only then a fetch keyed by schema.objectName — rawData = (hasExternalData ? externalData : undefined) || boundData || schema.data || fetchedData, with the fetch itself gated on schema.objectName && !boundData && !schema.data. Every objectName read is guarded. Both published faces nevertheless REQUIRED objectName, so a bind-only or data-only board — one that renders correctly today — was refused by the shipped validator and could not be annotated with its own type.

.changeset/8171-calendar-sort-input.md

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    objectui#7712's defect, one key over. ObjectCalendar.tsx lowers the authored key onto its own query as $orderby: convertSortToQueryParams(schema.sort), and @objectstack/spec's ComponentPropsMap['object-calendar'] declares sort (measured on 17.2.0: safeParse({ objectName, sort }) returns success: true, while the same strict schema on the same call refuses bogusProp by name — that control is what makes the acceptance a verdict). But neither of the two registrations that publish this renderer — plugin-calendar:object-calendar and view:calendar — listed sort in inputs, and sdui-parser's validateTree reports unknown-prop for every key no inputs entry claims. So an author writing the one spelling that WORKS was told it was unknown — objectui#6678's shape, where a correct write draws the same diagnostic as a write that does nothing.

.changeset/8174-kanban-calendar-filter-sort.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    filter had four declaration faces and only three of them named it: @objectstack/spec declares it (ComponentPropsMap['object-kanban'] and ['object-calendar']), both plugins' registration inputs publish it, and both renderers read it — ObjectKanban.tsx lowers schema.filter onto $filter, ObjectCalendar.tsx lowers schema.filter onto $filter and schema.sort onto $orderby through convertSortToQueryParams. This package's own published faces (the TypeScript interface and its zod mirror) named none of them, so an authored value reached the renderer only through BaseSchema's index signature and the mirror's .passthrough() — admitted, never examined. That is the same reasoning finding(types,plugin-kanban): ObjectKanbanSchema requires groupField (zero read sites) and declares neither groupBy nor limit — no working object-kanban node is assignable to any declared type #7322 used to move groupBy and limit into this same interface.

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    filter had four declaration faces and only three of them named it: @objectstack/spec declares it (ComponentPropsMap['object-kanban'] and ['object-calendar']), both plugins' registration inputs publish it, and both renderers read it — ObjectKanban.tsx lowers schema.filter onto $filter, ObjectCalendar.tsx lowers schema.filter onto $filter and schema.sort onto $orderby through convertSortToQueryParams. This package's own published faces (the TypeScript interface and its zod mirror) named none of them, so an authored value reached the renderer only through BaseSchema's index signature and the mirror's .passthrough() — admitted, never examined. That is the same reasoning finding(types,plugin-kanban): ObjectKanbanSchema requires groupField (zero read sites) and declares neither groupBy nor limit — no working object-kanban node is assignable to any declared type #7322 used to move groupBy and limit into this same interface.

.changeset/8466-calendar-color-allday-fields.md

  • names ObjectCalendar.tsx → packages/plugin-calendar/src/ObjectCalendar.tsx — edited by this change

    ObjectCalendar.tsx's getCalendarConfig reads FIVE flat keys off the node, and packages/plugin-calendar/README.md teaches all five in one sentence — "point titleField / startDateField / endDateField / allDayField / colorField at your own fields when they differ." Only three of the five were declared. The other two reached the renderer through BaseSchema's [key: string]: any on the TypeScript face and its .passthrough() on the zod mirror: admitted, never examined. A misspelling therefore left the calendar silently colourless while every published gate passed.

.changeset/8827-kanban-empty-state-settled.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    A genuinely empty board still paints DataEmptyState. The reverse regression — "gating on a truthy definition would leave those boards empty forever", named in ObjectKanban.tsx itself — is what the settle contract exists to prevent, and every exit settles: the query succeeding, the query throwing, no readable source, the non-fetch record sources (external, bound and inline data are settled from the first frame), and the schema-only kanban-ui entry, which has no ObjectKanban and therefore no provider and so takes the context's settled default. Lanes, headers, counts and drop targets keep rendering while the rows are in flight; only the claim is withheld.

.changeset/8990-object-kanban-groupby-optional.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    What a lane-less board does, measured rather than assumed. Every schema.groupBy read in ObjectKanban.tsx is a guarded early-return, so the board degrades instead of breaking: with no lane key and no columns it renders an empty board; with bare-string columns it draws those lanes, titled by the raw strings; card moves are inert (persistCardMove and the move callback both open if (!groupBy) return). ⚠️ Every lane-less board holds zero cards — bucketCardsIntoColumns returns before distributing records when there is no lane key — so omitting groupBy is not a way to configure a board, it is a board that groups by nothing.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 7baede374 (merge-base with origin/main): 6 file(s) changed outside .changeset/, read against 1516 pending declaration(s) that publish a body (2102 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3055.8 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-Dpijlqwt.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 547.22KB 130.96KB
core (index.js) 9.52KB 3.79KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.19KB 65.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.52KB 14.64KB
plugin-charts (index.js) 76.80KB 21.35KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.85KB 61.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 152.55KB 39.16KB
plugin-gantt (index.js) 169.71KB 41.93KB
plugin-grid (index.js) 216.65KB 59.28KB
plugin-kanban (index.js) 48.36KB 15.09KB
plugin-list (index.js) 115.77KB 28.75KB
plugin-map (index.js) 23.01KB 7.60KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.84KB 9.01KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.77KB 21.99KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5abe38602784ce92ad49c94eccf86cd79653724b

Read: card objectui#10663 (body, triage 5835290143, claim 5835697745; the os-dev-report comment was not used as evidence), prior card objectui#10578 and its PR objectui#10630 (the shape), card objectui#10664 (the parallel claim), the PR body and file list, the check-runs at the head, refs/review/pr-10680 diffed against its merge-base with refs/review/main, AGENTS.md 版本号策略 and .changeset/config.json. Merge-base is 7baede3745904d929d4afd80bcd5e2314b466072, which is also the tip of main at review time. Nine files: three sources, three new pin files, three changesets. No spec symbol from @objectstack/spec is consumed by the diff.

① Derived judgments

The triage's binding text (5835290143): 「first, measure: one failure, then a success, on a mounted timeline. Then census the other data views for the same never-cleared error, since this card covers the family, and give each the objectui#10578 shape (clear on a successful commit; state whether a failed background re-read keeps the last good rows). One enumeration pin per view found.」 The claim scopes the file surface to each view's error lifecycle only and hands fetch-effect dependency lists to objectui#10664.

1. ObjectTimeline — the objectui#10578 shape, right. packages/plugin-timeline/src/ObjectTimeline.tsx. On base the file has exactly one setError( (the catch) and zero setError(null); the render returns the error screen early at :841, ahead of the loading skeleton at :849. At the head: :265 adds const fetchSeqRef = useRef(0); :363 takes seq = ++fetchSeqRef.current at the top of every effect run and :364 defines isCurrent() as a comparison at write time; :440 if (isCurrent()) setError(null) sits AFTER setFetchedData(data) at :431, so the clear happens when the current run commits rows and never at run start; :447 if (isCurrent()) setError(e as Error) scopes the failure write. Judged right: this is the rule PR objectui#10630 set on ObjectGantt (clear on commit, inside the current-run guard, not at start).

2. The run-sequence guard cannot let a superseded run clear or set the error. Effect runs execute synchronously in commit order, so seq is strictly increasing; every newer run bumps the ref, so any older run's isCurrent() is false from then on. Both error writes call isCurrent() immediately before the write with no await between, so the check cannot go stale. A run that returns early (!objectDefReady at :468, or the else branch at :470) still takes a number, which is the right reading: a view that has switched to host or bound rows must not receive an older fetch's error. Nothing reads the ref except the two writes.

3. The rows' lifecycle is unchanged (separate finding, as the brief asked). :431 setFetchedData(data) and :449 finally { setLoading(false) } are untouched and unguarded: a superseded answer still overwrites the current rows and still releases loading. The PR body's Acceptance notes state this pre-existing race truthfully and keep it outside the card's ruled scope. Right to leave it, since the claim says error lifecycle only.

4. ObjectCalendar — clear on every commit branch, inside the run's own guard, right. packages/plugin-calendar/src/ObjectCalendar.tsx. Base: one catch write, zero setError(null), error screen at if (error) after the if (loading) return. Head: :683 (inline value provider commit), :771 (object arm commit), :777 (api arm, commits []) each add setError(null) inside the existing if (isMounted) block; let isMounted = true at :617 is per run and the cleanup at :792 flips it, so a superseded run's clear is discarded with its rows. :573 adds setError(null) in the host-data effect beside the existing setRowCeiling(null): rows a parent hands over are a commit, matching ObjectGantt's host-data clear at its :846. The api arm clear is a commit of an empty set on a stub provider: consistent with the shape, harmless. No clear at run start (the setLoading(true) at the top of fetchData gains nothing).

5. ObjectKanban — clear on the one commit site, right. packages/plugin-kanban/src/ObjectKanban.tsx. Base: one catch write at if (isMounted) setError(e as Error), zero clears, error return at if (error) before the board. Head :723 adds setError(null) after setFetchedData(data) (:708) and setFetchWindowSaturated inside the same if (isMounted); :727 catch write unchanged. Host, bound and inline cards come straight from props through the rawData chain (:755), so there is no other commit site to clear on; verified.

6. "Does a failed background re-read keep the last good rows?" stated and true. For all three the catch has no silent branch, the error return precedes the rows, and the next successful commit clears it. Contrast verified at source: ObjectGantt :954-956 and ObjectMap :992-995 log a background failure and keep the rows. The PR and changesets say "No, for all three", which is what the code does. The triage asked only that this be stated; adding a silent mode was not asked and was not done.

7. Census population and completeness, judged independently. I re-derived the census on refs/review/main from three sides: every set…Error( writer, every error-shaped useState, and every .find(/.aggregate( caller under packages/plugin-*/src and packages/components/src/renderers.

  • Never clears on base: ObjectTimeline, ObjectCalendar, ObjectKanban (fixed here), LineItemsPanel (packages/plugin-form/src/LineItemsPanel.tsx :335 load write, :373 clear only at save start, :441 an inline banner, not an early return) and the six record forms (ObjectForm :697/:807, DrawerForm :276/:344, ModalForm :354/:424, SplitForm :183/:245, TabbedForm :293/:348, WizardForm :505/:535, each two catch writes, zero clears, each an if (error) return). The PR names every one of these. I found no data view the census missed.
  • Clear at run start, verified at source: ObjectGrid :1991, ListView :2302 (loadError), ObjectTree :692, ObjectChart :772, ObjectDataTable :672, ObjectMetricWidget :387, ObjectPivotTable :196, data-list :136, record-picker :131, elements :407; DatasetWidget :615 and DatasetReportRenderer :403 replace their whole state with status: 'loading'. Clear on commit: ObjectGantt :846/:950, ObjectMap :839. Log only, no error state: ObjectGallery :514, ObjectView :1264. record-reference-rail replaces each entry's state at fetch start (:380-384) so its per-entry error cannot outlive a success. react-page :160-162 guards a dynamic import(), not a data fetch. saveError, submitError, addError, exportError, mutationError are write errors. All as the PR table says.
  • LineItemsPanel exclusion: right under the claim. The file is in open draft PR objectui#10650 (claude/issue-10631-master-detail-save-in-flight, the domain:ui#4 seat), verified from its file list. The claim says 「Stop on breach, or on a found file under another seat's live claim, and explain in the report」; the PR explains it and reports the probe. The triage's "one enumeration pin per view found" is therefore not met for this one view, by the claim's own stop rule, not by omission. Residue for the family card: a sub-issue is recommended by the PR, not filed by it.
  • Record forms exclusion: defensible, with residue. The triage's population is "the other data views"; anchors.ts :175 draws the product's own line ("List / Interface page = a data view … Record page renders one object record"). The forms are record-page renderers with two independent fetches (schema and record) and no run guard, so "clear on a successful commit" has no single home there, as the PR says. The family defect is real on all six (verified) and stays open until a sub-issue exists.

8. Nothing asked is missing; nothing done that was not asked. The measurement is the first pin case of the timeline file (red on base by construction, see 10). The useRef import at :9 is required by the ref. The comments added are explanatory only. No file outside the three views, three pins and three changesets moved.

9. Existing pins edited or deleted: none. The file list holds three added test files and no modified test. Nothing was weakened. The two controls the PR cites (ObjectGantt.errorClears-10578.test.tsx, ObjectMap.busReread-10623.test.tsx) exist on main and are untouched.

10. Can the new pins fail? Yes; each has a lit control and a negative leg. All three files mount through the real SchemaRenderer and the package's own registration (import './index'), hold every find open by hand, and re-read through notifyDataChanged.

  • Timeline (ObjectTimeline.errorClears-10663.test.tsx, 5 cases): case 1 asserts the error is lit after the first failure (:133) BEFORE asserting the clear (:139); on base setError(null) does not exist, so :139 fails: a negative leg by construction. Case 2 (background re-read) likewise fails on base at :162. Case 5 (a superseded failure over current rows) fails on base because the unguarded catch sets the error: negative leg. Case 3 (fail, then fail again shows the newer failure) is the lit control that would go red on a clear-in-finally regression. Case 4 (superseded success does not clear) is green on base by construction; its red leg can only be an ablation of the isCurrent() guard, which the PR states honestly. timeline-error (:843) and timeline-canvas (:881) test ids exist at the head.
  • Calendar (5 cases): cases 1, 2 red on base for the same reason; case 5 (host data after a failed own fetch) red on base because the host-data effect did not clear. The Error: {{message}} read matches the en locale key calendar.loadError (packages/i18n/src/locales/en.ts :740); rendering ObjectCalendar directly without a provider is the pattern five existing calendar pins already use.
  • Kanban (4 cases): cases 1, 2 red on base; control case 3; the : message$ regex and Error loading kanban data prefix match the render at :1484-1487; ./KanbanImpl is the same specifier index.tsx :221 lazy-loads.
  • Base-leg arithmetic checks: 3 + 3 + 2 = 8 red cases on base, as the PR reports (8 failed | 21 passed (29)); I did not run the suites (read-only review), CI carries the head leg.

11. objectui#10664 boundary holds. Grep over the three sources' diff for useEffect(, useCallback( and any dependency-array line: 0 hits. The three removed lines are the timeline react import, its bare catch write and the calendar's one-line api commit; the timeline effect's dependency list at :475 is byte-identical to base.

② Semver level

  • Changesets: .changeset/10663-timeline-error-clears.md ('@object-ui/plugin-timeline': patch), 10663-calendar-error-clears.md ('@object-ui/plugin-calendar': patch), 10663-kanban-error-clears.md ('@object-ui/plugin-kanban': patch). All three packages are in the fixed group of .changeset/config.json. AGENTS.md 版本号策略: never major; minor/patch evolve on their own. A behaviour fix that moves no export, prop or schema is patch. Level right. Not a docs-only change, so no empty-frontmatter changeset is owed. CI Changeset Declaration, Changeset Bump Policy, Changeset Overwrite Report, Changeset Fixed Group Check all success at the head.
  • Changeset prose, sentence by sentence against the diff:
    • Timeline: "set error when its fetch failed, and the render returns the error screen early whenever error is set" true (:841 precedes every other return). "Nothing ever cleared it" true (0 setError(null) on base). "Every later load that succeeded still wrote its rows, but the timeline stayed on the error screen until it remounted" true. "Since objectui#10623 the timeline re-reads on every data-invalidation event" true (invalidationNonce in the deps at :475; PR objectui#10649 merged as 02e6d36, an ancestor of main). "The current run of the fetch now clears the error when it commits rows" true (:440 after :431). "This is the rule objectui#10578 set for ObjectGantt" true (PR objectui#10630). "The error is not cleared when a run starts" true. "Only the current run writes the error at all. A run that a newer one has superseded can no longer clear the current run's error, or put the error screen over the current run's rows" true (:440, :447). "A failed background re-read is still reported… no silent mode… the next re-read that succeeds takes the screen back" true.
    • Calendar: "the render returns the error screen early whenever error is set": true in substance; strictly the loading placeholder at :1142 precedes the error return at :1152 while a re-read is in flight, which the PR body's Acceptance notes state. "Nothing ever cleared it" true. "Since objectui#10572 the calendar re-reads on every data-invalidation event" true (:583 comment, invalidationNonce in the deps). "clears the error when it commits rows, on each of its commit branches" true (:683, :771, :777). "The clear sits inside the run's existing isMounted guard, so a superseded run cannot clear the current run's error" true. "not cleared when a run starts" true. "Rows a parent hands over through data clear it too, beside the row-ceiling reset" true (:570-573). Silent-mode sentence true.
    • Kanban: "set error when its fetch failed, and the render returns the error screen early whenever error is set" true (:1484, no if (loading) return in the file). "Nothing ever cleared it" true. "Since objectui#10572 the board re-reads on every data-invalidation event" true (:564 comment, deps). "clears the error when it commits cards" true (:723). "inside the run's existing isMounted guard" true. "not cleared when a run starts" true. Silent-mode sentence true.
  • No docs or README prose is added by the diff. The PR body's census table and the "Files" line (+24/-2, +21/-1, +11/-0) match the REST file list; the "removed lines" sentence and the "grep over the diff: 0 hits" sentence are both true.

③ Boundary flags

  • PR body flags, answered. (a) "the timeline's rows write is unguarded… its finally also releases loading for any run": true at :431 and :449, pre-existing, rightly outside the card. (b) "ObjectGantt and ObjectMap keep the last good rows on a failed background re-read; the timeline, calendar and kanban report it": true at source for all five. (c) "If a view flips from fetching for itself to host rows after a failure, the error stays until the next successful fetch of its own" (timeline, kanban): true reading of the rawData chains; the calendar clears on host rows. (d) "the calendar re-enters its loading placeholder on every re-read": true (setLoading(true) at the top of fetchData, :1142 before :1152), pre-existing. (e) check:changeset-claims report: not re-run here; no pending changeset in the diff touches those files. (f) LineItemsPanel and the six record forms reported, not changed: see ①-7. The PR states no open questions.
  • CI at the head 5abe386, read three times (17:07Z, 17:15Z, 17:16Z): 42 check-runs; at the last read 35 success, 3 skipped (Test (coverage), Test (coverage shard), dependabot: by design), 4 in_progress (Test (shard 1/8), Test (shard 4/8), Test (shard 5/8), Spec Main Shape Gate), 0 failure, 0 cancelled. Commit status: success. Lint, Type Check, Build & E2E, Test (dist pins), Line Citation Gate, Control Byte Scan and shards 2, 3, 6, 7, 8 are green. The seat should confirm the three test shards and the shape gate settled green before ready.
  • Draft: yes (draft: true), as a dispatched PR must be.
  • Clause-②: no holds. No export, prop, schema, i18n key or accept set moves: the diff adds one private useRef, six setError(null) writes and one guard on an existing write. mergeable: true.
  • Main under the PR's files: merge-base 7baede3 equals the current main tip, so main has not moved since the merge-base; the PR merged origin/main twice (2d03929, 5abe386).
  • Overlap on GitHub. Every open PR's file list was read: none touches ObjectTimeline.tsx, ObjectCalendar.tsx or ObjectKanban.tsx. objectui#10650 touches LineItemsPanel.tsx (the reason it was left). objectui#8941 (the lucide-react bump) touches packages/plugin-calendar/package.json and packages/plugin-kanban/package.json only, no source overlap. The parallel objectui#10664 has no PR yet; its remote branch claude/issue-10664-fetch-deps-match-query (at 46c5432) changes ObjectMap.tsx, ObjectDataTable.tsx, data-list.tsx, record-picker.tsx, DashboardFilterBar.tsx, ObjectView.tsx and their pins, none of this PR's files. No overlap today.
  • Serial constraint: the triage's 「Serial after objectui#10623 (PR objectui#10649, open)」 is discharged; 02e6d36 is an ancestor of main.
  • Residue for the family closure card: LineItemsPanel and the six record forms still never clear their error; the PR recommends sub-issues and files none (a read-only review cannot either). The seat should file them before closing objectui#10663 as the family card, or the closure is partial.

Implemented-by: claude/issue-10663-view-error-clears
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 17:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit d435e96 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10663-view-error-clears branch September 25, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants