Skip to content

feat(spec): BlueprintViewSchema and StrictView declare filters and sort, so an AI-built view carries its own filter - #24152

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24129-blueprint-view-filters-sort
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24129-blueprint-view-filters-sort

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24129
Clause-②: yes (widening)

What

A solution-blueprint view can now carry the filter its name promises. BlueprintViewSchema (lenient, what apply_blueprint parses) and StrictView inside SolutionBlueprintStrictSchema (the OpenAI-strict generateObject contract the design model generates against) both declare filters and sort. 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, ANDed

Lenient BlueprintViewSchema.filters Strict StrictView.filters
presence optional required, nullable (null = a view over every record)
element ViewFilterRuleSchema itself (identity-pinned in the test) inline StrictViewFilterRule, keys pinned equal to ViewFilterRuleSchema's
field string string (no SNAKE_CASE on either side: value parity)
operator z.preprocess(fold, z.enum(VIEW_FILTER_OPERATORS)): canonical, legacy spellings folded z.enum(VIEW_FILTER_OPERATORS): canonical only, no fold
value optional; string, number, boolean, null, or a list of strings or numbers required, nullable; string, number, boolean, or a list of strings or numbers
operator/value shape (in takes an array, between takes [min, max], icontains takes a non-empty string) refused by the reused refinement not expressed

Why each strict-side difference exists:

  • value nullable, not optional. Strict mode needs every key listed in required, 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 sees value absent, which it accepts for unary operators.
  • No fold on operator. The lenient z.preprocess is 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 that eq is refused at views.0.filters.0.operator with invalid_value.
  • No operator/value-shape refinement. A refinement has no JSON-Schema form, and the strict mirror precedent already leaves out what strict mode cannot express (questions.max(2), seedData). The lenient parse that apply_blueprint runs refuses these, and a test pins it (in with a scalar, issue at views.0.filters.0.value, code custom). The describe on the strict value states the shape rules for the model.

sort: default record order

Lenient Strict
presence optional required, nullable (null = the platform default order)
element the shared SortItemSchema (shared/enums.zod.ts, identity-pinned) inline { field: string, order: SortDirectionEnum }, both keys required
form array only; a "field desc" string is refused (invalid_type at views.0.sort) array only

ListView.sort's entry is an inline strictObject and is not exported. SortItemSchema is the exported shared sort item, and that entry's own comment says its titles mirror it: the same { field, order } keys and the same asc / desc vocabulary. The two differ in one way. The ListView entry rejects unknown keys and folds direction to order. SortItemSchema is a plain z.object, so it strips unknown keys, which is the posture of every other blueprint shape. A { field, direction } entry is still refused: order is required and has no default, so it cannot silently reverse a sort. The blueprint does not accept a string form, because ListView.sort no 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, and examples/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)

  • Lenient: carries filters and sort through the parse; both optional; identity pins (filters element toBe(ViewFilterRuleSchema), sort element toBe(SortItemSchema)); a bad operator is refused at views.0.filters.0.operator (invalid_value); in with a scalar at views.0.filters.0.value (custom); a bad order at views.0.sort.0.order (invalid_value); a string sort at views.0.sort (invalid_type).
  • Strict: carries rules, including in with a list and is_null with null; null accepted for both keys; each key is required (omitting it gives invalid_type at that key); a rule's value key is required (invalid_union); eq is refused; a bad order is refused. The existing gallery/gantt strict fixture now sends filters: null, sort: null.
  • Key parity, extended a level further out (fields, then objects, then nav items, now VIEWS): the view key sets are equal, with no deliberate exclusions. The nested filter-rule and sort-entry key sets are equal. The strict operator enum equals the lenient one, and the strict order enum equals the lenient one. The VALUE-parity walker covers the new string leaves automatically: views[].filters[].field and views[].sort[].field carry no pattern on either side.

