Repository navigation
feat(spec): BlueprintViewSchema and StrictView declare filters and sort, so an AI-built view carries its own filter - #24152
Conversation
A blueprint view can now carry the filter its name promises: the lenient schema reuses ViewFilterRuleSchema and the shared SortItemSchema, and the strict structured-output mirror carries the same keys in strict-compatible shapes (nullable, canonical operator enum). Key parity is extended to the view shape and the nested rule and sort-entry shapes. Claude-Session: https://claude.ai/code/session_01HSDGREvqxQUt7K7pjHXMyC Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSDGREvqxQUt7K7pjHXMyC Co-authored-by: Claude <noreply@anthropic.com>
…ueprint view The lenient blueprint view's filters reuse ViewFilterRuleSchema, whose operator/value-shape refinement does not project into JSON Schema; the build requires the two new sites in the hand-edited ledger, the same site ui/ListView already declares as filter.element. Claude-Session: https://claude.ai/code/session_01HSDGREvqxQUt7K7pjHXMyC Co-authored-by: Claude <noreply@anthropic.com>
…skill index gen:docs, gen:strictness-ledger and gen:skill-refs output for the blueprint view's filters and sort. The skill index gains the transitive spec modules the blueprint now imports (ui/view.zod and its deps, shared/enums.zod). Claude-Session: https://claude.ai/code/session_01HSDGREvqxQUt7K7pjHXMyC Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSDGREvqxQUt7K7pjHXMyC Co-authored-by: Claude <noreply@anthropic.com>
…ueprint-view-filters-sort
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 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 87bb0a536b900146931ef3b2c9f6eb68243c4c83 && git checkout 87bb0a536b900146931ef3b2c9f6eb68243c4c83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 265ef64ba7047919460862c4e798cd5651dd92a7 d896c66e62eb00be6625bb5f1ee1f9c9de8d7623 && git checkout -B drift-repro 265ef64ba7047919460862c4e798cd5651dd92a7 && git merge --no-ff d896c66e62eb00be6625bb5f1ee1f9c9de8d7623
node scripts/docs-audit/affected-docs.mjs --json 265ef64ba7047919460862c4e798cd5651dd92a7
|
席位核对ACCEPT · The seat's own review at
Public correction by the seat: the dispatch order's A5 told the dev to stop on any Out of scope, where each goes:
Generated by Claude Code |
Contract reviewServed-tier: Inputs read: card #24129 body and both comments (the Claim and the os-dev-report), PR #24152 body, its 8-file list and the net diff against ① Derived judgments
Check-runs on the head as read: 28 success (among them ② Semver level
Clause-②: yes (widening) ③ Boundary flagsOpen question (the dev's one): does the strict mirror's own accept-set narrowing — a strict document whose views lack
Dev deviations, each answered:
Out-of-scope findings, both correctly left unfiled: the Nothing escalated. objectstack-ai/cloud is unreadable from this session; every statement about the ruling text, the interim extension and the applier mapping is the card's restatement, named as such above. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #24129
Clause-②: yes (widening)
What
A solution-blueprint view can now carry the filter its name promises.
BlueprintViewSchema(lenient, whatapply_blueprintparses) andStrictViewinsideSolutionBlueprintStrictSchema(the OpenAI-strictgenerateObjectcontract the design model generates against) both declarefiltersandsort. This is ruling A on objectstack-ai/cloud#2932 (「2932 同意」), as the card restates it; the cloud ruling text is not readable from this session.Before this, a plan could say 「overdue only」 in a view's name and nowhere else, so the applier shipped an unfiltered grid under that name (objectstack-ai/cloud#2922). Neither key invents a format.
filters: which records the view shows, ANDedBlueprintViewSchema.filtersStrictView.filtersViewFilterRuleSchemaitself (identity-pinned in the test)StrictViewFilterRule, keys pinned equal toViewFilterRuleSchema'sfieldstringstring(noSNAKE_CASEon either side: value parity)operatorz.preprocess(fold, z.enum(VIEW_FILTER_OPERATORS)): canonical, legacy spellings foldedz.enum(VIEW_FILTER_OPERATORS): canonical only, no foldvalueintakes an array,betweentakes[min, max],icontainstakes a non-empty string)Why each strict-side difference exists:
valuenullable, not optional. Strict mode needs every key listed inrequired, and null is how this mirror spells "optional". It is also the spelling the unary operators use. The blueprint tools strip nulls before the lenient parse, so the lenient rule seesvalueabsent, which it accepts for unary operators.operator. The lenientz.preprocessis a parse-time transform with no JSON-Schema form. A model emitting the canonical vocabulary is the alias table's own contract for new producers. The strict test pins thateqis refused atviews.0.filters.0.operatorwithinvalid_value.questions.max(2),seedData). The lenient parse thatapply_blueprintruns refuses these, and a test pins it (inwith a scalar, issue atviews.0.filters.0.value, codecustom). The describe on the strictvaluestates the shape rules for the model.sort: default record orderSortItemSchema(shared/enums.zod.ts, identity-pinned){ field: string, order: SortDirectionEnum }, both keys required"field desc"string is refused (invalid_typeatviews.0.sort)ListView.sort's entry is an inlinestrictObjectand is not exported.SortItemSchemais the exported shared sort item, and that entry's own comment says its titles mirror it: the same{ field, order }keys and the sameasc/descvocabulary. The two differ in one way. TheListViewentry rejects unknown keys and foldsdirectiontoorder.SortItemSchemais a plainz.object, so it strips unknown keys, which is the posture of every other blueprint shape. A{ field, direction }entry is still refused:orderis required and has no default, so it cannot silently reverse a sort. The blueprint does not accept a string form, becauseListView.sortno longer does.Describes
Both sides tell the model that a view whose name or label states a condition (「overdue / 逾期」, 「my open / 我的未完成」, 「待审批」, 「本月」) MUST carry the rules that make the name true. They give worked rules that use the platform's declared tokens
{today}(date macros) and{current_user_id}(context tokens). Both tokens are expanded on both sides of the wire, andexamples/app-todo's overdue list view uses the same rule pair. The describe prose is deliberately not pinned by a test.Tests (
packages/spec/src/ai/solution-blueprint.test.ts)filtersandsortthrough the parse; both optional; identity pins (filterselementtoBe(ViewFilterRuleSchema),sortelementtoBe(SortItemSchema)); a bad operator is refused atviews.0.filters.0.operator(invalid_value);inwith a scalar atviews.0.filters.0.value(custom); a badorderatviews.0.sort.0.order(invalid_value); a string sort atviews.0.sort(invalid_type).inwith a list andis_nullwith null; null accepted for both keys; each key is required (omitting it givesinvalid_typeat that key); a rule'svaluekey is required (invalid_union);eqis refused; a badorderis refused. The existing gallery/gantt strict fixture now sendsfilters: null, sort: null.orderenum equals the lenient one. The VALUE-parity walker covers the new string leaves automatically:views[].filters[].fieldandviews[].sort[].fieldcarry no pattern on either side.Generated and ledger artifacts
packages/spec/authorable-surface/ai.json(+BlueprintView:filters, +BlueprintView:sort),content/docs/references/ai/solution-blueprint.mdx,docs/audits/2026-07-unknown-key-strictness-ledger.counts/ai.md(ai sites 78 to 80: the two inline strict shapes), andskills/objectstack-ai/references/_index.md(see below).api-surface/,json-schema.manifest/,declaration-map/,export-origins/andauthorable-surface.base.jsondid not move, because there are no new exports.build-schemas.tsrequires:packages/spec/dropped-refinements.baseline.jsongainsai/BlueprintView: ["filters.element"]andviews.element.filters.elementonai/SolutionBlueprint(sites 764 to 766, schemas 234 to 235). This is theViewFilterRuleSchemarefinement that the published lenient JSON Schema cannot carry.ui/ListViewalready declares the same gap asfilter.element. The strict schema carries no refinement and drops nothing.json-schema/ai/SolutionBlueprintStrict.json, built locally):views.items.requiredis nowobject, name, label, type, columns, groupBy, filters, sort, withadditionalProperties: false. The rule item requiresfield, operator, value, andoperatoris the 20-member canonical enum. The sort item requiresfield, order, andorderis the enumasc, desc.skills/**: one generator-owned file movesgen:skill-refslists each skill's transitive spec modules. The blueprint now importsui/view.zodandshared/enums.zod, soskills/objectstack-ai/references/_index.mdgains 7 pointer lines (shared/enums.zod,shared/http.zod, andui/view.zodwith itsbulk-action,chart,i18nandsharingmodules). Without it, the requiredcheck:skill-refsis red.skills/*/SKILL.md) goes from 4389 to 4389 lines: no SKILL.md is touched.node scripts/pm/check-governed-merges.mjs --branch claude/issue-24129-blueprint-view-filters-sorton headd896c66e6: "governed-surface predicate: 0 of 8 path(s) hit the register after 1 generated-artifact lift(s) (7 surfaces govern objectstack). ✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list." The index is a PURE REGENERATION, byte-equal topnpm --filter @objectstack/spec gen:skill-refsrecomputed on this tree. That is the generator-owned carve-out in that script, andgoverned-surface-guard.ymlinstalls the toolchain so the queue guard can recompute the same answer. No hand-authoredskills/**content moves. Size: 325 changed lines (+318 / -7), under the 3000 threshold.Local verification
pnpm --filter @objectstack/spec build: exit 0.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 692 files, 20969 passed, 1 todo, exit 0.pnpm --filter @objectstack/spec typecheck: exit 0. These ran at0b87a3245. The later merge oforigin/main(d896c66e6) brings onlydocs/qa/**andexamples/app-showcase/**, so no spec input moved.pnpm --filter @objectstack/spec check:generated: "✓ All 14 generated artifacts are up to date", ond896c66e6.node scripts/pm/dispatch-gates.mjs --commandsderived 113 commands, and all 113 ran ond896c66e6with exit 0.--ranreconciliation: "113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN". On the first pass (at0b87a3245), seven gates refused with exit 3 because a prerequisite was unbuilt or a fixture commit was not in the shallow clone. They were re-run green afterturbo run buildof formula, lint, client, client-react and objectql, and a depth-1 fetch of the fixture commit.pnpm check:dev-prereqs(local-only family): exit 0.test:repoproject, the integration tiers, and the path-scheduled CI jobsdispatch-gateslists (Test Core shards, Build Core, Build Docs, dogfood).Acceptance notes
filters, as the ruling and the card name it. The runtimeListViewkey isfilter. The applier (cloud, not readable here) is what expands a blueprint view into aListViewbody, so the mapping between the two spellings lives there. The lenient blueprint schemas arez.objects and strip undeclared keys, so a hand-authored blueprint view that writes the singularfilterloses it silently. The same is true of every other undeclared blueprint key, and the strict path cannot emit an undeclared key at all.filters/sorton a view parsed before and is refused now. That holds for every key ever added to this mirror (viewName,nameField,sharingModel,expressionall shipped the same required-nullable way, under Minor Changes). The mirror is a generation contract, so its only producer is the model, which receives the new JSON Schema. It is declaredClause-②: yes (widening)as the card rules. The contract review may weigh this.git grepon this head, outsidepackages/spec): only a comment inpackages/cli/src/lint/metadata-eval.ts(the type nameSolutionBlueprint), plus the generated docs. The real producer and consumer are cloud'spropose_blueprint/apply_blueprint. Its interim.extend()(cloud PR feat(lint): bidirectional binding-root check for visibility predicates (ADR-0089 D3b) #2931) is not readable here, and its key spelling was not compared.Generated by Claude Code