Skip to content

feat(spec,lint): a date or datetime field declares its deadline: dueLike and settledWhen - #22503

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-22227-date-due-like
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-22227-date-due-like

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

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 date and datetime field 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 as visibleWhen / readonlyWhen / requiredWhen) is a per-record CEL predicate. While it holds, the deadline is settled.

Declared = enforced (ADR-0049), at two seams:

Door What it refuses Where
parse (FieldSchema superRefine, the maxLength / valueDomain / rows template) dueLike on any type outside DEADLINE_FIELD_TYPES (date, datetime), settledWhen on any such type, and settledWhen unless dueLike: true field.zod.ts
authoring CEL gate (os build, os validate, the object save door) a settledWhen that does not parse, reads a bare field, names an undeclared field, or reads a root the field-rule family does not bind packages/lint/src/validate-expressions.ts, the field-rule pass

No 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

  • H1: is there a legacy spelling to convert? No. git grep -n "dueLike\|settledWhen" over this tree at dee7692f0b finds 3 hits, all CHANGELOG.md prose (packages/console, packages/platform-objects, packages/spec), and no source. The view schemas refuse the key: ListColumnSchema.safeParse({ field: 'due_date', dueLike: true }) answers unrecognized_keys with keys dueLike, 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, and check-adr-0087-registration judges only breaking declarations (breakingDeclaration: "widening adds NO signal"). It exits 0.
  • H2: the CEL scope. The field-rule family binds exactly FIELD_RULE_BOUND_ROOTS = record, previous, parent. today() is a stdlib function in every CEL env, not a scope name, so it applies. value is not a root in SCOPE_ROOTS, so it reads as a bare field reference and is refused. settledWhen joins the same loop as visibleWhen and gets exactly that verdict. No new scope name is added. The pins: the three bound roots are admitted, current_user gets the family's root verdict naming settledWhen, today() passes, and value is refused. Like visibleWhen, the slot is display only. The server-side gates further down (the parent gate, the null guard, the traversal refusal) stay with requiredWhen / readonlyWhen.
  • H3: type scoping idiom. field.zod.ts scopes a type-specific key with a superRefine over a ReadonlySet of types (BOUNDED_STRING_FIELD_TYPES, VALUE_DOMAIN_FIELD_TYPES, MULTILINE_EDITOR_FIELD_TYPES), not with a discriminated variant. DEADLINE_FIELD_TYPES follows it, so dueLike on a text field is a located custom issue at [dueLike].
  • H4: the example objects' real status values. showcase showcase_task: status options include done, so settledWhen: record.status == 'done'. app-todo todo_task: completed (also the app's own documented overdue definition, due_date past AND status not completed). crm crm_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.ts and 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 new FieldSchema key 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:liveness UNCLASSIFIED) and its generated state-counts/field.md. dueLike is live cross-repo: objectui resolveDueLike reads it, and the detail surfaces copy it through ENRICHED_FIELD_METADATA_KEYS, at main 55e90fd and at the .objectui-sha pin f0268ad78. settledWhen is planned: no reader in either repo yet, with no authorWarn.
  • packages/qa/dogfood/test/expression-conformance.ledger.ts: a new settledWhen CEL slot is unclassified otherwise. The new row is experimental and unevaluated, and it says NO EVALUATOR FOUND.
  • packages/metadata-core/src/object-schema-fls-references.ts: FIELD_REFERENCE_POSITIONS is pinned against the live FieldSchema shape. dueLike is keep and settledWhen is expression.
  • 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.ts files are byte-identical to base. object-lifecycle-panel-echo-decisions.test.ts moves its translated-label control 665 → 667.
  • Patch round 1, four more, each named by the REWORK: 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.mjs and packages/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; REWORK 6084352738 on #22227, contract review FAIL 6084330967, then PASS 6085939799)