Generated and ledger artifacts

  • Regenerated by the generators: 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), and skills/objectstack-ai/references/_index.md (see below). api-surface/, json-schema.manifest/, declaration-map/, export-origins/ and authorable-surface.base.json did not move, because there are no new exports.
  • Hand-edited ledger, as build-schemas.ts requires: packages/spec/dropped-refinements.baseline.json gains ai/BlueprintView: ["filters.element"] and views.element.filters.element on ai/SolutionBlueprint (sites 764 to 766, schemas 234 to 235). This is the ViewFilterRuleSchema refinement that the published lenient JSON Schema cannot carry. ui/ListView already declares the same gap as filter.element. The strict schema carries no refinement and drops nothing.
  • The published strict JSON Schema (json-schema/ai/SolutionBlueprintStrict.json, built locally): views.items.required is now object, name, label, type, columns, groupBy, filters, sort, with additionalProperties: false. The rule item requires field, operator, value, and operator is the 20-member canonical enum. The sort item requires field, order, and order is the enum asc, desc.

skills/**: one generator-owned file moves

gen:skill-refs lists each skill's transitive spec modules. The blueprint now imports ui/view.zod and shared/enums.zod, so skills/objectstack-ai/references/_index.md gains 7 pointer lines (shared/enums.zod, shared/http.zod, and ui/view.zod with its bulk-action, chart, i18n and sharing modules). Without it, the required check:skill-refs is red.

  • Readings: the changed file goes from 43 to 50 lines. The whole published skill package (the sum of all skills/*/SKILL.md) goes from 4389 to 4389 lines: no SKILL.md is touched.
  • Governance, node scripts/pm/check-governed-merges.mjs --branch claude/issue-24129-blueprint-view-filters-sort on head d896c66e6: "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 to pnpm --filter @objectstack/spec gen:skill-refs recomputed on this tree. That is the generator-owned carve-out in that script, and governed-surface-guard.yml installs the toolchain so the queue guard can recompute the same answer. No hand-authored skills/** 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 at 0b87a3245. The later merge of origin/main (d896c66e6) brings only docs/qa/** and examples/app-showcase/**, so no spec input moved.
  • pnpm --filter @objectstack/spec check:generated: "✓ All 14 generated artifacts are up to date", on d896c66e6.
  • node scripts/pm/dispatch-gates.mjs --commands derived 113 commands, and all 113 ran on d896c66e6 with exit 0. --ran reconciliation: "113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN". On the first pass (at 0b87a3245), 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 after turbo run build of 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.
  • Not run locally, left to CI: the test:repo project, the integration tiers, and the path-scheduled CI jobs dispatch-gates lists (Test Core shards, Build Core, Build Docs, dogfood).

Acceptance notes

  • Key name. The blueprint key is filters, as the ruling and the card name it. The runtime ListView key is filter. The applier (cloud, not readable here) is what expands a blueprint view into a ListView body, so the mapping between the two spellings lives there. The lenient blueprint schemas are z.objects and strip undeclared keys, so a hand-authored blueprint view that writes the singular filter loses it silently. The same is true of every other undeclared blueprint key, and the strict path cannot emit an undeclared key at all.
  • The strict mirror's own accept set narrows. A strict document without filters / sort on a view parsed before and is refused now. That holds for every key ever added to this mirror (viewName, nameField, sharingModel, expression all 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 declared Clause-②: yes (widening) as the card rules. The contract review may weigh this.
  • In-repo consumers (git grep on this head, outside packages/spec): only a comment in packages/cli/src/lint/metadata-eval.ts (the type name SolutionBlueprint), plus the generated docs. The real producer and consumer are cloud's propose_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.
  • Surface beyond the claim's file list, all mechanically required: the dropped-refinements ledger lines, the strictness-ledger count shard, and the skill index regeneration.

