Add staged optional-to-required SharedTree field migration API (sf.stagedRequired) - #27952
Add staged optional-to-required SharedTree field migration API (sf.stagedRequired)#27952Noah Encke (noencke) wants to merge 6 commits into
Conversation
|
Hi! Thank you for opening this PR. Want me to review it? Based on the diff (1081 lines, 20 files), I've queued these reviewers:
How this works
|
|
VERDICT: REQUEST_CHANGES Repository review skill: The design is genuinely good: the lazy read-time failure, the monotonic Automated checks (run from
Blocking issues:
Minimum diff to ship: rebase onto current Non-blocking observationsThese are informational and require no action before merge.
Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
15a2cc5 to
7136a32
Compare
Adds SchemaFactoryAlpha.stagedRequired / stagedRequiredRecursive, allowing an application to migrate a field from optional to required across a staged rollout without a coordinated deployment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7136a32 to
8f57978
Compare
Fix summary — rebased onto
|
| Command | Result |
|---|---|
npm run build (tree — 193 fluid-build tasks: tsc ESM+CJS, api-extractor, depcruise, eslint, biome) |
Build succeeded |
npm run build (packages/framework/fluid-framework) |
Build succeeded |
npm run test:mocha:esm -- --fgrep "staged" |
75 passing, 0 failing |
npm run test:mocha:esm -- --fgrep "toStoredSchema" |
90 passing, 0 failing |
npm run test:mocha:esm -- --fgrep "schema" |
725 passing, 3 pending, 1 failing (pre-existing, below) |
npm run format |
Checked 678 files. No fixes applied. |
git merge-tree --write-tree upstream/main HEAD |
exit 0 — no conflicts; GitHub now reports mergeable: MERGEABLE |
The single failure is snapshotCompatibilityChecker > write current view schema snapshot, unchanged from the review: a Windows-only path-separator bug in the test itself (literal / in the expected string vs. path.join in the code under test). Neither snapshotCompatibilityChecker.spec.ts nor snapshotCompatibilityChecker.ts appears in git diff --name-only upstream/main...HEAD, so it is not caused by this PR.
API reports were regenerated by the builds above, not hand-edited. The only delta beyond the previously reviewed additions is the one new optional member on StagedSchemaUpgradePolicy in tree.alpha.api.md and fluid-framework.alpha.api.md. Per .claude/skills/ci-readiness-check/tree-api-checks.md, I discarded the incremental-build flake that appeared in fluid-framework.legacy.public.api.md (spurious InteriorSequencePlace / SequencePlace / Side re-export lines, unrelated to this change).
Fixed by Minions (Ripley — Lead / Explorer · claude-opus-5)
Make stagedRequired use the looser (Optional) view field kind during the staged phase, mirroring stagedOptional, instead of a Required view kind with a read-time throw. Reads now return T | undefined honestly; writing or constructing undefined is rejected at runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review feedback addressed —
|
| Command | Result |
|---|---|
npm run build (tree, 211 tasks) |
Build succeeded (12m 06s) |
npm run build (packages/framework/fluid-framework) |
Build succeeded (6m 01s) |
npx mocha --config ./.mocharc.cjs "dist/test/**/*.spec.js" --fgrep "staged" |
150 passing, 0 failing |
npx mocha --config ./.mocharc.cjs "dist/test/**/*.spec.js" --fgrep "schema" |
1449 passing, 6 pending, 2 failing (pre-existing) |
npm run format |
Checked 678 files in 4s. No fixes applied. |
| GitHub merge status | mergeable: MERGEABLE |
The 2 failures are snapshotCompatibilityChecker > snapshotSchemaCompatibility > write current view schema snapshot (ESM and CJS copies of the same spec) — a Windows-only path-separator bug in the test's expected string (${testSrcPath}/schemaSnapshots/point vs. path.join). Messages are otherwise character-identical, and neither that spec nor its source is in git diff --name-only upstream/main...HEAD.
API reports were regenerated by the builds. Three union-reordering hunks in unrelated JsonAsTree/FluidSerializableAsTree entries — the documented incremental-TS/API-Extractor flake (.claude/skills/ci-readiness-check/tree-api-checks.md) — were discarded, so the committed report diff contains only the stagedRequired* signature change (FieldKind.Required → FieldKind.Optional) and the isRootPresent removal.
Fixed by Minions (Ripley — Lead / Explorer · claude-opus-5)
Review pass on
|
| claim | verified at | |
|---|---|---|
view field kind is Optional |
✅ | schemaFactoryAlpha.ts:432 — createFieldSchema(FieldKind.Optional, t, { …, stagedRequiredUpgrade: createSchemaUpgrade() }) |
stored kind tightens to Required only on explicit opt-in |
✅ | toStoredSchema.ts:478-484 — options.includeStagedRequired?.(isStagedRequired) === true ? FieldKind.Required : f.kind |
reads return undefined (no read-time throw) |
✅ | no throwStagedRequiredFieldMissing anywhere in src/ |
writes of undefined / delete throw at runtime |
✅ | objectNode.ts:417 (setField), objectNode.ts:826 (applyFieldChange) |
| construction without a value throws at runtime | ✅ | objectNode.ts:731 (objectToFlexContent) |
This is exactly the shape you described: looser (optional) read and write types, with the target state enforced by runtime UsageErrors rather than the type system — mirroring how stagedOptional handles the same TS mapped-type limitation. Note the guard at objectNode.ts:731 runs before the defaultProvider branch at :740, so a staged-required field can never silently fall back to an empty default.
One question from that thread is still open and I did not want it lost when the thread was resolved: I also removed TreeViewAlpha.isRootPresent(), since with reads now returning undefined the presence check is just view.root === undefined, and keeping it would add permanent alpha surface for a problem that no longer exists. It was added and removed within this PR, so it is net-zero against main — but if you would rather keep it as a general optional-root convenience, say so and I will restore it (one-line revert). TreeAlpha.child was pre-existing and is untouched.
2. 🔗 "Found some broken links!" bot comment — ❌ not caused by this PR
I treated this as a claim and checked it rather than chasing links. It does not hold up:
- The check itself passed on this exact head. Job
94579746426(🔗 Broken Link Check,head_sha: db18adef61…) reportsconclusion: successwith all 23 steps green, including step 8Check for broken links. - The comment names no broken link. Its entire payload is the server starting and then
[ELIFECYCLE] Command failed with exit code 1— a tooling/exit-code artifact, not a link report. - It is repo-wide. Of the currently open PRs that have a link report at all, 5 of 5 say BROKEN — build(eslint-config-fluid): consolidate custom rules into config package #27943 (eslint-config consolidation), feat(container-runtime): Default document schema to declare createBlobPayloadPending support at 2.40.0+ #27948 (container-runtime), refactor(tree): Reserve positional argument in
treeChangedevent listener signature #27951, Simplified summary process without summarizer nodes #27953 (summarization flow) and this one. Those PRs share no files with this one. - This PR touches zero docs/website files. All 17 changed files are under
.changeset/,packages/dds/tree/**, and two generatedapi-report/*.api.mdfiles.Build siteand🧪 Website Testsboth pass.
So this is pre-existing infrastructure noise in the link-check reporter, out of scope here. I did not change anything for it.
3. "Want me to review it?" reviewer-dispatch bot comment — non-actionable
Checkbox UI for dispatching the review fleet; nothing for the author to do.
Validation
No code changed in this pass, so there is no new behavior to cover and I did not re-run the suite. The authoritative evidence for the current head is CI on db18adef61 itself:
| Check | Result |
|---|---|
Build - client packages (Build Stage Build) |
pass (16m 10s) |
Build - client packages (Policy checks) |
pass |
repo-policy-check |
pass |
pnpm lint |
pass |
Build site / 🧪 Website Tests |
pass |
🔗 Broken Link Check |
pass |
| merge status | MERGEABLE |
Full ADO test legs (JestTest, RealsvcTinyliciousTest, StressTinyliciousTest, coverage) were still queued at the time of writing.
I attempted a corroborating local npm run build in packages/dds/tree; it failed with 'fluid-build' is not recognized because this is a fresh worktree with no node_modules (node_modules absent at both repo root and package level) — an environment gap, not a code failure. I deliberately did not install the full monorepo to re-prove a zero-line diff that CI already builds green. The targeted results quoted in the PR description (--fgrep "staged" → 150 passing, 0 failing) were produced on this same commit when it was authored.
The PR description already describes the optional-during-staging semantics and carries an accurate scope summary (17 changed files, verified via git diff --name-only c29323d9ca...HEAD) plus the review map, so it needed no edit either.
Fixed by Minions (Ripley — Lead / Explorer · claude-opus-5)
|
VERDICT: REQUEST_CHANGES Re-review of Repository review skill: Automated checks:
Blocking issues:
Minimum diff to ship: guard the staged-required emptiness check at the shared field-construction layer ( Non-blocking observations
Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
Enforce the staged-required invariant on the cursor and insertable construction paths, and correct two inaccurate TSDoc claims. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review fixes — staged required invariant now enforced on all construction pathsAddresses the three findings from the previous review at Blocking finding 1 — the "a staged client never creates an empty value" invariant was unenforced on the cursor / insertable construction pathsVerified as valid. Before this commit, the invariant was only enforced in
Fixes:
Behavioural consequence, now documented explicitly in the TSDoc and changeset: importing or cloning legacy content in which the field is empty throws rather than silently reintroducing an empty value. That is the intended trade-off — it fails loudly instead of quietly falsifying the precondition for the irreversible step-3 narrowing. Blocking finding 2 —
|
Review pass on
|
|
VERDICT: REQUEST_CHANGES Repository review skill: Base resolution: Automated checks:
Blocking issues:
Minimum diff to ship: fix the three Note on outstanding human feedback (not counted as my findings): two of Noah Encke (@noencke)'s inline threads are still unresolved — Non-blocking observations
Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
Shorten the staged-required empty-content error in TreeAlpha.importVerbose to match the sibling non-optional error message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review feedback addressed —
|
| command | result |
|---|---|
tsc -p tsconfig.json |
11 errors, all pre-existing (treeDataStore.ts, treeFactory.ts, arrayNode.ts), none in touched files |
tsc -p src/test/tsconfig.json |
31 errors in 7 unrelated files, all pre-existing; stagedSchemaUpgrade.spec.ts compiles clean |
biome check --formatter-enabled=true on both changed files |
clean |
direct check of emitted lib/shared-tree/treeAlpha.js |
message is exactly "undefined provided for staged required field.", the spec regex matches it, old string gone |
Environment limitation, stated plainly: mocha could not be run in this worktree. pnpm install is impossible here (the npm registry fails TLS handshake), so validation used the only available node_modules, whose sibling workspace lib/ artifacts are stale relative to this branch — module load fails on core-utils clamp, then shared-object-base defineDataStore, then runtime-utils DataStoreKindImplementation. Those are package-wiring staleness, not failures in the code under test; I rebuilt core-utils and shared-object-base from this worktree to push through two of them before stopping. The pre-existing tsc errors have the same root cause. The 22 tests in stagedSchemaUpgrade.spec.ts (incl. the assertion I updated) passed on the previous head and the only behavior change is the literal string, which I verified directly in the emitted JS. The PR pipeline covers the full suite.
No API report or changeset change needed — the diff is one internal error message plus its test assertion. Scope is unchanged at 20 changed files.
Fixed by Minions (Ripley — Lead / Explorer · claude-opus-5)
|
VERDICT: REQUEST_CHANGES Repository review skill: Base resolved to Automated checks:
Blocking issues:
Minimum diff to ship: let Otherwise this is well-built: the Non-blocking observations
Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
|
VERDICT: REQUEST_CHANGES Repository review skill: Automated checks:
Blocking issues:
Also outstanding, and not mine to close: Noah Encke (@noencke)'s unresolved thread on Minimum diff to ship: fix the three Non-blocking observations
Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
Review-feedback pass at
|
|
VERDICT: REQUEST_CHANGES Reviewed at head Repository review skill: Automated checks:
Blocking issues:
Minimum diff to ship: disambiguate the three Non-blocking observations
Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
Disambiguate {@link StagedSchemaUpgradePolicy} TSDoc references, which
API Extractor rejected with ae-unresolved-link because the name has both
an interface and a const declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build fix:
|
| Before | After |
|---|---|
{@link StagedSchemaUpgradePolicy} |
{@link (StagedSchemaUpgradePolicy:interface)} |
{@link StagedSchemaUpgradePolicy.includeStagedRequired} |
{@link (StagedSchemaUpgradePolicy:interface).includeStagedRequired} |
{@link StagedSchemaUpgradePolicy.enabledStagedUpgrades} |
{@link StagedSchemaUpgradePolicyFactory.enabledStagedUpgrades} |
The third link is retargeted to the factory interface because enabledStagedUpgrades is a member of StagedSchemaUpgradePolicyFactory, not of the StagedSchemaUpgradePolicy interface. This is the narrowest possible repair: it touches comment text only, changes no runtime code, no type signature, and no .api.md (Fluid API reports do not carry @link text — verified: zero @link occurrences under packages/dds/tree/api-report/).
What changed
packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts:264-266— disambiguated the three@linkreferences above.packages/dds/tree/src/simple-tree/api/stagedRequiredUpgrades.ts:78— same disambiguation applied to the identical latent ambiguity added by this PR, so it cannot surface once that symbol is exported.
Commit: 38f053df. Diff is 2 files, +4/−3, comments only.
Validation
A full local pnpm install is not possible in this environment: the package registry is unreachable from this machine (corepack fetching pnpm-11.15.1.tgz fails with ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE against registry.npmjs.org), and the worktree has no node_modules. So API Extractor could not be executed locally. Instead the repair was verified by two offline checks plus direct evidence from the failing CI run itself:
-
TSDoc syntax parse check — all three replacement forms parse cleanly under
@microsoft/tsdoc(the parser API Extractor uses):OK {@link (StagedSchemaUpgradePolicy:interface)} OK {@link (StagedSchemaUpgradePolicy:interface).includeStagedRequired} OK {@link StagedSchemaUpgradePolicyFactory.enabledStagedUpgrades} -
Resolution proven by the failing build itself — both replacement targets are already used and already resolve in the same API Extractor run that produced these errors:
toStored.ts:43uses{@link (StagedSchemaUpgradePolicy:interface)}andtoStored.ts:53uses{@link StagedSchemaUpgradePolicyFactory.enabledStagedUpgrades}, and neither was reported as unresolved in build 417433.includeStagedRequiredis a declared member of theStagedSchemaUpgradePolicyinterface (toStored.ts:39). -
No remaining ambiguity —
grep '\{@link StagedSchemaUpgradePolicy[.} |]'overpackages/dds/tree/srcnow returns zero matches.
The change is confined to comment text, so it cannot affect compilation, tests, or the generated API reports; the authoritative confirmation is the next CI run of Build - client packages.
Other checks
No other failing check on this PR is attributable to this diff. repo-policy-check, Policy checks, Website validation, Validate CODEOWNERS, and license/cla all passed on build 417433 / the corresponding GitHub Actions runs; Build - client packages was the only FAILURE, and its sole error class was the ae-unresolved-link set addressed above.
Scope
Scope: 20 changed files, one concern (the sf.stagedRequired staged optional-to-required migration API).
This PR is at the 20-file boundary and is deliberately atomic: the API surface (schemaFactoryAlpha.ts, stagedRequiredUpgrades.ts, toStored.ts, toStoredSchema.ts, configuration.ts, schematizingTreeView.ts, entrypoint/index re-exports) cannot be split from the schema-projection behavior it introduces without shipping an intermediate state in which a stagedRequired field is declarable but not correctly projected into stored schema — an unsafe, data-affecting half-state. The generated/mechanical portion of the diff is the api-report/*.api.md files and the index.ts / entrypoints/alpha.ts re-export lines; the tests (toStoredSchema.spec.ts, stagedSchemaUpgrade.spec.ts, testTrees.ts, and the call-site updates in existing specs) are required regression coverage and the mechanical fallout of toStoredSchema gaining a required options parameter. Read order: toStored.ts → toStoredSchema.ts → schemaFactoryAlpha.ts → stagedRequiredUpgrades.ts → tests.
Fixed by Minions (Dallas — Engineer · claude-opus-5)
|
VERDICT: REQUEST_CHANGES Repository review skill: Base: microsoft/FluidFramework:main at Automated checks:
Blocking issues:
Minimum diff to ship: make Non-blocking observationsThese are informational and do not require action before merge.
Verified clean (no findings): the write-enforcement guards are complete across the construction/mutation paths I and the Breaker traced — Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
- Make the staged-required tightening reachable through TreeView.upgradeSchema - Cover the end-to-end upgrade path in tests - Make the staged optional/required markers mutually exclusive Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review feedback addressed —
|
| Check | Result |
|---|---|
tsc --project ./tsconfig.json (packages/dds/tree) |
0 errors |
tsc --project ./src/test/tsconfig.json |
0 errors |
mocha lib/test/simple-tree/api/stagedSchemaUpgrade.spec.js |
24 passing, 0 failing |
mocha lib/test/simple-tree/** + lib/test/shared-tree/** |
4442 passing, 113 pending, 2 failing |
biome check on all changed source files |
clean |
api-extractor run --local (packages/dds/tree) |
regenerated tree.alpha.api.md |
The 2 failures were proven pre-existing by stashing this branch's changes, rebuilding, and re-running the same two specs on the unmodified tree — identical failures. They are environment artifacts, not regressions: snapshotCompatibilityChecker.spec compares Windows \ against / inside an asserted error message, and treeNodeApi.spec "rejects operations beyond the post-edit length" fails because assert short code 0xd19 is not mapped back to its message when the repo's assert-tagging build step is bypassed.
sharedTree.spec.js was excluded from the broad run because it requires a newer @fluidframework/local-driver than the shadowed checkout provides.
API reports. packages/dds/tree/api-report/tree.alpha.api.md was regenerated by api-extractor and contains exactly the 12 expected Omit<…> lines. packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md could not be regenerated here — flub generate entrypoints fails in this shadowed graph with No export declarations found for "defineDataStore", so lib/alpha.d.ts is never produced. The 12 corresponding lines in that rollup were byte-identical to tree's pre-change lines, so the exact same substitution api-extractor emitted for tree was applied. Please confirm CI's API-report check agrees; if it reports drift, the CI-generated version is authoritative.
Fixed by Minions (Ripley — Lead / Explorer · claude-opus-5)
|
🔗 Found some broken links! 💔 Run a link check locally to find them. See Checking for Broken Links for more information. linkcheck output |
|
VERDICT: APPROVE Re-review of Repository review skill: Base: Automated checks:
Blocking issues:
Verification of the three previously-blocking findings:
Also re-verified as behavior-preserving for existing consumers: with no staged-required field in play, Minimum diff to ship: n/a Non-blocking observationsThese are informational and do not require action before merge.
Verified clean (no findings): the write-enforcement guards remain complete across Review by Minions (Ripley — Lead / Explorer · claude-opus-5) |
Review pass on
|
Bundle size comparisonBase commit: Notable changes
Per-bundle deltas
|
Review pass on
|
| Category | Files | Added | Removed |
|---|---|---|---|
Shipped source (packages/dds/tree/src, non-test) |
16 | +514 | −20 |
Test (stagedSchemaUpgrade.spec.ts) |
1 | +448 | −2 |
| Generated API reports | 2 | +34 | −12 |
| Changeset | 1 | +51 | 0 |
Only the first row can reach a bundle. A net +494 lines of shipped TypeScript producing +2102 parsed bytes on sharedTree.js is ≈ 4.3 bytes per net-added line — on the lean side, which is what you'd expect here given how much of that 514 is JSDoc and type-only declarations (UpgradeSchemaProjection, the isStagedRequired accessors, the optional includeStagedRequired policy member) that are fully erased at runtime.
The largest delta lands where the new API is actually exposed. fluidFrameworkAllAlpha.js (+4004 parsed / +1045 gzip) is the bundle that includes the alpha surface, and sf.stagedRequired / stagedRequiredRecursive / computeUpgradeSchemas are alpha APIs. sharedTree.js (+2102) and fluidFramework.js (+2109) carry the same implementation. The remaining bundles move by 0–56 bytes, i.e. incidental. Nothing shows a new module being pulled into a bundle that shouldn't have it — stagedRequiredUpgrades.ts is imported only from the simple-tree API layer that already ships in these bundles.
Adding a new alpha feature has a non-zero code size cost; +490 gzip on sharedTree.js for a full staged migration API is a reasonable price and there is no accidental bloat to remove.
Re-confirming the earlier link-check item with stronger evidence
The 🔗 "Found some broken links!" comment (2026-08-14T20:20:38Z) was previously rebutted as a repo-wide/infra failure rather than a regression from this PR. That rebuttal now has direct confirmation it didn't have at the time: the 🔗 Broken Link Check check run on 616911f6df is success, as is 🧪 Website Tests. The sticky comment is a workflow_run reporter re-posting an artifact and is stale relative to the actual check result on this head. Still no change made, and this PR touches nothing under docs/.
Still-open thread (unchanged, deliberately left active)
simple-tree/api/create.ts:58 — "Should we be doing this validation in isFieldInSchema instead of outside?" remains active for Noah Encke (@noencke). I independently re-verified the standing rebuttal rather than taking it on faith, and it holds architecturally:
isFieldInSchema(feature-libraries/schemaChecker.ts) is typed(childNodes, schema: TreeFieldStoredSchema, schemaAndPolicy: SchemaAndPolicy, onError) => T | undefined. Its entire view of the field is the stored schema.- Staged-required-ness is carried only on the view schema:
getStagedRequiredUpgrade(simple-tree/fieldSchema.ts) narrows toFieldSchemaAlphaand readsisStagedRequired, i.e.propsAlpha?.stagedRequiredUpgrade ?? false. - By design the stored projection of a staged-required field is
Optionaluntil the step-3 opt-in — that is the whole point of the feature. So an emptiness check insideisFieldInSchemais not merely awkward to factor, it is not expressible: the stored schema it receives legitimately permits the empty field, and making it expressible would require handing view-schema knowledge to the stored-schema validator, inverting the layering.
I have deliberately not resolved that thread — the reviewer should get to overrule this.
Validation
No source files were modified in this dispatch, so no build or test run was performed and none is claimed. The branch is unchanged from the previously validated head:
$ git rev-parse HEAD
616911f6df5be58b85d14fb4fa077005f2cb231f
$ git status --porcelain
?? .minions-worktree # engine metadata, untracked, not part of the diff
The validation of record therefore remains the run reported for 616911f6df, plus the all-green check runs on that exact SHA cited above.
Scope: 20 changed files, one concern (staged optional-to-required field migration). Nothing was added by this dispatch.
Fixed by Minions (Rebecca — Architect · claude-opus-5)
Scenario
Applications that shipped a field as
sf.optional(T)often later discover the field is conceptually mandatory, but they cannot simply switch tosf.required(T): doing so is a schema narrowing change, so old clients that are still running (and documents they created with the field empty) would immediately break. Today the only options are a coordinated flag-day deployment or leaving the field permanently optional and hand-checking forundefinedeverywhere.This adds an alpha SharedTree schema API for a staged optional-to-required field migration, so an application can stop writing empty values first, and tighten the stored schema later once old clients are gone — the same rollout shape as the existing
SchemaFactoryAlpha.stagedOptionalandSchemaStaticsBeta.staged, but for the opposite direction.Rollout contract
sf.optional(T).sf.stagedRequired(T)(stagedRequiredRecursivefor recursive schema). The stored schema staysOptional, so version N clients are unaffected and their documents remain viewable. Because such a document may contain a node where the field is empty, the field is alsoOptionalin the view schema during this phase — reads are typedT | undefined. What this step changes is that a version N+1 client never creates an empty value: constructing a node without a value, assigning/insertingundefined, anddeleteall throw aUsageErrorat runtime.TreeView.upgradeSchematreats this staged change as a no-op.StagedSchemaUpgradePolicy.includeStagedRequired, also reachable throughextractPersistedSchema), which tightens the stored field kind fromOptionaltoRequired. Note that this member is optional on the@inputStagedSchemaUpgradePolicyinterface and defaults to never applying the tightening, so existing policy implementations keep compiling and keep their current behavior.sf.required(T)and drops the staged marker. Only at this point does the field become non-optional in the TypeScript types.The upgrade is monotonic: the new
computeUpgradeSchemas(viewSchema, stored, stagedSchemaUpgrades?)preserves staged-required fields that the stored schema has already tightened, so a staged client cannot propose an "upgrade" that reverts the tightening back toOptional. It never applies a staged tightening on its own, since that is a narrowing change that must always be opted into explicitly. It composes with the caller'sStagedSchemaUpgradePolicy, and is used by bothcheckSchemaCompatibilityandTreeView.upgradeSchema.Because step 3 is the one upgrade that legitimately narrows the stored schema,
computeUpgradeSchemasreturns two projections of the same view schema, differing only in staged-required field kinds:wideningOnly— only the staged-required upgrades already applied in the stored schema.TreeView.upgradeSchemarequires this to be a superset of the stored schema, which preserves the "upgrades never narrow" guarantee for everything else.target— additionally the ones explicitly opted into. This is what is actually written, using the pre-existingTreeCheckout.updateSchema(schema, allowNonSupersetSchema)escape hatch when it is not a superset.checkSchemaCompatibilityderivescanUpgradefromwideningOnlyandisEquivalentfromtarget. When nothing is opted in the two projections are the same object, so behavior is unchanged.TreeView.upgradeSchemadoes not scan the document: nothing verifies that every node already has a value for the field. Ensuring that is the application's responsibility, and this is called out in the API docs and the changeset.Why the view field is Optional during the staged phase
This mirrors how
stagedOptionalandSchemaStaticsBeta.stagedalready work: during the staged phase the view schema describes the looser of the two states. A document created by a version N client may legitimately have the field empty, soT | undefinedis the only read type that honestly describes every document a version N+1 client can open. Typing the read as non-undefinedand throwing on read would make the TypeScript type a lie.The target state is enforced on the write side instead:
undefined, anddelete, throw aUsageError(objectNode.tssetField/applyFieldChange, and the root setter viasetField).UsageError(objectNode.tsobjectToFlexContent).UsageError, soTreeAlpha.create,TreeAlpha.importVerbose,TreeAlpha.importCompressed,TreeBeta.cloneandTreeView.initializecannot reintroduce an empty value (simple-tree/api/create.ts,simple-tree/unhydratedFlexTreeFromInsertable.ts,shared-tree/treeAlpha.ts).These are runtime rather than compile-time errors because a TypeScript mapped type cannot make the write type of a property required while its read type is optional — the same limitation
stagedOptionalworks around.Nothing is scanned or materialized when opening a document or creating a view, no value is synthesized, and nothing is written during reads. For object fields,
TreeAlpha.child(node, key)remains available as a proxy-bypassing presence check.Compatibility caveat
Enabling step 3 assumes version N clients have been phased out; it is an operational precondition, not something the API can enforce. A
stagedRequiredclient refuses to clear the field itself (assignment ofundefinedanddeleteboth throw), which reduces the remaining race to concurrent clients two rollout generations behind. This is deliberately not a claim of absolute safety against arbitrarily old concurrent clients, and that caveat is stated in the API docs and the changeset.Compatibility behavior outside this explicit staged case is unchanged: the discrepancy tolerance in
discrepancies.tsis scoped to exactly a staged-required view field (Optional) over an already-tightenedRequiredstored field, so schema narrowing is still not generally permitted.Validation
Rebased onto
main(c29323d9ca). All commands run frompackages/dds/treeunless noted. The table below was produced atdb18adef61(the view-kind redesign); the write-enforcement follow-updcc52965d5is validated separately beneath it.npm run build(tree — 211 fluid-build tasks: tsc ESM+CJS, api-extractor, depcruise, eslint, biome)npm run build(packages/framework/fluid-framework)npx mocha --config ./.mocharc.cjs "dist/test/**/*.spec.js" --fgrep "staged"npx mocha --config ./.mocharc.cjs "dist/test/**/*.spec.js" --fgrep "schema"npm run formatChecked 678 files in 4s. No fixes applied.mergeable: MERGEABLEPre-existing / environmental failures, not caused by this change:
snapshotCompatibilityChecker > snapshotSchemaCompatibility > write current view schema snapshot(both the ESM and CJS copies of the same spec, hence 2) — a Windows-only path-separator bug in the test itself: the expected string is built with a literal/(${testSrcPath}/schemaSnapshots/point) while the code under test usespath.join, which yields\on Windows. The two messages are otherwise character-identical. NeithersnapshotCompatibilityChecker.spec.tsnorsnapshotCompatibilityChecker.tsis touched by this PR (git diff --name-only upstream/main...HEADconfirms).Head is now
dcc52965d5, which added the cursor / insertable construction-path enforcement and two new test cases. That commit could not be validated withnpm run build: the environment has no npm registry access, so it was validated against locally built dependencies instead —stagedSchemaUpgrade.spec.js22 passing, 0 failing, and the fullsimple-treesuite 3191 passing, 34 pending, 1 failing (the same Windows path-separator artifact described above). A later independent re-verification ofdcc52965d5type-checked the package again and found only the 11 pre-existing errors inarrayNode.ts/treeDataStore.ts/treeFactory.ts— none of those files are touched by this PR, and all 11 come from stale sibling-package build output in that environment. CI remains authoritative for a clean-install build.API reports were regenerated by the builds above. Three union-reordering hunks in unrelated
JsonAsTree/FluidSerializableAsTreeentries (the documented incremental-TS/API-Extractor flake, see.claude/skills/ci-readiness-check/tree-api-checks.md) were discarded, so the committed report diff contains only thestagedRequired*signature changes (FieldKind.Required→FieldKind.Optional) and theisRootPresentremoval.Head is now
616911f6df, which addressed review feedback: it made step 3 actually reachable throughTreeView.upgradeSchema, added end-to-end coverage of that path, and made thestagedOptional/stagedRequiredmarkers mutually exclusive in both the props types and at runtime. Validated in the same registry-less environment against locally rebuilt dependencies:tscon the package and the test project 0 errors,stagedSchemaUpgrade.spec.js24 passing, 0 failing, the combinedsimple-tree+shared-treesuites 4442 passing, 113 pending, 2 failing — both failures reproduced identically on the unmodified tree after stashing this branch's changes, so neither is a regression (the Windows path-separator artifact described above, plus an assert short-code mapping artifact from bypassing the repo's assert-tagging build step).packages/dds/tree/api-report/tree.alpha.api.mdwas regenerated by API Extractor;fluid-framework.alpha.api.mdcould not be regenerated locally (flub generate entrypointsfails in that environment), so the identical substitution API Extractor emitted for tree was applied to its twelve byte-identical lines. CI is authoritative for both reports.Test coverage (
src/test/simple-tree/api/stagedSchemaUpgrade.spec.ts, mirroring the existingstaged optional upgradesuite):Optionalbefore opt-in andRequiredafter.undefined(present root reads normally); an absent object field reads asundefinedwhile sibling fields stay usable, andTreeAlpha.childagrees.undefinedwrites anddeleteon both root and object fields; repair by assigning a real value.undefined, throws aUsageErrorat runtime.stagedRequiredRecursivein a recursive schema.TreeView.upgradeSchema— no-op at phase N+1, real tightening at step 3, version N clients losingcanView, equivalence at step 4, and a non-opted-in staged client not reverting the tightening.canUpgrade === false, andupgradeSchema()throws).Scope
Scope: 20 changed files, one concern.
Atomicity justification: this is a single API addition that must be threaded through one code path end-to-end — the view field schema, its stored-schema projection, compatibility/discrepancy reporting, the upgrade-schema computation, and the write enforcement points. Splitting it would produce intermediate states where, for example,
stagedRequiredexists in the type system but projects to aRequiredstored field (silently breaking old clients) or where the upgrade path reverts a tightening — i.e. unsafe and misleading rather than merely incomplete. Dropping the tests, changeset, or API reports to shrink the count is not an option under repo policy.Review map (read in this order):
simple-tree/api/schemaFactoryAlpha.ts(the new API + docs),simple-tree/fieldSchema.ts(staged marker),simple-tree/toStoredSchema.ts(stored-kind projection),simple-tree/api/stagedRequiredUpgrades.ts(new — monotonic upgrade computation),simple-tree/api/discrepancies.ts(scoped compatibility tolerance),simple-tree/node-kinds/object/objectNode.ts(blocked clears + construction guard)shared-tree/schematizingTreeView.ts, and the three construction-path enforcement pointssimple-tree/api/create.ts,simple-tree/unhydratedFlexTreeFromInsertable.tsandshared-tree/treeAlpha.ts.simple-tree/simpleSchema.ts,simple-tree/core/toStored.ts(the new optional policy member),simple-tree/api/schemaCompatibilityTester.ts,simple-tree/api/storedSchema.ts.simple-tree/index.ts,simple-tree/api/index.ts.stagedSchemaUpgrade.spec.tscarries the new coverage.api-report/*.alpha.api.mdfiles (regenerated by API Extractor, never hand-edited) and the changeset.Repo harnesses used
No visual/UI surface is affected by this change, so no screenshots are included.
Authored with Minions.