Repository navigation
feat(spec,lint): a date or datetime field declares its deadline: dueLike and settledWhen - #22503
Conversation
The deadline semantic on the field definition: `dueLike` declares the date a deadline, `settledWhen` is a per-record CEL predicate in the visibleWhen family that settles it. Both refused off the date/datetime types, and settledWhen refused without dueLike: true. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…n the field-rule pass (wip) Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
… liveness, FLS and expression ledgers (wip) Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…s; teach dueLike and settledWhen; changeset (wip) Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…e two deadline rows, generator output unedited (wip) Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…and es-ES (wip) Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…ledWhen; type the form-row read in the pin (wip) Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…65 to 667 for the two deadline rows (wip) Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 140 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 5a3bceee79af297af99c0de01e45217e93219840 && git checkout 5a3bceee79af297af99c0de01e45217e93219840
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 446c8b2a6420a61a2862e6f5140dda71a53316d1 c73e779f9a04e78461220274f2d0b5356f1d99aa && git checkout -B drift-repro 446c8b2a6420a61a2862e6f5140dda71a53316d1 && git merge --no-ff c73e779f9a04e78461220274f2d0b5356f1d99aa
node scripts/docs-audit/affected-docs.mjs --json 446c8b2a6420a61a2862e6f5140dda71a53316d1
|
Contract reviewServed-tier: Inputs: card #22227 (body and all four comments, the dev report included), PR #22503 (body, 24-file list, the net diff against ① Derived judgmentsThe accept set of
② Semver level
③ Boundary flags
Prescription for the next head. Add Check-runs on this head, read at 2026-10-09T15:49Z: 34 check-runs, 29 completed (24 success, 2 skipped, 3 failure: Implemented-by: VERDICT: FAIL |
…TORAGE_CLASS; settledWhen joins the docs gate's field-rule slots and the consumer census's behaviour segments FIELD_KEY_STORAGE_CLASS is pinned against Object.keys(FieldSchema.shape) (builtin-column-collision.test.ts): both keys are presentation, read by the renderer's date cells and never by createColumn. The docs-corpus gate now root-judges a settledWhen example (slot list and text tripwire together, three self-test cases), and validate-field-consumers counts a settledWhen read as behaviour beside its three siblings. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…-level properties; the field-level alias verdict covers settledWhen protocol.meta-types-degenerate-derivation.test.ts holds the per-type served property counts as a positive control; `field` moves 75 to 77 with the two declared keys, the `picklist` precedent. The #6585 user-alias table over the field-rule slots gains settledWhen, which binds the same roots. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…s as four, settledWhen included Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22227 (body and all six comments: the claim The four commits since ① Derived judgmentsThe accept set of
② Semver level
③ Boundary flags
Check-runs on this head, read at 2026-10-09T17:29Z: 35 check-runs, all 35 completed — 33 success, 2 skipped ( Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22227
Clause-②: yes
The protocol half of objectstack-ai/objectui#11815's ruling D (declared-only overdue). The maintainer's words there: 「每个状态不是可以自己配置颜色吗?done 配置为红色不是元数据的 bug 吗?为什么平台要兜一圈」, then 「同意」 to option D.
What
On the
dateanddatetimefield definitions (packages/spec/src/data/field.zod.ts):dueLike: boolean(optional, no default) declares the date a deadline. Absent means not a deadline, and nothing is inferred from the field's name.settledWhen(optional,EvaluatedExpressionInputSchema, the same schema asvisibleWhen/readonlyWhen/requiredWhen) is a per-record CEL predicate. While it holds, the deadline is settled.Declared = enforced (ADR-0049), at two seams:
FieldSchemasuperRefine, themaxLength/valueDomain/rowstemplate)dueLikeon any type outsideDEADLINE_FIELD_TYPES(date,datetime),settledWhenon any such type, andsettledWhenunlessdueLike: truefield.zod.tsos build,os validate, the object save door)settledWhenthat does not parse, reads a bare field, names an undeclared field, or reads a root the field-rule family does not bindpackages/lint/src/validate-expressions.ts, the field-rule passNo select option carries a terminal marker. Nothing changes in objectui here: objectui#11815 is
Blocked-by:this card and reads the keys after this spec publishes.Mechanism hypotheses, measured
git grep -n "dueLike\|settledWhen"over this tree atdee7692f0bfinds 3 hits, allCHANGELOG.mdprose (packages/console,packages/platform-objects,packages/spec), and no source. The view schemas refuse the key:ListColumnSchema.safeParse({ field: 'due_date', dueLike: true })answersunrecognized_keyswith keysdueLike, while the control{ field: 'due_date' }parses. So no spec-validated source can spell it, and no ADR-0087 entry is added. The changeset declares no break, andcheck-adr-0087-registrationjudges only breaking declarations (breakingDeclaration: "wideningadds NO signal"). It exits 0.FIELD_RULE_BOUND_ROOTS=record,previous,parent.today()is a stdlib function in every CEL env, not a scope name, so it applies.valueis not a root inSCOPE_ROOTS, so it reads as a bare field reference and is refused.settledWhenjoins the same loop asvisibleWhenand gets exactly that verdict. No new scope name is added. The pins: the three bound roots are admitted,current_usergets the family's root verdict namingsettledWhen,today()passes, andvalueis refused. LikevisibleWhen, the slot is display only. The server-side gates further down (theparentgate, the null guard, the traversal refusal) stay withrequiredWhen/readonlyWhen.field.zod.tsscopes a type-specific key with a superRefine over aReadonlySetof types (BOUNDED_STRING_FIELD_TYPES,VALUE_DOMAIN_FIELD_TYPES,MULTILINE_EDITOR_FIELD_TYPES), not with a discriminated variant.DEADLINE_FIELD_TYPESfollows it, sodueLikeon atextfield is a locatedcustomissue at[dueLike].showcase_task: status options includedone, sosettledWhen: record.status == 'done'. app-todotodo_task:completed(also the app's own documented overdue definition,due_datepast ANDstatusnotcompleted). crmcrm_activity:completed. All three follow the card.Files beyond the claim's declared surface (each one demanded by a gate)
The claim fenced the surface to
field.zod.tsand its tests, the lint field-rule gate and its tests, generated baselines and forms, the docs page, the three example objects and one changeset. A newFieldSchemakey and a new CEL slot are also counted by these classification ratchets. Each one is red until the key is classified:packages/spec/liveness/field.json(check:livenessUNCLASSIFIED) and its generatedstate-counts/field.md.dueLikeislivecross-repo: objectuiresolveDueLikereads it, and the detail surfaces copy it throughENRICHED_FIELD_METADATA_KEYS, at main55e90fdand at the.objectui-shapinf0268ad78.settledWhenisplanned: no reader in either repo yet, with noauthorWarn.packages/qa/dogfood/test/expression-conformance.ledger.ts: a newsettledWhenCEL slot is unclassified otherwise. The new row isexperimentalandunevaluated, and it says NO EVALUATOR FOUND.packages/metadata-core/src/object-schema-fls-references.ts:FIELD_REFERENCE_POSITIONSis pinned against the liveFieldSchemashape.dueLikeiskeepandsettledWhenisexpression.packages/lint/src/validate-predicate-path-refs.test.ts: the shipped-form predicate census moves 85 → 87 for the two new form rows.packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts: generator output plus six hand-authored leaves, in two commits. The*.source-hashes.generated.tsfiles are byte-identical to base.object-lifecycle-panel-echo-decisions.test.tsmoves its translated-label control 665 → 667.packages/drivers/driver-sql/src/builtin-column-collision.ts(the tenth ratchet),packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts(the eleventh),packages/lint/scripts/check-doc-formula-expressions.mjsandpackages/lint/src/validate-field-consumers.ts(the field-rule family lists).None of them is touched by any of the 12 open PRs (file lists read while this PR was being built).
Patch round 1 (HEAD
c73e779f9a; REWORK6084352738on #22227, contract review FAIL6084330967, then PASS6085939799)a520b4eea5and fixed:driver-sqlFIELD_KEY_STORAGE_CLASSclassifiesdueLikeandsettledWhenaspresentation(c064f4b335);metadata-protocolprotocol.meta-types-degenerate-derivation.test.tsserves 77fieldproperties, up from 75 (d0af72c6f8).settledWhenjoins the field-rule family lists:FIELD_RULE_SLOTS/FIELD_RULE_TEXT_REincheck-doc-formula-expressions.mjs(three self-test cases; ablation turns exactly those red),BEHAVIOUR_SEGMENTSinvalidate-field-consumers.ts, and the 字段级*When的用户根拒绝只认current_user—— ADR-0068 的两个别名user/ctx.user静默放行,写哪个拼写决定拿不拿得到诊断 #6585 user-alias table.origin/main2e10c9abe0merged (23c192bfad), clean; no regeneration owed. PR 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 (protocol 18) landed after it; this PR adds no ADR-0087 entry, so no re-sync is owed.c73e779f9a: metadata-protocol 223 files / 28,347; driver-sql 218 / 3,635; lint 130 / 5,958; metadata-core 17 / 387; service-messaging 48 / 534; platform-objects 68 / 1,078; spec local 631 / 18,829, repo 54 / 915; objectql local 391 / 7,716; cli unit 274 / 4,053; the three example apps' suites andvalidategreen. 118 derived gate families run, 0 not measured. CI: 35 runs, 33 success, 2 expected skips.Tests, round 0 (HEAD
a520b4eea5, all locked runs throughos-verify-lock)@objectstack/spec,--project local: 631 files, 18824 passed, 1 todo.--project repo: 54 files, 915 passed. Both ran at832743b836; the one later commit touches only aplatform-objectstest.@objectstack/lint130 files / 5956 ·@objectstack/metadata-core17 / 421 ·@objectstack/objectql(--project local) 390 / 7698 ·@objectstack/cli(--project unit) 274 / 4037 · example-crm 5 / 45 · example-todo 7 / 238 · example-showcase 33 / 408 · dogfoodexpression-conformance.test.ts7 / 7. All green at832743b836.@objectstack/platform-objects68 files / 1078 passed ata520b4eea5.objectstack validatepasses for all three example apps.typecheckis green for spec (includingcheck:test-typecheck, which caught one error in the new pin before this head), lint, metadata-core, platform-objects, dogfood and the three example apps, ata520b4eea5.@objectstack/cliintegration tier: not run locally, declared to CI. The diff touches no spawn entry.Ablation, three runs, each committed first and mutated through
scripts/ablation-replace.mjs. Each anchor hit once, the blob changed, and each restore was proven with blob == HEAD and an emptygit diff HEAD. The subjects resolve tosrcthrough relative imports, so nodistleg applies.settledWhendropped from the lint field-rule loopcurrent_user,value, object door, slot list), 386 greendueLike !== truebranch →falsefalse, ondateanddatetime)dueLiketype refusal →falsedueLike: falseoff-type)All three turned red, which was the expected direction.
Gates:
dispatch-gates --commandsre-derived on the actual diff (--repo objectstack-ai/objectstack) is unioned with the dispatched list: 119 commands, all exit 0 ata520b4eea5. The re-derivation added 10 families:check:generated,check:engine-double-contract,check:i18n,check:i18n-stale-fill,check:objectql-double-limit,check:platform-checklist,check:quick-reference-counts,check:type-check-coverage,check:type-check-debtandcheck:where-matcher.dispatch-gates --ranreports 114 derived families, 114 run, 0 NOT-MEASURED, all with a recorded exit code.Acceptance notes
crm_activityhas a second finished state.cancelledexists besidecompleted. Under the ruled predicate a cancelled activity past due keeps the overdue affordance.record.status in ['completed', 'cancelled']is a one-line change if that is wanted; the card's text was followed. app-todo'sdeferredis in the same position, and the app's own overdue definition counts onlycompleted.previous/parenton a display predicate. The family's bound roots are admitted forsettledWhenas they are forvisibleWhen. A display-row evaluator may have neither bound. That is objectui#11815's to honour or a later narrowing card's to decide (carrier: objectui#11815).dueLikereaches the cell only on the detail surfaces today. objectui's grid copy set does not carry it, and objectui still also guesses due-likeness from the field name. Both retire with objectui#11815 (carrier: objectui#11815).settledWhenis a 35th evaluated-expression position.packages/spec/src/shared/evaluated-slot-population.test.tshand-lists 34 (pinnedtoBe(34)), andevaluated-slot-union.ts:10says 34. No gate is red, and the three-spelling refusal holds by composition. Carrier: spec(ui): field-level conditionalFormatting — a field declares[{ condition, style }]cell rules (CEL overvalueandrecord), the row block's own grammar, for presentational rules that carry no semantic (maintainer-directed) #22228, which adds the next evaluated position on the field definition and adds both rows (contract review6085939799③).Generated by Claude Code