Tests, round 0 (HEAD a520b4eea5, all locked runs through os-verify-lock)

  • @objectstack/spec, --project local: 631 files, 18824 passed, 1 todo. --project repo: 54 files, 915 passed. Both ran at 832743b836; the one later commit touches only a platform-objects test.
  • @objectstack/lint 130 files / 5956 · @objectstack/metadata-core 17 / 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 · dogfood expression-conformance.test.ts 7 / 7. All green at 832743b836.
  • @objectstack/platform-objects 68 files / 1078 passed at a520b4eea5.
  • objectstack validate passes for all three example apps.
  • typecheck is green for spec (including check:test-typecheck, which caught one error in the new pin before this head), lint, metadata-core, platform-objects, dogfood and the three example apps, at a520b4eea5.
  • @objectstack/cli integration 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 empty git diff HEAD. The subjects resolve to src through relative imports, so no dist leg applies.

Mutation Result
settledWhen dropped from the lint field-rule loop 7 red (parse, bare reference, unknown field, current_user, value, object door, slot list), 386 green
the dueLike !== true branch → false 4 red (absent or false, on date and datetime)
the dueLike type refusal → false 9 red (8 types, plus dueLike: false off-type)

All three turned red, which was the expected direction.

Gates: dispatch-gates --commands re-derived on the actual diff (--repo objectstack-ai/objectstack) is unioned with the dispatched list: 119 commands, all exit 0 at a520b4eea5. 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-debt and check:where-matcher. dispatch-gates --ran reports 114 derived families, 114 run, 0 NOT-MEASURED, all with a recorded exit code.

Acceptance notes

  • crm_activity has a second finished state. cancelled exists beside completed. 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's deferred is in the same position, and the app's own overdue definition counts only completed.
  • previous / parent on a display predicate. The family's bound roots are admitted for settledWhen as they are for visibleWhen. 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).
  • dueLike reaches 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).
  • settledWhen is a 35th evaluated-expression position. packages/spec/src/shared/evaluated-slot-population.test.ts hand-lists 34 (pinned toBe(34)), and evaluated-slot-union.ts:10 says 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 over value and record), 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 review 6085939799 ③).

Generated by Claude Code

claude added 8 commits October 9, 2026 12:56
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>
…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>
@github-actions github-actions Bot added the size/l label Oct 9, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/driver-sql, @objectstack/lint, @objectstack/metadata-core, @objectstack/platform-objects, @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json, packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 446c8b2a6420a61a2862e6f5140dda71a53316d1.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json, packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md) — pages documenting those are invisible to this run
  • 1 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 — 140 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 446c8b2a6420a61a2862e6f5140dda71a53316d1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5a3bceee79af297af99c0de01e45217e93219840 — the merge of head c73e779f9a04e78461220274f2d0b5356f1d99aa into base 446c8b2a6420a61a2862e6f5140dda71a53316d1, 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 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

⚠️ 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 446c8b2a6420a61a2862e6f5140dda71a53316d1 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a520b4eea5bc833531f48293ea6d40de046c6481
Local-runs: none

Inputs: card #22227 (body and all four comments, the dev report included), PR #22503 (body, 24-file list, the net diff against main at the merge-base dee7692f0b), and the head's check-runs. Read-only: no worktree, no build, no test, no gate re-run. The settledWhen describe's P before the backtick is the repo's predicate tagged-template convention (its siblings spell it the same), not a typo.

① Derived judgments