Generated by Claude Code

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>
…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 8 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json, packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-handling-server.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/api/wire-format.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/automation/flows.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/concepts/metadata-driven.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/data-modeling/field-type-decision-tree.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/data-modeling/fields.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/data-modeling/objects.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/getting-started/common-patterns.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort), not_equals (literal, a string literal in BlueprintViewSchema; a string literal in filters))
  • content/docs/getting-started/examples.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/kernel/contracts/data-engine.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/protocol/objectql/query-syntax.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/protocol/objectui/index.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/protocol/objectui/layout-dsl.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/ui/actions.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/ui/views.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via due_date (literal, a string literal in BlueprintViewSchema; a string literal in StrictViewFilterRule; a string literal in filters; a string literal in sort))
  • content/docs/releases/v17/17-6.mdx (via not_equals (literal, a string literal in BlueprintViewSchema; a string literal in filters))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json, packages/spec/dropped-refinements.baseline.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 265ef64ba7047919460862c4e798cd5651dd92a7 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 87bb0a536b900146931ef3b2c9f6eb68243c4c83 — the merge of head d896c66e62eb00be6625bb5f1ee1f9c9de8d7623 into base 265ef64ba7047919460862c4e798cd5651dd92a7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 265ef64ba7047919460862c4e798cd5651dd92a7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 · huangyiirene · session session_01HSDGREvqxQUt7K7pjHXMyC · 2026-10-17T05:18Z · head d896c66e62eb00be6625bb5f1ee1f9c9de8d7623

The seat's own review at TIER_DEFAULT of the parts outside the contract-review scope. The CONTRACT_REVIEW_TIER record for solution-blueprint.zod.ts and the skills index follows as its own comment.

item reading
shape Draft, base main. First line Fixes #24129; second line Clause-②: yes (widening), line-start. No other closing keyword sits next to an issue number.
file list 8 files, +318 / −7: the schema and its test, the changeset, three generated artifacts (authorable-surface/ai.json, the reference .mdx, the strictness count shard ai.md 78 → 80), the hand-edited dropped-refinements.baseline.json (+2 sites, recounted at the head: 235 schemas / 766 sites, equal to its measured block), and the regenerated skills/objectstack-ai/references/_index.md (+7 pointer lines). No content/docs/releases/ change.
premise Both view shapes had no filters / sort on origin/main 462d02f00a; the dev re-measured on its base.
token spelling The describes teach {today} and {current_user_id}. Checked: the single-brace dialect is retired from flow positions only; both remain query placeholders the query engine resolves (packages/spec/src/automation/builtin-node-config.zod.ts:558), and examples/app-todo view filters already use '{today}'.
changeset '@objectstack/spec': minor, Clause-②: yes (widening). Sentences checked against the diff: the two keys on both shapes; the lenient element "IS ViewFilterRuleSchema" (identity-pinned in the test); strict "required and nullable", "canonical vocabulary only", "operator/value-shape coupling is not in the strict mirror's JSON Schema"; sort "{ field, order } … array only"; "every blueprint that parsed before still parses with the lenient schema"; and the strict-producer note in the last bullet. Each is true of the diff.
governed / size The CI Governed Surface Queue Guard on d896c66e62eb printed "generated-surface exception (#11705) LIFTED skills/objectstack-ai/references/_index.md … byte-equal to pnpm --filter @objectstack/spec gen:skill-refs recomputed on this tree" and ✅ CLEAR. The seat's own check-governed-merges.mjs --pr 24152 in a checkout without the toolchain failed closed (the generator's --check exited 254, so nothing could be lifted); the CI job, which installs the toolchain, is the reading that counts. 325 changed lines, under 3000. Ordinary queue landing.
gates At this comment: 30 success, 5 skipped (all rostered), 5 Test Core shards in_progress, 0 failures, on d896c66e62eb. The seat waits for every check before the queue.
deviations Accepted: the three mechanically required paths beyond the claim (ledger lines, count shard, skills index); the merge of main before the PR; the claim's bare Clause-②: yes against the body's yes (widening).

