Repository navigation
fix(runtime)!: the /i18n dispatcher domain refuses an anonymous caller, with the console pin moved past the sign-in companion (#22432) - #22496
Conversation
… first handleI18nRequest opens with the shared anonymous-deny floor (shouldDenyAnonymous, ADR-0056 D2) ahead of the provider probe and every route, in the hoisted form the analytics and security domains use. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
The anonymous-deny-i18n row covers the domain's gate pin and its dispatcher-domain key, which leaves the shrink-only population baseline (31 to 30). The probe census and the matrix header are re-derived on the merged ref, the booted showcase proof drives every mounted face, the system-context page anchors the new elevation read, and the changeset declares the narrowing. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Six runtime suites drove the /i18n domain with no identity, as the smallest context that compiled. The domain stands on the anonymous-deny floor now, so they carry a session (or the dispatcher's resolution seam is stubbed, as the analytics cases already do); only identity moves and every expectation is unchanged. The multi-tenant suite's probe comment no longer calls the domain anonymous-reachable. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…gn-in translations companion) Written by the pin-update procedure, no hand edits: scripts/bump-objectui.sh 47b1f0bb7174 --no-commit (the pin and the @objectstack/console changeset), pnpm objectui:build, then node scripts/gen-sdui-manifest-node.mjs (the manifest record; the manifest bytes did not move) and pnpm gen:sdui-lockstep against the pinned objectui worktree (the lockstep record). objectui@47b1f0bb71748a7d16f36edecc50059367d2e35a Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bbdfbc77b2abc5309ab5c33b1c3c9344bf0298b8 && git checkout bbdfbc77b2abc5309ab5c33b1c3c9344bf0298b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f782f1764410dcf7ac80108c3526eab4043032d7 a252adcef18e6e99d9b51f13af2d746bf6de6d07 && git checkout -B drift-repro f782f1764410dcf7ac80108c3526eab4043032d7 && git merge --no-ff a252adcef18e6e99d9b51f13af2d746bf6de6d07
node scripts/docs-audit/affected-docs.mjs --json f782f1764410dcf7ac80108c3526eab4043032d7
|
…8n-anonymous-deny
Every record that asserted the old pin f0268ad78 is re-read at the new pin by the gate's procedure: the cited objectui file read at 47b1f0bb7, its line numbers re-derived there, and the anchor and the sha moved together. Nine records cite ObjectGrid.tsx or ListView.tsx, whose cited lines moved with their text byte-identical; the other 34 cite only files byte-identical across the hop. The six migration entries' corpus counts are re-taken at the new pin; registry.ts is regenerated by gen:migration-registry. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…ibe's new pin sha Written by pnpm --filter @objectstack/spec check:generated --fix, which regenerated only the one artifact it proved stale. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs, and nothing else: card #22432 (its body and all ten comments, ① Derived judgments
② Semver level
③ Boundary flagsRound-1
Round-2
PR body, Acceptance notes:
Cross-lane on the card: Implemented-by: VERDICT: PASS Generated by Claude Code |
system-context.mdx: both sides' prose kept (main's row 9b from #22513, this branch's row-51 anchor packages/runtime/src/domains/i18n.ts#handleI18nRequest); counts re-derived by pnpm gen:system-context-census. registry.ts: re-derived by pnpm --filter @objectstack/spec gen:migration-registry (byte-identical to the text merge). Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…e merged tree Main's 4e9fe9f (PROTOCOL_VERSION 17 -> 18, #22215) began rendering the 18.* semantic entries into both artifacts. Six of them carry this branch's re-measured objectui pin readings, so the merged tree's copies were stale. Generators only: pnpm --filter @objectstack/spec gen:spec-changes and gen:upgrade-guide. No source edit. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…8n-anonymous-deny
…e merged tree (main f782f17) The os-regen text merge kept this branch's stale copy of main's flow-value-slot-template-dialect-refused replacement text (list[0]); main's source entry reads items[0] since 40a6ee5 (#22524). Generators only: pnpm --filter @objectstack/spec gen:spec-changes and gen:upgrade-guide. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22432
Clause-②: no (narrowing)
What changed
packages/runtime/src/domains/i18n.ts#handleI18nRequestnow opens with the shared anonymous-deny decision,shouldDenyAnonymousfrom@objectstack/core(ADR-0056 D2). It is the handler's first statement, in the hoisted form the analytics and security domains use:401whether or not a provider is installed, never the501an empty slot answers.401, never the400the face answers.401 UNAUTHENTICATEDand serves nothing of the bundle. A signed-in caller, an API-key caller and an internal system context are served exactly as before. A CORS preflight is unchanged.Clause-②: no. No second gate at the dispatcher mount:dispatcher-plugin.tsis untouched.The console pin moves past the objectui companion in the same PR, through the repo's bump procedure, so the refusal never ships on a Console that reads the domain before sign-in.
This executes ruling
6074960686on #22146, item 1, verbatim:Readings (class level: faces, caller classes, statuses and codes)
A real boot of this branch's build:
pnpm dev -- --fresh(the showcase, withI18nServicePluginauto-registered), at5b97d08ae. The faces are the locale list, the translation bundle and the field labels.UNAUTHENTICATED, dispatcher-wrapper envelope, nodataThe anonymous body is the dispatcher's wrapped envelope, so the gated dispatcher handler is the one answering on this composition.
I18nServicePlugin's own mounts of the same paths register later, atkernel:ready, and the first registration wins. The "before" column comes from the ablation below: on the booted showcase, with the gate removed, each anonymous face answered 200.The Console at the new pin, in a browser (triage's pin)
Chromium (
/opt/pw-browsers/chromium, Playwright 1.63.0) against the same boot, served the bumped Console (/_console/, built from objectui47b1f0bb7174bypnpm objectui:build). Each run signs in through the Console's own sign-in form, then opens the showcase app and its task list. It was run twice, once inenand once inzh-CN./api/v1/i18nrequests signed outenzh-CNzh-CNthe task list shows the showcase's own translated object, column and status labels after sign-in. The built-in packs carry no showcase labels, so the post-sign-in load is what put them there.UNAUTHENTICATEDwith nodata.The pin bump (H2)
Range
f0268ad78485..47b1f0bb7174: 14 objectui commits, the companion (objectui#12034, PR objectui#12042) is its tip. Written by the procedure, with no hand edits:scripts/bump-objectui.sh 47b1f0bb7174 --no-commit:.objectui-shaand.changeset/console-47b1f0bb7174.md. The range walked completely: 13 releasing changesets, 0 breaking, so@objectstack/consoletakespatch.pnpm objectui:build: the Console built at the pin against this tree. The single-zod canary and the "carries THIS tree's spec" check passed. No objectui commit in the range needed a framework change that is not onmain.node scripts/gen-sdui-manifest-node.mjs:scripts/sdui-manifest.record.jsonre-recorded at the pin.sdui.manifest.jsondid not move by a byte (107 components, same sha256).pnpm gen:sdui-lockstep, withOBJECTUI_ROOTset to the pinned build worktree (its HEAD is the pin):packages/sdui-parser/objectui-lockstep.jsonre-recorded.check:sdui-lockstepthen read the two parser copies byte-identical over 214 grammar lines and agreed on all 25 codes and the containment predicate.check:sdui-manifest,check:sdui-lockstepandcheck:console-shaare green at the new pin. The one red ischeck:objectui-pin-citations, the gap at the top.The pin citations, re-measured at
47b1f0bb7174The gate's own procedure, record by record. Each cited objectui file was read at the new pin in a dedicated objectui worktree (the shared checkout's HEAD was not moved), its line numbers re-derived there, and the anchor and the sha moved together. The hop
f0268ad78485..47b1f0bb7174touches 129 paths over 14 commits and deletes or renames none. Two files a record cites changed on it, each for objectui#11865 (a picker drawn with the sharedSelect):ObjectGrid.tsx, +19/-11: one import line, and the grouped pager's rows-per-page picker redrawn;ListView.tsx, +85/-26: its "Color by field" and rows-per-page pickers redrawn.Every cited line in those two files moved with its text byte-identical. Every other cited file is byte-identical across the hop.
src/ui/component.zod.tskeyboardNavigationreadObjectGrid.tsx:5519→:5520(two records); the empty-state message6529-6530→6537-6538and its draw6546-6547→6554-6555;inlineEditable1824→1825; thecase 'tree'armListView.tsx:3853-3872→3913-3932,:3867→:3927src/ui/component.test.tssrc/kernel/functional-completeness.tscase 'calendar'/'gantt'/'timeline'/'map'arms, each byte-identical and moved by 60; the renderers,resolveTimelineDateBindingandresolveListMapConfigunmovedsrc/data/api-methods-batch-conformance.test.tsObjectGrid.tsx:4867-4894→4868-4895, its hash-object stillc88443302d40…src/ui/view.zod.tsFormField.spanrecord, and its describe, whose sha alone changes)src/ui/dataset.zod.ts,src/ui/action.zod.ts,src/ui/action-outcome-messages.test.tssrc/migrations/entries/semantic/18.*-unit-in-key.tssrc/migrations/registry.tspnpm --filter @objectstack/spec gen:migration-registrycontent/docs/references/ui/view.mdxpnpm --filter @objectstack/spec check:generated --fix, which regenerated only the one artifact it proved stale (check:docs)keyboardNavigationmethod still finds 15 hit lines against 3 for the control. The kanban counts read 2 / 2 / 11, and objectui still registers nokanban-uiblock. TheElementDataSourceGateshells read 0 / 3 / 3 / 3 / 4.git grep -o -F(the method reproduces everyf0268ad78485number).objectstack17956 → 17980,@objectstack/spec7522 → 7523,useState2622 → 2630,timeout1658 → 1674,window4430 → 4449,metrics404 → 455.period,interval,TTL,tenant,RuntimeConfig,resourceLimits,SpanandSpanSchemaare unchanged.--verify-anchorsat47b1f0bb7: exit 0. "55 asserting objectui pin citation(s) match .objectui-sha (47b1f0bb7), 144 historical citation(s) recorded and not checked, across 1932 spec source(s). 13 anchor content assertion(s) verified against objectui at 47b1f0bb7; 502 file:line anchor(s) seen.".changeset/objectui-pin-citations-47b1f0bb7174.md:@objectstack/specpatch, in the precedent's shape.describetext (beyond thespandescribe's sha) or behaviour changes.Pins
packages/runtime/src/domains/i18n-anonymous-deny.test.ts(52 cases).code,statusand the message, and that no bundle key and nodataride on the body.showcase-anonymous-deny-surfaces.dogfood.test.tsgains the three mounted faces. Each is driven anonymously (401, dispatcher-wrapper family, no bundle key) and by a signed-in member (200, the showcase's ownzh-CNbundle and labels). Each anonymous body is classified into exactly one envelope family. The file claims the new matrix row.Translation-flip sweep
Every pin and prose line that held "the domain answers without a session", all in this PR:
AUTHED_CALLER, aSIGNED_INcontext, or the dispatcher's resolution seam stubbed the way the analytics cases already do. Only identity moved, and every expectation is unchanged. The suites:http-dispatcher(20 calls),domain-handler-registry(2),http-dispatcher.multi-tenant-concurrency(1),i18n-success-envelope.conformance(6),i18n-supported-locales(2) anderror-envelope.conformance(1).http-dispatcher.multi-tenant-concurrency.test.tscalled the domain "anonymous-reachable (noshouldDenyAnonymousgate)". Each tenant kernel now carries a taggedauthslot that signs the request in. The probe is still the i18n read behind the floor, and the suite still parks request A inside its own identity resolution.code, nothing served.content/docs/permissions/system-context.mdx, row 51 (the anonymous-deny seams on the domain dispatchers) now nameshandleI18nRequest. The page's census counts were regenerated bypnpm gen:system-context-census(the new floor readsisSystem).content/docs/**,skills/**,@objectstack/client, the route ledgers (the domain's rows aresdk, notpublic), and the CLI.http-conformanceauthenticates every request with a stub session, so its 501 probe is unchanged.Ledgers (H4)
enforcedrowanonymous-deny-i18n. It covers a GATE_PIN key on the domain'sshouldDenyAnonymouscall (i18n:domains/i18n.ts:anonymous-gate) and the dispatcher-domain keydispatcher-domain:route-ledger.ts:/i18n, the pairing the analytics row makes. Its cited proof is the booted file above.authz-ledger-population.baseline.ts: the/i18nkey leaves. MAX goes from 31 to 30, with a dated note. The population pin lists it as classified.deriveProbeFileCensus()on the merged ref:PROBE_TABLEmoves from 19 / 14 / 18 to 20 / 15 / 20, andMATRIX_HEADER_PROBE_CLAIMfrom 19 to 20. A newPROBE_FILE_CENSUSrow coversdomains/i18n.ts: gate pin, population 1, blind spot 0, controlsshouldDenyAnonymous(1 andhandleI18nRequest3.handleI18nRequestcounted as a population of 0 under a reach of 1. No other row's count moves (re-derived).route-ledger.ts) row note now reads 21 domains, 7 classified, 14 baselined, and 9 domain files no probe names. It had read 5 / 16 and 11 files since before/analyticswas classified.covers(10 rows, 20 keys); 38 of 47enforcedrows are in-resolver; 7 of the 20 keys are gate pins.Reverse verification (from the committed fix)
The mutation replaced the gate's condition with a never-true comparison against a planted literal, through
scripts/ablation-replace.mjs(anchor hit x1, blob changed), inside a script carrying its own EXIT/INT/TERM restore.authz-conformance.test.tsandauthz-probe-blind-spot.test.tswent 7 failed / 85 passed, namingSTALE covers … i18n:domains/i18n.ts:anonymous-gate,DEAD PROBE … domains/i18n.ts, and the census control.@objectstack/runtimewas rebuilt, andablation-dist-preflightfound the marker in 2 built files.showcase-anonymous-deny-surfaceswent 7 failed / 69 passed: the 3 anonymous faces (each readexpected 200 to be 401), their 3 envelope classifications and the shared code-and-message case. All 3 member controls stayed green.git diff HEADempty. Rebuilt; the marker is absent from all 6 built files, and the tree is clean.Tests and gates
@objectstack/runtimeat5b97d08ae: the full suite gave 349 files, 5681 passed, 19 skipped.typecheckexit 0:tsc --noEmitpluscheck:test-typecheck, whose program holds 7 of the 7 touched test files (counted with--listFiles).@objectstack/dogfoodat5b97d08ae:typecheckexit 0, with 6 of the 6 touched files in the program. The WHOLE suite, through the verify lock: 236 files (234 passed, 1 skipped, 1 failed), 1872 tests (1861 passed, 9 skipped, 2 failed), exit 1. The 2 failures are both inexternal-import-destructive-remedy.dogfood.test.ts, eachTest timed out in 5000ms, while unlocked gate scripts ran beside it on the shared box. That file never touches the/i18ndomain. Re-run alone through the lock at the same head: 4 of 4 passed, exit 0.pnpm lint(the full run) exit 0 at5b97d08ae.node scripts/pm/dispatch-gates.mjs --commandsderives 107 commands at5b97d08ae(the dispatch's 82 plus the docs and scripts families this diff reaches). All 107 ran at5b97d08ae, each exit code captured before any pipe. 106 exited 0. The one red ischeck:objectui-pin-citations(exit 1), the gap at the top.--ranreconciles 107 derived, 107 run, 0 NOT-MEASURED (a derived zero: every line carries its exit code).Round 2, at
e7c00d422, after mergingmain(e148ca984) in, without conflicts or a generated-artifact deferral:dispatch-gates --commandsderives 128 commands; all 128 exit 0,check:objectui-pin-citationsincluded.--ranreconciles 128 derived, 128 run, 0 NOT-MEASURED. Two of them,check:console-shaandcheck:console-injection, skipped for want of a Console dist in the fresh worktree: NOT MEASURED locally this round. Both were measured green in round 1 at the same pin, and CI'sConsole Pin Gateis green one7c00d422.@objectstack/spec: test 630 files / 18798 passed; typecheck exit 0;check:generatedreports all 15 artifacts up to date.@objectstack/runtime: the full suite 349 files / 5680 passed / 19 skipped (re-run because the merge touchedpackages/runtime/src).servecomposes): 236 files (235 passed, 1 skipped), 1871 tests (1862 passed, 9 skipped), exit 0.@objectstack/runtimeand@objectstack/dogfoodboth exit 0. A first pass red on both with TS7016 "Could not find a declaration file", because it ran while an unlocked gate (check:type-check-debt, its--re-measure) rebuilt the workspacedist/. Re-run through the lock after that: exit 0.pnpm lint(the full run) exit 0 ate7c00d422.Round 3, at
d4357cac5, after mergingmain(ee8751d41) in (merge6656a940d), no new behaviour:content/docs/permissions/system-context.mdx, on the census line both sides had moved.main(fix(service-storage,plugin-audit,plugin-security)!: the attachment and comment parent gates judge a controlled_by_parent parent through its master #22513) brought row9band its own +1 to the counts; this branch kept its row-51 anchorpackages/runtime/src/domains/i18n.ts#handleI18nRequest. The counts were then re-derived bypnpm gen:system-context-census: 123 reads in 58 files, living in 105 symbols.registry.tstext-merged, andgen:migration-registryrewrote it byte-identical.main's4e9fe9ff6(PROTOCOL_VERSION 17 → 18, feat(spec)!: PROTOCOL_VERSION 17 → 18 in an ordinary PR — regenerated spec-changes.json and upgrade guide, ^18 handshakes, pre-mode lockstep exception (#22085 Q1 → B) #22215) began rendering the 18.* semantic entries intopackages/spec/spec-changes.jsonanddocs/protocol-upgrade-guide.md. Six of them carry this PR's re-measured pin readings, so both artifacts read stale on the merged tree:check:spec-changes,check:upgrade-guideandcheck:generatedeach exit 1. They were regenerated bygen:spec-changesandgen:upgrade-guideind4357cac5: +12/-12 and +6/-6, the six entries' rationale text only.dispatch-gates --commandsderives 128 commands, and all 128 exit 0 atd4357cac5.--ranreconciles 128 derived, 128 run, 0 NOT-MEASURED.check:console-shaandcheck:console-injectionagain skipped for want of a Console dist, so they are NOT MEASURED locally.@objectstack/specatd4357cac5: test 631 files / 18833 passed.@objectstack/runtimeat6656a940d: 349 files / 5680 passed / 19 skipped. Dogfood at6656a940d: the authz pair plus the booted proof, 3 files / 168 passed. The WHOLE dogfood suite: 237 files (236 passed, 1 skipped), 1881 tests (1872 passed, 9 skipped), exit 0. The typecheck of spec, runtime and dogfood exits 0.d4357cac5touches only the two generated files, and neither the runtime suite nor the dogfood suite reads them.git diffoverpackages/spec, with the generatedregistry.tsandspec-changes.jsonexcluded, hashes921af3c8abc9…both ate7c00d422(againste148ca984) and atd4357cac5(againstee8751d41); the seat re-computed both. The contract review6086141672therefore still covers this PR's contract face.Round 4, at
a252adcef, after mergingmain(f782f1764, which carriesce78ff7bc, the ci: the required Temporal Conformance job pullspostgres:16andmysql:8.0from Docker Hub unauthenticated, and Docker Hub's pull rate limit now fails it before any test runs, so the merge queue ejects every pull request #22541 CI fix) and regeneratingspec-changes.jsonand the upgrade guide through their generators:a252adcef: 34 success and 1 roster skip (Packed-tarball smoke),Temporal Conformanceincluded.e7c00d422,d4357cac5,a252adcef), each diffed against its own merge base overpackages/specwith the generatedregistry.tsandspec-changes.jsonexcluded. The seat measured it three ways:--full-indexe66dc13a8567…; with theindexlines dropped,926808406aae…; with--abbrev=10,48d27a3badea…. The round-3 figure921af3c8…is the same diff under a shorter blob abbreviation.main86bf9ed7d5, a plain three-way text merge ofspec-changes.jsonand the upgrade guide (as the merge queue does it) has no conflict: this PR's hunks andmain's are disjoint.File surface beyond the claim, declared
content/docs/permissions/system-context.mdx: one anchor on row 51, plus the census counts its generator rewrote.check-system-context-censusis red without it, because the floor readsExecutionContext.isSystem. I read it as the claim's docs bullet (a hand-written page stating which domains hold the anonymous floor). The seat accepted that reading and declared the page on [PM seat] domain:devx @ objectstack — 🟢 os-bill · session_01LYXc6ckoWuZyVZpWYizdMh #6023 (6083343996).The six runtime suites under "Translation-flip sweep" sit beside the handler under test, and the dispatch's sweep clause ordered them flipped.
packages/spec(the 15 files whose records cited the old pin; anchors and shas only), the generatedcontent/docs/references/ui/view.mdx, and.changeset/objectui-pin-citations-47b1f0bb7174.md: the seat widened the claim to the pin bump's citation re-measure in6083306429, and declared it todomain:specon [PM seat] domain:spec · seat 2 — 🟢 os-sales · session_01DhTqaEHqPVSVnAkjG3jywn #18549 and [PM seat] domain:spec — 🟢 os-tesla · session_01VZqqwTj2wsihZEbfT6yyYN #6017.packages/spec/spec-changes.jsonanddocs/protocol-upgrade-guide.md(round 3): generator output only. Both were re-derived on the merged tree from the six claimed18.*-unit-in-key.tsentries, oncemain's protocol-18 bump began rendering those entries. No source edit.Acceptance notes
6077215779on the card, seat reading6078635289). objectui'sexamples/console-starterloads translations with a barefetchand signs in in place. Once this lands, a starter user sees untranslated application labels after sign-in until a reload. No raw key is shown: the readers fall back to the authored literal. objectui's seat owns any starter card.47b1f0bb7174no later than its framework pin carries this refusal. That row on objectstack-ai/cloud#2709 belongs to the landing seat.covers, 17 keys and 45enforcedafter PR 22446 (53 / 9 / 18 / 46 measured at the base). The census's dispatcher-domain note had read 5 / 16 and 11 files since/analytics.I18nServicePlugin.registerI18nRoutesmounts the same three paths with no anonymous floor of its own. In every in-repo composition (os serve,os dev,DevPlugin) the dispatcher's gated mounts register first and answer; this boot measured that. A host that composesI18nServicePluginon an HTTP server without the runtime dispatcher plugin was not measured. It is listed in the card report for the seat, with the ADR-0138 everything-else re-measure as its natural carrier.anonymous-deny.tsdocblock in@objectstack/corestill says "the five runtime domains", and the table header inpackages/runtime/src/endpoint-policy.tsis in the same state. Neither names/packages,/analyticsor now/i18n. Carrier: none.access-security.anonymous-deny-surfacesdoes not gain an i18n variant, and thei18n.jsonitem's server-truth step reads the three faces without saying it signs in first. A runner who reads them anonymously now gets 401. Carrier: none.minorwith the BREAKING banner, the launch-window convention for a door narrowing. ADR-0087 disposition:not-required (no-migration-prescription).Generated by Claude Code