The accept set of FieldSchema (packages/spec/src/data/field.zod.ts, a strictObject) widens by exactly two optional keys. Each derived judgment, named right or wrong:

  1. The two keys. dueLike: z.boolean().optional() and settledWhen: EvaluatedExpressionInputSchema.optional(), the envelope visibleWhen / readonlyWhen / requiredWhen use; a bare string normalises to { dialect: 'cel', source } (pinned). Neither has a default, so a field without them parses byte-identically ('dueLike' in result.data is false, pinned). RIGHT.
  2. Refusal: either key off date / datetime. A superRefine over DEADLINE_FIELD_TYPES, the maxLength / valueDomain / rows idiom; one custom issue at [dueLike] or [settledWhen], the message enumerating the set itself; time and formula excluded with a stated reason; eight wrong types pinned. dueLike: false off-type is refused too, which is right under ADR-0049 (an authored value on a type it describes nothing on). RIGHT.
  3. Refusal: settledWhen without dueLike: true (absent or false) on a date / datetime. RIGHT, as the card's text rules. Off-type settledWhen gets the type refusal alone, not the pairing one, so the author is not sent into a second refusal. RIGHT.
  4. No existing input newly refused. FieldSchema is strict, so neither key parsed before (unrecognized_keys); both superRefine branches fire only on !== undefined; the no-keys control is pinned. The published accept set widens by the two keys and nothing else. RIGHT.
  5. The CEL scope gate (packages/lint/src/validate-expressions.ts). settledWhen is the fifth member of the field-rule loop, judged as visibleWhen is: it parses, reads record.FIELD and never a bare field, names a declared field, and gets the root verdict over FIELD_RULE_BOUND_ROOTS (record, previous, parent); current_user is refused with the slot named; today() passes as a stdlib function; value reads as a bare reference and is refused. The write-path gates stay off it, read at the head: the parent gate and the A field-level requiredWhen / readonlyWhen that reads through a lookup (record.account.tier) is accepted at authoring, but the runtime never hydrates it, so since ADR-0137 D2 every write that reaches it is refused #20078 traversal refusal iterate literal readonlyWhen / requiredWhen, and the null guard is requiredWhen only. The validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 meta-test moves to the five-slot list; FIELD_RULE_SLOT_CONSEQUENCE.settledWhen takes the generic clause (no runtime to measure). RIGHT, and as the card ruled, "as for its siblings".
  6. The form (field.form.ts): two rows gated data.type in ['date','datetime'] and ... && data.dueLike == true, so each key is offered exactly where it parses. RIGHT.
  7. Generated surface. authorable-surface/data.json gains data/Field:dueLike and data/Field:settledWhen; the reference pages field.mdx / object.mdx / migration.mdx carry the describe text. Type Check · source gates (which runs check:authorable-surface and check:docs) is success on this head. api-surface/ and json-schema.manifest/ are unchanged, rightly: DEADLINE_FIELD_TYPES is module-local and nothing new is exported. RIGHT.
  8. The nine classification ratchets past the claim's surface. Each is a forced consequence of a new FieldSchema key or CEL slot, and each classification holds:
    • packages/spec/liveness/field.json: forced (check:liveness refuses an unclassified key; Spec property liveness is success on the head). dueLike live, cross-repo: verified in the objectui checkout, packages/fields/src/index.tsx resolveDueLike and packages/plugin-detail/src/fieldEnrichment.ts ENRICHED_FIELD_METADATA_KEYS carry the key at objectui origin/main 55e90fd and at the .objectui-sha pin f0268ad78. settledWhen planned, cross-repo: no reader in either repo (objectui 55e90fd has no settledWhen), the two in-repo doors cited as anchors; authorWarn is optional on a planned row and forbidden only on a live one. RIGHT.
    • liveness/state-counts/field.md: generator output; 94 + 0 + 0 + 1 + 1 = 96. RIGHT.
    • packages/qa/dogfood/test/expression-conformance.ledger.ts: forced (the ratchet discovers EvaluatedExpressionInputSchema positions by schema walk). experimental with unevaluated; the cell states NO EVALUATOR FOUND and PARSE ONLY and names no site the detector regex knows. RIGHT.
    • packages/metadata-core/src/object-schema-fls-references.ts FIELD_REFERENCE_POSITIONS: forced (the metadata-core pin classifies every FieldSchema key). dueLike: keep, settledWhen: expression, scrubbed when it names a denied field, which fails toward "still overdue". RIGHT.
    • packages/lint/src/validate-predicate-path-refs.test.ts 85 to 87: forced by the two form rows' gates. RIGHT.
    • The four LOCALE.metadata-forms.generated.ts catalogs: bd5da7ec03 is the extract's fill (the en text copied into all four, source-hash entries added for the three copies); 6e59d0db25 replaces the three leaves by hand and the hash entries drop, which is the file header's own workflow (edit only the leaf string values); net source-hashes byte-identical to base. Generator output where it claims to be, hand-authored leaves where the generator says to author them. RIGHT.
    • object-lifecycle-panel-echo-decisions.test.ts 665 to 667: forced by two labels in three locales. RIGHT.
  9. WRONG: a tenth ratchet the diff does not render, and the head is red on it. packages/drivers/driver-sql/src/builtin-column-collision.ts FIELD_KEY_STORAGE_CLASS is a hand-maintained map, pinned against Object.keys(FieldSchema.shape) by builtin-column-collision.test.ts lines 44 to 51 ("classifies EVERY FieldSchema key, and invents none"). The diff adds two keys to FieldSchema and does not touch the map. Read off the diff, then confirmed by the head's own check-runs: the Temporal Conformance (live PG + MySQL) check-run annotation reads src/builtin-column-collision.test.ts:49 AssertionError: unclassified FieldSchema key(s): expected [ 'dueLike', 'settledWhen' ] to deeply equal [], and Test Core (3/6) fails at the same package (packages/drivers/driver-sql pnpm run test exited 1). Both are required contexts; both are success on main at e148ca98 (three runs today) and at the merge-base, so the red is this head's. The classification is forced and obvious, two lines: dueLike: 'presentation' and settledWhen: 'presentation', neither read by the DDL, display only like visibleWhen. The conditionalRequired liveness note the dev edited beside names this very map. The dev's test list carries no @objectstack/driver-sql run, and dispatch-gates --ran counts gate families, not the affected package suites Test Core shards.
  10. Test Core (5/6) is also red, at packages/metadata-protocol pnpm run test (exit 1). Its assertion is not readable while the run is in progress (job logs are withheld until the run completes, and the check-run carries only the process-level annotations). The shard is success on main and at the merge-base, so it is the head's own; no metadata-protocol test I read pins a FieldSchema key count or a form-row count (the one served-property pin is action at 50), so whether this is a second ratchet or something else, the completed log decides. Not judged green.