Public correction by the seat: the dispatch order's A5 told the dev to stop on any skills/** move. That was too wide: a generator-owned _index.md is carved out of the governed fork by the maintainer's ruling on #11705 when it is a pure regeneration. The seat sent the correction to the running dev, which kept the file; the guard's lift above confirms it.

Out of scope, where each goes:


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d896c66e62eb00be6625bb5f1ee1f9c9de8d7623
Local-runs: none
Scope: packages/spec/src/ai/solution-blueprint.zod.ts, skills/objectstack-ai/references/_index.md + packages/spec/src/ui/view.zod.ts (VIEW_FILTER_OPERATORS, ViewFilterRuleSchema and checkViewFilterRuleValueShape, the ListView filter and inline sort entries), packages/spec/src/shared/enums.zod.ts (SortItemSchema, SortDirectionEnum), packages/spec/src/data/filter.zod.ts (bare condition list = implicit AND), packages/spec/src/data/context-tokens.zod.ts and packages/spec/src/data/date-macros.zod.ts (the {current_user_id} / {today} tokens the describes name), packages/spec/scripts/build-schemas.ts with packages/spec/dropped-refinements.baseline.json (the ledger rule and its gate, read as one), packages/spec/scripts/build-skill-references.ts with scripts/pm/check-governed-merges.mjs GENERATED_SURFACE_EXCEPTIONS (the skills index generator and its lift), packages/spec/src/ai/solution-blueprint.test.ts, .changeset/24129-blueprint-view-filters-sort.md, packages/spec/CHANGELOG.md (the 17.0.0 / 17.2.0 / 17.4.0 strict-mirror precedent entries)

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 main (+318/-7), and the 39 check-runs on the head. objectstack-ai/cloud is not readable from this session: the ruling text on cloud#2932 and the interim .extend() in cloud PR #2931 are taken as the card restates them, and the applier's filters-to-filter mapping is unverified here. The rest of the diff (the regenerated mdx, the strictness-ledger count shard, the changeset prose) is the lane seat's ACCEPT at TIER_DEFAULT.