② Semver level

  • Clause-②: yes sits on PR line 2 and in the changeset body, identically; a bare yes reads as declared with no arm, which is a widening (readClause2Line). RIGHT.
  • @objectstack/spec minor: a widening takes at least minor. RIGHT. @objectstack/lint minor: the field-rule pass judges a fifth slot, new behaviour on a published package; minor is right, patch would understate it. @objectstack/metadata-core (FLS positions) and @objectstack/platform-objects (catalogs) also change and carry no line, which is fine: all four sit in the fixed group of .changeset/config.json and version together. Check Changeset is success.
  • ADR-0087: no disposition is owed on a widening. check-adr-0087-registration's breakingDeclaration reads major, BREAKING, bang and the narrowing arm only; a widening adds no signal. No legacy spelling to convert: the view schemas are strict (ListColumnSchema refuses dueLike), and the dueLike liveness row records objectui#9738, objectui-owned until the protocol claims it, which this card is. RIGHT.
  • The semver verdict stands on its own; it is not what fails this record.

③ Boundary flags

  • open_questions[0], crm_activity cancelled: ANSWERED, A. The card's ruled text names status == 'completed' for crm activity, and under ruling D the predicate is the example app's own authoring (no terminal marker; the deadline declares when it is settled), so B is a one-line example change with no effect on the published accept set. Not escalated. Carry it to objectui#11815's landing, where the CRM grid first paints the affordance and the maintainer can read cancelled as settled or not; app-todo's deferred stays unsettled by that app's own overdue definition, which counts only completed.
  • previous / parent admitted on a display predicate: ANSWERED. As ruled ("as for its siblings"); objectui#11815 honours the scope or files a narrowing. Carrier named.
  • dueLike reaches only the detail surfaces today, the grid copy set and the name guess retire later: ANSWERED, objectui#11815's.
  • Not merged with origin/main: ANSWERED. 11 commits since dee7692f0b, zero path overlap measured over the two name lists against e148ca98; the queue judges the joint tree.
  • check-doc-formula-expressions.mjs FIELD_RULE_SLOTS does not root-judge a settledWhen docs example, and fields.mdx now carries one: ANSWERED. A docs-gate reach gap on a prose face, not a contract defect. "Carrier: none" is not a carrier: the adopting seat files a small card for it together with the advisory validate-field-consumers.ts BEHAVIOUR_SEGMENTS note.
  • The nine-file surface breach: ANSWERED, accepted, each row forced and judged in ①; but the breach list was short by one, item 9 above, which is the FAIL.
  • Prose faces (the fields.mdx teaching section, the changeset body) sit outside this review's three faces; the dispatch seat reads them at ACCEPT. Noted in passing: the fields.mdx section matches the schema's behaviour as read.