① Derived judgments

  1. Lenient BlueprintViewSchema.filters: z.array(ViewFilterRuleSchema).optional() — RIGHT. A pure widening of the lenient accept set. The element is the same schema object ListView.filter carries (view.zod.ts:2775), so the blueprint rule takes the canonical VIEW_FILTER_OPERATORS, folds the legacy spellings, and runs the operator/value-shape and icontains comparand refinements; the test pins identity (toBe), not shape. Unknown rule keys are refused (the rule is a strictObject) inside a parent that strips (z.object) — the same mixed posture ListView itself has.

  2. Lenient BlueprintViewSchema.sort: z.array(SortItemSchema).optional() — RIGHT. The ListView entry (view.zod.ts:2813) is an inline, unexported strictObject with exactly { field, order: asc | desc }, a direction to order alias fold and the retired bare-string prescription. The shared export SortItemSchema (enums.zod.ts:27-30) has the same two keys and vocabulary. The blueprint differs in three measured ways, none silent: a { field, direction } entry is refused because order is required with no default (it cannot reverse a sort quietly); an unknown key is stripped rather than refused; a bare string gets a plain invalid_type and not the retirement text, which is correct because the blueprint never accepted the string, so there is no retired spelling to prescribe for. Reusing the export rather than copying the inline shape was the right choice given the file surface (an edit to view.zod.ts was a declared breach).

  3. Strict StrictView.filters / StrictView.sort, required-nullable — RIGHT as the mirror's standing convention (every key in required, null spells optional; the same shape viewName, nameField, sharingModel and expression took). The strict rule's operator: z.enum(VIEW_FILTER_OPERATORS) is the same constant without the preprocess fold — correct, a transform has no JSON-Schema form and the mirror is a new-producer contract; the parity test pins the two enums equal. value is a nullable union of the same members as the lenient union (null moved from an inner arm to .nullable()), so present values have the same accept set and absence is refused, as strict mode needs. The operator/value-shape refinement is not re-declared — correct (no JSON-Schema form; the mirror already omits questions.max(2) and seedData), and apply_blueprint's lenient parse refuses the violation (pinned: in with a scalar at views.0.filters.0.value, code custom). Residual, stated plainly: a model can still emit in with a scalar or icontains with an empty string and learn it at apply time rather than generation time; the strict value describe states the shape rules to the model, which is the available mitigation and the one the mirror already relies on elsewhere. A strict value: null on a scalar operator is loud on either cloud path: stripped to absent, the lenient refinement refuses it (the undefined-comparand rule in checkViewFilterRuleValueShape); left as null, it is the same legal shape ListView accepts today.

  4. Published JSON-schema shapes — RIGHT. json-schema/ai/BlueprintView.json and SolutionBlueprint.json gain optional filters and sort; SolutionBlueprintStrict.json's view item required grows to include both (the regenerated mdx in the diff shows the strict rows as required and the lenient rows as optional, which is the rendering of that tree). authorable-surface/ai.json gains exactly ai/BlueprintView:filters and ai/BlueprintView:sort. api-surface/ does not move and should not: StrictViewFilterRule and StrictSortItem are module-private consts, no export is added, removed or renamed.

  5. Dropped-refinements ledger — RIGHT, and the gate was honoured. Two new sites, ai/BlueprintView: filters.element and ai/SolutionBlueprint: views.element.filters.element, are the ViewFilterRule refinement the published lenient file cannot carry; ui/ListView: filter.element and ui/ViewTab: filter.element are the standing precedent lines for the same rule. measured moves 234 to 235 schemas and 764 to 766 sites; recounted from the file at the head: 235 entries, 766 sites. No refinement was deleted or weakened; the strict side declares no refinement and so adds no line. build-schemas.ts refuses the build until the hand-edited line moves with the site, and that build ran green inside Type Check · source gates (the check:authorable-surface step writes the json-schema tree through it).

  6. Strictness-ledger count shard, ai/ 78 to 80 — RIGHT: the two inline z.object sites the strict mirror adds, the same posture as every inline shape already in that mirror. Generator-owned.

  7. skills/objectstack-ai/references/_index.md, +7 pointer lines — RIGHT, and not hand-authored content. The seven are the transitive closure of the two new imports: shared/enums.zod, and ui/view.zod with the modules view.zod.ts itself imports at lines 64-66, 80 and 86 (ui/i18n.zod, ui/chart.zod, ui/sharing.zod, ui/bulk-action.zod, shared/http.zod). No SKILL.md moves. check:skill-refs is green on this head (Type Check · source gates) and the Governed Surface Queue Guard is green, which is the gate that recomputes gen:skill-refs on the tree under test and lifts this path on proven pure-regeneration provenance (the spec-skill-refs row). The path sits in this record's Scope because the review face reads the governed register, not the lift; the landing is an ordinary queue landing.

  8. Every published .describe() sentence, against the platform it describes:

    • "ANDed" — TRUE. A view's rule array lowers to a bare list of AST conditions, and filter.zod.ts:2856 / :2916 define a bare list as implicit AND.
    • {today} and {current_user_id} — TRUE. today is a fixed date-macro token ("Start of today, YYYY-MM-DD", date-macros.zod.ts:89, :238); {current_user_id} is a context token (context-tokens.zod.ts), whose own doc example is exactly [{ field: 'owner', operator: 'equals', value: '{current_user_id}' }]; both vocabularies are what validate-filter-tokens lints and ObjectQL resolves. The worked overdue rule pair (due_date less_than {today} and status not_equals completed) is the overdue definition examples/app-todo records at task.object.ts:161.
    • "in / not_in take an ARRAY (any length)", "between takes exactly [min, max]", unary operators take their direction from the name — TRUE (checkViewFilterRuleValueShape, VIEW_FILTER_LIST_VALUE_OPERATORS, VIEW_FILTER_PAIR_VALUE_OPERATORS, VIEW_FILTER_VALUELESS_OPERATORS). The strict value describe omits the icontains non-empty-string rule the lenient (inherited) describe states — an omission, not a falsehood; the lenient parse refuses it.
    • "order asc or desc", "the first entry sorts first", "omit / null for the platform default order" — TRUE (SortDirectionEnum; ordered multi-key sort).
    • "REQUIRED whenever the view's name or label states a condition" — model guidance, not a schema constraint (no schema can read a name's meaning). This is the established idiom of this file (groupBy: "REQUIRED for kanban views"; summaryOperations: "REQUIRED when type is summary"), and it is not a declared-but-unenforced runtime capability: the enforcement the sentence promises is the filter the view then carries.
    • The strict field describes say snake_case without a regex, matching the lenient ViewFilterRuleSchema.field: z.string() so the VALUE-parity walker stays equal — guidance only, consistent on both sides.
    • The docblock's claim that the ListView sort entry "is an inline shape, not an export" — TRUE (view.zod.ts:2813).
  9. Tests — adequate for the contract: identity pins on both elements, refusal pins asserted as path plus code on both sides, key parity extended to views and one level further to the rule and sort-entry key sets, and enum parity for operator and order. The strict gallery/gantt fixture now sends filters: null, sort: null, which is the right update for a required-nullable key.

  10. Key spelling filters (blueprint) against filter (ListView) — as the card and ruling name it. The mapping is the cloud applier's and unreadable here. A hand-authored blueprint writing the singular filter loses it to z.object stripping, as it loses every undeclared key today; the strict path cannot emit it, and a blueprint is never persisted. Not a defect of this PR; the repo-wide z.object posture is already tracked in the unknown-key strictness ledger, so no card is owed by this PR. If a stricter blueprint parent is ever wanted, that is its own card.

  11. In-repo consumers: the diff touches nothing outside packages/spec except generated or ledger files; with no export change there is nothing for a consumer typecheck to pick up. Nothing is armed: the PR is draft, auto-merge is off, head repo equals base repo, 325 changed lines.

Check-runs on the head as read: 28 success (among them Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset, Spec property liveness, Type Check · source gates, every Lint · lane and every single-writer / claim check), 5 skipped by design (Console Pin Gate, Dogfood matrix placeholder, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance), 11 still in_progress (Build Core, Build Docs, Test Core 1/6 through 6/6, Type Check · consumer gates / debt ledger / workspace), 0 failure. The green-CI landing pre-check is the seat's and is not yet satisfied at this reading; it is separate from this verdict.

② Semver level

'@objectstack/spec': minor with Clause-②: yes (widening) on the PR body (line 2) and in the changeset (line 7), identical. The claim comment's bare Clause-②: yes is the claim-form field, which carries no arm; the arm belongs on the PR body and changeset, where it is. The lenient accept set — the one apply_blueprint, defineSolutionBlueprint and every hand-authored blueprint are judged by — only widens: every blueprint that parsed before parses after. Minor is the floor a yes takes and the right level here. The changeset states both halves, including the sentence a strict producer needs ("must now send filters and sort on each view, null when it has none"), which is the upgrade note for the one consequence that is not purely additive (judged in ③). Check Changeset is green; check:adr-0087-registration reads a non-breaking arm and so asks for no disposition, and Lint & Repo Gates is green.

Clause-②: yes (widening)

③ Boundary flags

Open question (the dev's one): does the strict mirror's own accept-set narrowing — a strict document whose views lack filters / sort parsed before and is refused now — keep Clause-②: yes (widening) / minor, or is it a breaking change owing an ADR-0087 disposition? Answer: A, keep yes (widening) and minor; no ADR-0087 disposition is owed. On the evidence:

  • Who parses against the strict mirror: by the file's own header it "is used ONLY as the generateObject output contract", and "Validate / apply still go through the lenient SolutionBlueprintSchema"; the blueprint "is never persisted on its own". So no stored row and no hand-authored document is ever judged by it, and ADR-0087's dispositions (D2 conversions of stored or authored shapes, D3 migration steps, os migrate meta) have nothing to act on — there is no document at rest to convert or migrate. ADR-0059's two promises (what you author works; what worked on N keeps working on N+1) are about authored metadata, and authored blueprints are judged by the lenient schema, which only widens.
  • Its only measured producer is cloud's propose_blueprint, which is handed the schema on every call; per the card, cloud PR feat(lint): bidirectional binding-root check for visibility predicates (ADR-0089 D3b) #2931 already extends both schemas with these keys as the ruling's bounded interim, so the one producer emits them today. The spelling of that extension is unverified here (cloud unreadable); the cloud removal card that is Blocked-by spec(ai): BlueprintViewSchema and StrictView declare filters (ViewFilterRule[]) and sort, so an AI-built view carries its own filter (ruling A on cloud#2932) #24129 is where it gets compared.
  • Precedent, read in packages/spec/CHANGELOG.md at the head: expression (4820f55, 17.0.0, Minor Changes — "BlueprintFieldSchema and its OpenAI-strict mirror both gain expression"), sharingModel (7d2d112, 17.2.0, Minor Changes), nameField (ceb33a9, 17.2.0, Minor Changes — "required-but-nullable, matching the strict convention"), viewName (8333a6c, 17.4.0, Minor Changes — "in lockstep, to the strict mirror's StrictNavItem"). Four keys, four Minor entries, no ADR-0087 disposition on any. Each one grew the strict required set exactly as this PR does.
  • Residual, stated plainly: a third party generating against a cached copy of the published json-schema/ai/SolutionBlueprintStrict.json that does not refresh it will have its output refused at views.N.filters until it refreshes. That is the convention's defined behaviour ("every key present"), carried by every earlier strict key, and the changeset's last bullet tells the upgrader so. The standing rule for the next author, so this question is not re-litigated: a key added to the strict mirror is minor while (a) the lenient schema only widens and (b) the mirror is used only as a generation contract; the PR on which either stops holding is the narrowing one and owes the disposition.

Dev deviations, each answered:

  • packages/spec/dropped-refinements.baseline.json hand-edited beyond the claim's file list — accepted; mechanically required by build-schemas.ts, consistent on recount (judgment 5).
  • docs/audits/2026-07-unknown-key-strictness-ledger.counts/ai.md regenerated — accepted (judgment 6).
  • skills/objectstack-ai/references/_index.md regenerated and kept — accepted; the queue guard's green on this head is the certification of the lift (judgment 7). Premise A5 of the dispatch was falsified by measurement and reported as such, which is the right handling.
  • Clause-② spelling differing between the claim comment and the PR body / changeset — not a flag (② above).
  • Merge of origin/main before opening, a depth-1 fetch of a fixture commit into the shared object store, a trap-restored double run of gen:skill-refs — local-process notes, outside the contract; the regenerated index is judged on its bytes and its gate.
  • No describe-prose pin — correct under the dev contract; the regenerated mdx in the diff pins the rendered text through check:docs in any case.

Out-of-scope findings, both correctly left unfiled: the filters / filter spelling (judgment 10); the cloud #2931 extension spelling, unmeasurable here and carried by the cloud removal card.

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: claude/issue-24129-blueprint-view-filters-sort
Reviewed-by: session_01HSDGREvqxQUt7K7pjHXMyC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 05:34
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 17, 2026 05:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 9f024d8 Oct 17, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24129-blueprint-view-filters-sort branch October 17, 2026 06:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ai size/m tests tooling

Projects

None yet

2 participants