Prescription for the next head. Add dueLike: 'presentation' and settledWhen: 'presentation' to FIELD_KEY_STORAGE_CLASS in packages/drivers/driver-sql/src/builtin-column-collision.ts, run the driver-sql suite, and read the Test Core (5/6) log from the completed run. That head is not a pure regeneration, so it owes a new ## Contract review record; the contract halves above (①1 to ①8, ②, ③) can be adopted on it as read, with only item 9 and item 10 re-judged.

Check-runs on this head, read at 2026-10-09T15:49Z: 34 check-runs, 29 completed (24 success, 2 skipped, 3 failure: Test Core (3/6), Test Core (5/6), Temporal Conformance (live PG + MySQL)), 5 in progress (Test Core 1/6, 2/6, 4/6, 6/6 and Lint & Repo Gates). Not waited for; nothing in progress is judged green.

Implemented-by: claude/issue-22227-date-due-like
Reviewed-by: session_01VZqqwTj2wsihZEbfT6yyYN

VERDICT: FAIL

claude added 4 commits October 9, 2026 15:53
…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c73e779f9a04e78461220274f2d0b5356f1d99aa
Local-runs: none

Inputs: card #22227 (body and all six comments: the claim 6081172569, both dev reports 6084029019 and 6085690999, the seat's REWORK 6084352738), PR #22503 (body, the 28-file list, its comments including the first record FAIL 6084330967, and the net diff against main at the merge-base 2e10c9abe0), and the head's check-runs. Read-only: no worktree, no build, no test, no gate re-run. Second record on this PR; re-verified from the diff rather than inherited — every item below was read at this head.

The four commits since a520b4eea5: c064f4b335 (the driver-sql classification; the docs gate's slot list and text tripwire with three self-test cases; BEHAVIOUR_SEGMENTS), 23c192bfad (merge of origin/main 2e10c9abe0 — the combined diff is empty, so no hunk was hand-resolved, and the file lists of dee7692f0b..2e10c9abe0 and this PR share no path), d0af72c6f8 (metadata-protocol field 75 to 77; the #6585 alias table gains settledWhen), c73e779f9a (a docblock count).

① Derived judgments

The accept set of FieldSchema (packages/spec/src/data/field.zod.ts, strict) widens by exactly two optional keys and nothing else. Each derived judgment, named right or wrong:

  1. The two keys. dueLike: z.boolean().optional() and settledWhen: EvaluatedExpressionInputSchema.optional(), the envelope visibleWhen / readonlyWhen / requiredWhen compose; a bare string normalises to { dialect: 'cel', source } (pinned). No default on either, so a field without them parses byte-identically ('dueLike' in result.data is false, pinned). RIGHT.
  2. Refusal of either key off date / datetime. One superRefine over DEADLINE_FIELD_TYPES (the maxLength / valueDomain / rows idiom), one custom issue at [dueLike] or [settledWhen], the message enumerating the set itself; time and formula excluded with a stated reason; eight wrong types pinned, and dueLike: false off-type refused too (ADR-0049). RIGHT.
  3. Refusal of settledWhen without dueLike: true (absent or false) on a deadline type; off-type settledWhen gets the type refusal alone, so the author is not sent into a second refusal. RIGHT, as the card's text rules.
  4. No existing input newly refused. Both branches fire only on !== undefined, and FieldSchema is strict, so neither key parsed before. RIGHT.
  5. settledWhen composes the evaluated schema, so it refuses an ast-only envelope and a blank source with the family's one sentence, and a blank bare string (pinned in field-due-like.test.ts as invalid_union; a non-CEL value shape pinned too). RIGHT — and see item 16 for the census this slot is missing from.
  6. The CEL scope gate (packages/lint/src/validate-expressions.ts). settledWhen is the fifth member of the field-rule loop, judged as visibleWhen is: it parses, reads record.FIELD and never a bare field, names a declared field, and gets the root verdict over FIELD_RULE_BOUND_ROOTS (record, previous, parent); current_user is refused naming the slot; today() passes as a stdlib function; value reads as a bare reference; the object save door (runtimeWriteType: 'object') judges it; the write-path gates (the parent gate, the null guard, the A field-level requiredWhen / readonlyWhen that reads through a lookup (record.account.tier) is accepted at authoring, but the runtime never hydrates it, so since ADR-0137 D2 every write that reaches it is refused #20078 traversal refusal) iterate literal readonlyWhen / requiredWhen and stay off it; FIELD_RULE_SLOT_CONSEQUENCE.settledWhen takes the generic clause; the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 meta-test pins the five-slot list. All pinned at the head. RIGHT.
  7. The form (field.form.ts): two rows, gated data.type in ['date','datetime'] and ... && data.dueLike == true, settledWhen as type: 'code', language: 'expression'. Each key offered exactly where it parses; the spec's own pin reads the gates off the rows. RIGHT.
  8. Generated surface. authorable-surface/data.json gains data/Field:dueLike and data/Field:settledWhen (75 to 77 data/Field: rows, counted at base and head); the reference pages carry the describe text; api-surface/ and json-schema.manifest/ unchanged, rightly (DEADLINE_FIELD_TYPES is module-local and nothing new is exported). Type Check · source gates and Lint & Repo Gates success on the head. RIGHT.
  9. The nine classification ratchets of record one (the liveness rows and counts 94 + 1 + 1 = 96, the dogfood ledger row experimental / unevaluated, FIELD_REFERENCE_POSITIONS keep / expression, the form-predicate census 85 to 87, the four locale catalogs with three hand-authored leaves and byte-identical source hashes, the echo-decisions control 665 to 667): re-read at the head, each forced and each classification holds. RIGHT.
  10. The tenth, FIELD_KEY_STORAGE_CLASS (packages/drivers/driver-sql/src/builtin-column-collision.ts): both keys presentation. Judged against the module's own line: storage is a key the SQL driver's DDL layer would have read to shape the physical column or its indexes; presentation is a key honoured by some other layer (metadata, i18n, the engine's write contract, the UI), the line "drawn at what the DDL reads, not at what the key feels like". A grep of dueLike / settledWhen over packages/drivers/ at the head hits the map and nothing else: createColumn and syncTableIndexes read neither; the readers are the renderer (dueLike, objectui's date cells) and nobody yet (settledWhen, planned). Both presentation, like visibleWhen. The pin (builtin-column-collision.test.ts:44, every FieldSchema key classified and none invented) is green: Test Core (3/6) and Temporal Conformance (live PG + MySQL) are success on this head and were failure on a520b4eea5. RIGHT.
  11. The eleventh, the served-property control (packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts): field 75 to 77. The control counts the served /meta/types document's top-level properties plus the retired tombstones (retiredTopLevelCount); two declared keys move it by exactly two, the picklist precedent the docblock cites and now extends. The independent count in item 8 agrees. Test Core (5/6) is success on this head and was failure on a520b4eea5; the red was this PR's, not main's. RIGHT.
  12. settledWhen joins FIELD_RULE_SLOTS and FIELD_RULE_TEXT_RE in packages/lint/scripts/check-doc-formula-expressions.mjs. Both lists together, as the file's own rule says (the text tripwire reconciles against the parse); three self-test cases (RED bare reference, RED current_user root verdict, GREEN the fields.mdx example), and fields.mdx is not on the skip list. Lint & Repo Gates success. RIGHT.
  13. BEHAVIOUR_SEGMENTS in packages/lint/src/validate-field-consumers.ts. Read against bucketFor: a settledWhen site under an objects root was already behaviour by the fall-through, so the entry changes no verdict today; it would matter only for a settledWhen segment under a display root, which no shape carries. Consistent with its three siblings; no pin exists for the set and none is owed. RIGHT.
  14. The 字段级 *When 的用户根拒绝只认 current_user —— ADR-0068 的两个别名 user / ctx.user 静默放行,写哪个拼写决定拿不拿得到诊断 #6585 user-alias rows (validate-expressions.test.ts:1220): settledWhen binds the same roots, so the user / ctx.user verdict covers it; two new cases. RIGHT.
  15. Census of every pin over the FieldSchema key set, re-run by grep at the head over packages/ and scripts/: cli/test/data-model-rules.test.ts:737 (membership), validate-security-posture.test.ts (subset), field.test.ts:1018 (above 50), field-autonumber-default-unique.test.ts:91 (order), ui/component.zod.ts (borrows named keys). None an added key turns red; the dev's census agrees. No twelfth key-set ratchet.
  16. Census of the field-rule slot lists, re-run the same way: the loop (:2534), the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 pin, the 字段级 *When 的用户根拒绝只认 current_user —— ADR-0068 的两个别名 user / ctx.user 静默放行,写哪个拼写决定拿不拿得到诊断 #6585 table, the docs gate's two lists, BEHAVIOUR_SEGMENTS, the :1905 three-slot causal-sentence test (deliberately the three MEASURED slots, since settledWhen takes the generic clause as conditionalRequired does — right), component-props-unknown-members.pin.test.ts:268 (view form-field paths, not FieldSchema), the objectql rule-validator and erased-authoring-mark.ts (prose). ONE list the dev's census does not name: packages/spec/src/shared/evaluated-slot-population.test.ts, the hand table POSITIONS of every evaluated-expression declaring position in the spec, pinned toBe(34) and reaching slot(FieldSchema, 'visibleWhen'), 'readonlyWhen', 'requiredWhen' by identity, one per line, which a one-line grep for the three names misses. It is not a ratchet: its structural half scans only for the persistence name ExpressionInputSchema, and its behavioural half reaches only what it lists, so a 35th evaluated position (FieldSchema.settledWhen) passes it green. Consequence: the three-spelling refusal with the published sentence EVALUATED_EXPRESSION_SOURCE_REQUIRED is unpinned at the new slot in the suite whose job that is, and the suite's "34" (and the prose "34" in evaluated-slot-union.ts:10) undercounts the live spec by one. Behaviour is right by composition (item 5); the pin is what is missing. Carried to ③.

② Semver level

  • Clause-②: yes sits on PR line 2 and in the changeset body, identically; a bare yes reads as a widening with no arm. RIGHT.
  • @objectstack/spec minor (a widening takes at least minor) and @objectstack/lint minor (the field-rule pass judges a fifth slot: new behaviour on a published package). @objectstack/metadata-core, @objectstack/platform-objects, @objectstack/driver-sql and @objectstack/metadata-protocol change with no line of their own, which is fine: all sit in the fixed group and version together, and the driver-sql and metadata-protocol edits are a classification row and a test. Check Changeset success. RIGHT.
  • ADR-0087: no disposition owed on a widening (check-adr-0087-registration reads only breaking declarations). No legacy spelling to convert: the view schemas are strict and ListColumnSchema refuses dueLike. RIGHT.
  • Protocol 18 and the re-sync question. main has moved past the merge-base 2e10c9abe0 by five commits, 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 (4e9fe9ff6a, PROTOCOL_VERSION 17 to 18) among them. The two file lists share no path. spec-changes.json and the upgrade guide are registry-derived (build-spec-changes.ts reads the protocol version, the migrations registry and api-surface/; build-upgrade-guide.ts reads the conversions and migrations registries); this PR touches none of those inputs and adds no entry. GitHub reports the PR mergeable and clean against current main. No re-sync is owed: a widening with no ADR-0087 entry has nothing for the step-18 regeneration to pick up. Noted for the queue leg: the head's own CI merge ref predates 4e9fe9ff6a (base 446c8b2a64), so check:generated on a tree holding protocol 18 is the queue's reading; by the input analysis above nothing in this diff feeds a protocol-18 artefact. The protocol-18 entry evaluated-expression-slots-source-required names "34 declaring positions", the 17-era population the narrowing converts; settledWhen is born under the rule in the same unpublished major, so no upgrader meets it and the entry owes no edit.

③ Boundary flags

  • crm_activity cancelled (record one's open_questions[0]): ANSWERED, A as shipped, now confirmed by the seat's REWORK. The card's ruled predicate governs; an example-app authoring choice revisited at objectui#11815's landing. Not escalated.
  • previous / parent admitted on a display predicate: ANSWERED, carrier objectui#11815, as before.
  • dueLike reaches only the detail surfaces today, the grid copy set and the name guess retire later: ANSWERED, objectui#11815's.
  • Not merged with origin/main: CLOSED by 23c192bfad, a clean merge.
  • The docs gate's slot list and BEHAVIOUR_SEGMENTS: CLOSED by c064f4b335 (items 12 and 13).
  • The REWORK's census ask (its item 4): answered in full for the key-set pins; for the slot lists it is short by the one named in ①16. ESCALATED to the seat: evaluated-slot-population.test.ts wants one row (data/field.zod.ts:FieldSchema.settledWhen, slot(FieldSchema, 'settledWhen')), its three "34"s moved to 35 with a history line in the table's docblock, and the evaluated-slot-union.ts docblock count — a test-census edit with no published surface in it. Either a patch round on this PR (which moves the head and owes a third record) or a small card filed at ACCEPT; this record does not make that choice. It does not block the contract: no derived judgment is wrong, no check is red, and the slot's refusal is enforced by composition and partly pinned in the spec's own test.
  • The second surface breach (four files named by the REWORK) and the two extra edits (the 字段级 *When 的用户根拒绝只认 current_user —— ADR-0068 的两个别名 user / ctx.user 静默放行,写哪个拼写决定拿不拿得到诊断 #6585 rows, the docblock count): ANSWERED, accepted, each judged in ①.
  • PR body: stale on the three faces the dev listed (the Tests head and counts at a520b4eea5, the beyond-surface list short by four files, two acceptance notes now closed). A prose face for the seat at ACCEPT, outside this review's three faces.
  • open_questions on the second report: none.

Check-runs on this head, read at 2026-10-09T17:29Z: 35 check-runs, all 35 completed — 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure, 0 in progress. Test Core 1/6 to 6/6, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Spec property liveness, Governed Surface Queue Guard and Check Changeset all success; the one legacy status (Vercel) success.

Implemented-by: claude/issue-22227-date-due-like
Reviewed-by: session_01VZqqwTj2wsihZEbfT6yyYN

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 17:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 17:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 215e662 Oct 9, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22227-date-due-like branch October 9, 2026 18:08
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:data size/l tests tooling

Projects

None yet

2 participants