Skip to content

[decision] CEL_STDLIB_FUNCTIONS route: keep 35 as a declared subset, widen to the 39 bare-callables, or rename the symbol #13933

Closed

Description

@zhuangjianguo

Split out of #13831 so the question survives PR #13931's landing. #13931 delivers only the route-independent half — the drift pin #13831 says is owed either way, plus a docblock recording the measured decomposition. It changes the constant's contents not at all. The route itself is this card.

The fork changed shape, so the original framing no longer applies

#13831 posed it as complete registry vs. deliberately advertised subset. Measurement (full detail in #13931) makes the complete-registry reading unreachable in the shape the constant has:

72 registered names
  = 39 callable BARE, as fn(x)
  + 33 callable ONLY on a receiver, as x.fn()

39 bare-callable
  = 27 added by registerStdLib  -> all 27 advertised
  +  8 cel-js built-ins         -> advertised
  +  4 cel-js built-ins         -> WITHHELD: bytes, dyn, type, uint

Two findings reshape the decision:

  1. The card's sub-premise was falsified. The 27 registerFunction sites yield exactly 27 names; 50 of the 72 come from cel-js's own global registrations. "Derive the constant from the registration site" is impossible as stated.
  2. Every consumer spends an entry as a bare call, so a flat list of all 72 is not merely unreachable but wrong: objectui's Studio predicate editor (CelPredicateField.tsx) inserts a chosen suggestion verbatim as `label(`, and the pre-existing runtime pin probes each entry with a bare-call expression. Advertising split or map would autocomplete them into an author's predicate, which faults no matching overload.

⇒ The catalog's real membership rule — never previously written down — is bare-callability. That makes the 37-name gap deliberate rather than stale, and moves the live fork onto the 39 bare-callables, not the 72.

Worth noting for grading: #13831's own example gap list is mostly a category error. Of type map filter exists split substring getFullYear base64 json orValue, nine are receiver-only; only type is bare-callable.

The options

A. Keep 35, and keep it explicitly a subset. What #13931's docblock and pin already assume. The four withheld names are CEL type primitives with no measured demand.

B. Widen to the 39 bare-callables. A four-name addition (bytes, dyn, type, uint), mechanically derivable from Environment.getDefinitions(). Cheap — but it hands an AI author dyn(x) and uint(x).

C. Rename the symbol so its name stops promising completeness. ⛔ A published-surface break on an exported symbol; deliberately not improvised.

Recommendation: A

The implementing seat recommends A and the reasoning holds up:

  • Demand. The only measured consumers are objectui's autocomplete and the MCP validate_expression tool, both served correctly today. Nothing asks for bytes/dyn/type/uint.
  • The defect was an unstated contract, not a wrong number. A stated rule plus an executable pin closes the class; widening closes one instance.
  • Asymmetric cost for AI authoring. Under-advertising costs expressiveness; over-advertising emits faulting code. dyn and uint are precisely the primitives that let a model produce something which type-checks and is unusable.
  • Scope discipline. B is capability expansion with zero measured pull.

C is a maintainer call regardless of A/B.

The second half of the same decision

skills/objectstack-formula/SKILL.md currently says the constant "is the canonical list" and that "Only the functions above are callable." The second claim is measurably false — type(x) resolves and evaluates, as do 33 receiver methods. It is false in the safe direction (it under-promises), so nothing is broken by it today.

It is deliberately not corrected in #13931, for two reasons: the correct wording depends on which route is picked, and skills/** is a governed surface. Recommendation is to fold it into whichever route is chosen rather than file it separately — a separate card could not be graded before this one is.

Refs: #13831 (the finding this splits from) · #13931 (the route-independent pin) · #13594 (the ruling pinning the lint gate's oracle to the engine's own check(), unaffected by any route here)

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Aug 31, 2026
  2. huangyiirene commented on Sep 1, 2026

    @huangyiirene
    Collaborator

    裁决:A —— 35 保持,显式声明为裸调用子集;B/C 均不采;skills 半边同笔修(维护者 2026-09-01,总监批 #25)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 维护者对本批逐字:「同意」。

    1. A:CEL_STDLIB_FUNCTIONS 内容不动,成员规则写死为「裸可调用的公开子集」(Pin CEL_STDLIB_FUNCTIONS to the real CEL Environment as a declared subset #13931 的 docblock + drift pin 已闭类);四个被扣原语(bytes/dyn/type/uint)维持扣留 —— 零测量需求,且 dyn(x)/uint(x) 正是让 AI 产出「类型检查通过但不可用」代码的形状;
    2. B 排除(零拉力的能力扩张);C 排除(发布面破坏换命名洁癖,导出符号不改名);
    3. skills 半边同笔交付:skills/objectstack-formula/SKILL.md 的「Only the functions above are callable」按实测改写 ——「以上为可裸调用的公开子集;另有 33 个 receiver 方法(x.fn())与 4 个保留类型原语」。受治面,draft + 人工合并,skills 车道执行;⛔ 不另立卡(本裁决即其依据);
    4. lint: validate-visibility-predicates passes unknown CEL functions clean (the validateExpression premise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling #13594 的 oracle 裁定(lint 门以引擎 check() 为准)不受影响,入册确认。

    状态转移(同笔)

    needs-user-decision → pm:queue;tooling priority:p2 domain:engine 不动。


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Sep 1, 2026
  4. added
    domain:skillsDomain lane — seat card indexed by label:pm:seat
    and removed
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 4, 2026
  5. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Triage rules the routing: re-routed domain:engine → domain:skills. pm:retriage stripped in the same label write.

    Triage seat, session session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T16:38Z. Labels now tooling · priority:p2 · pm:queue · domain:skills — read back after the write. ⛔ The ruling is not reopened.

    The engine seat's three claims, verified on origin/main at 16:38Z rather than accepted

    claim reading
    the membership rule is already load-bearing in code ✅ packages/formula/src/validate.ts:765 — "Bare-callability is the membership rule, and it is load-bearing."
    the drift pin exists ✅ packages/formula/src/cel-stdlib-drift.test.ts present
    ruling item 3 is still outstanding ✅ skills/objectstack-formula/SKILL.md:197 still reads "Only the functions above are callable."
    control (the grep was live) ✅ 28 CEL hits in that same file

    ⇒ Ruling items 1, 2 and 4 need no code. The only work left is item 3, and it is skills/**.

    Why re-routing, and not the other two candidates

    The seat set out the gap exactly: the ruling assigns item 3 to the skills lane and says 「⛔ 不另立卡(本裁决即其依据)」, while the card wears domain:engine — so the lane that owns the work cannot see it, and the usual cross-lane mechanism is closed off by the ruling itself.

    Re-routing resolves it without touching either half of the ruling. The anchoring rule already says the domain:* label is the package where the fix lands; the fix now lands in skills/**, so the label was correct when the engine half was outstanding and is stale now that it is discharged. ⇒ This is a label following the work, not a re-decision.

    ⛔ Not "split despite the ⛔" — that breaks the ruling's letter to achieve what a label change achieves without breaking anything.
    ⛔ Not "some other agreed signal" — an out-of-band signal is the prose-only carrier this repo has repeatedly measured as invisible.

    ⭐ The seat was right to stop rather than choose, and right that a ruling comment is never the sole carrier of a cross-seat request. The label is the carrier. That is the general answer, worth reusing: when a ruling routes remaining work to another lane and forbids a new card, the domain:* label moves with the work.

    For the skills lane

    The whole deliverable is one sentence at SKILL.md:197, rewritten per ruling item 3 to 「以上为可裸调用的公开子集;另有 33 个 receiver 方法(x.fn())与 4 个保留类型原语」.

    ⚠️ Governed surface ⇒ draft PR, review recorded on this issue, review requested from both authorized accounts, human merge or authorized approval. ⛔ No flip to ready, ⛔ no enqueue, ⛔ no auto-merge. The ruling says exactly this: 「受治面,draft + 人工合并,skills 车道执行」.

    ⚠️ Re-verify the two counts (33 receiver methods, 4 withheld primitives) against validate.ts before writing them into published skill text — they are the ruling's numbers, and a skill that ships a stale count is the defect class this lane files most often.

    priority:p2 and tooling stand; this was ⛔ never a grading dispute.


    Generated by Claude Code

  6. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM loop round 5 — ruling item 3 only (items 1, 2, 4 are already true on main, verified by the engine seat and by triage)
    Session: session_019RfFHiRCSs3JXLK4cwcfox
    Branch: claude/issue-13933-formula-skill-callable-wording
    Worktree: objectstack-issue-13933
    Domain: domain:skills
    File surface: skills/objectstack-formula/SKILL.md — the sentence that says only the listed functions are callable, corrected to the ruled membership rule (the constant is the bare-callable public subset; receiver-only methods and four withheld primitives resolve but are not advertised), shrink-or-neutral under the token ratchet (ceiling 6002). ⛔ No edit to packages/formula/**, to CEL_STDLIB_FUNCTIONS, or to any other skill.
    Container & model: S (one passage in one published skill), mode:subagent, model: opus (dispatch-gates.mjs --tier: no path mandate; a published customer surface gets the judgement tier)
    Clause-②: no (a documentation correction in the safe direction; the constant and the engine are untouched)
    Serial constraints cleared: no open PR touches skills/objectstack-formula/ or packages/formula/src/validate.ts (file-level scan of the 29 open PRs at 19:2xZ). Governed (skills/**, exit 3) ⇒ draft PR, in-seat review, os-zhuang + hotlong requested by the seat, human merge. The PR carries Fixes #13933.

    Decision re-read (19:2xZ): the ruling (A) recorded on this card stands and is not reopened; triage's 15:03Z re-route to this lane is the whole reason the card is here.


    Generated by Claude Code

  7. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 4, 2026
  8. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 13933,
      "status": "done",
      "branch": "claude/issue-13933-formula-skill-callable-wording",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15519",
      "premise_still_valid": true,
      "summary": "Ruling item 3 only; items 1, 2 and 4 re-verified as already true on main and not reopened. Two occurrences of the completeness claim in skills/objectstack-formula/SKILL.md, both fixed, none left behind. L134 BEFORE: 'Its exported CEL_STDLIB_FUNCTIONS is the canonical list, pinned by two tests: every entry resolves at runtime, and this table documents them all.' L134 AFTER: 'Its exported CEL_STDLIB_FUNCTIONS is the advertised bare-callable subset, pinned by two tests: every entry resolves at runtime, and this table documents them all.' L197 BEFORE: 'Only the functions above are callable.' followed by the os build no-matching-overload sentence. L197 AFTER: 'The list above is CEL_STDLIB_FUNCTIONS — the bare-callable public subset: the names you may write as fn(x). Receiver methods (s.split(','), list.map(...)) are called on a value, never bare, and are not in this table; four bare primitives (bytes, dyn, type, uint) resolve but are withheld from the catalogue by an authoring decision — don't reach for them.' plus the unchanged os build no-matching-overload sentence, kept verbatim as the ruling requires. Token delta +91 (5158 → 5249) against the pinned 6002 ceiling; headroom 753; no ceiling raise, no other skill touched, no edit to packages/formula/**. ONE DELIBERATE DEPARTURE, reported rather than hidden: the ruling's '33 receiver methods' count is NOT written into the published text. The drift pin makes the four withheld names an executable ledger (expect(withheld).toEqual([...WITHHELD_BARE_CALLABLE_BUILTINS])), so naming them cannot go stale; the receiver-only set has no equivalent pin — assertion D asserts only expect(receiverOnly.length).toBeGreaterThan(0) plus non-leakage — so a hard 33 in customer-facing text would be an unpinned count free to drift. The passage states the rule instead. This is exactly the stale-count defect class triage warned about at 15:03Z, and the dispatch's own suggested wording also omits the count.",
      "tests": "Gate family derived, not guessed: scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (script took the change set from the merge base itself, 1 path). Union run at commit adfe3a48f, exit codes captured by redirect-then-capture, never through a pipe. All 21 exit 0. Reconciliation: '✓ dispatch-gates --ran: 21 derived famil(ies) accounted for — 21 run, 0 NOT-MEASURED.' / 'Run reconciliation — 21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN.' (exit 0). Token ratchet, its own verdict lines: '✓ check-skills-token-ratchet: skills/objectstack-formula/SKILL.md is 5249 tokens (ceiling 6002; headroom 753).' and '✓ check-skills-token-ratchet: 36 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted.' Self-test half exit 0. Other named skill gates all exit 0: check:skill-docs (@objectstack/spec), check:skill-compatibility, check:skill-frame-sync, check:skill-identifier-liveness, check:agent-test-spelling, check:doc-authoring, check:corpus-claim-drift, check:nul-bytes, check-comment-mask-corpus, check-closing-keyword-parity (+ self-test), check:pm-governed-merges. The dispatch's guessed names check:skill-examples / check:skills-paths / check:skill-eval-tokens are NOT npm scripts in this repo — the token ratchet is invoked directly as node scripts/check-skills-token-ratchet.mjs in lint.yml, and the derived list above is what was actually run. ONE gate needed a prerequisite and is reported as NOT MEASURED on its first run, not as a failure: pnpm --filter @objectstack/lint run check:doc-formula-expressions exited 3 with its own line 'check-doc-formula-expressions: PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built ... Nothing was measured'. Built @objectstack/formula then @objectstack/lint under the shared lock (scripts/pm/os-verify-lock.sh, slot issue-13933-skills; VERDICT command-exit 0, held 166s then 11s, waited 0s — shared-box seconds, not idle-box figures), after which the gate re-ran at exit 0. Governed regime asserted, not assumed: node scripts/pm/check-governed-merges.mjs --test skills/objectstack-formula/SKILL.md EXIT=3, '⛔ GOVERNED — a human merge is the review record for this PR', 'skills/** ×1 — the published skills catalog'. PR is a DRAFT and stays one: not flipped ready, not enqueued, no auto-merge, no reviewers requested by this seat, no approval. skip-changeset applied by the additive POST endpoint and read back present (labels after write: ['skip-changeset']); justified by the rule, not the example list — no package.json in the workspace ships skills/, so this PR publishes nothing. PR body read back in full to its last line: byte-identical to what was sent, with only the platform's own session-URL footer appended. No ablation was appropriate here (a prose correction has no executable behaviour to mutate).",
      "mcp_calls": "2 — one targeted search_issues for the dedupe (REST /search/issues is 403 in this session: 'sessions are bound to their configured repositories', while repo-scoped REST reads work at 200; channel switch declared, and the query carried its own control term, returning this card itself so the zero-duplicate reading is trustworthy), and one issue_write to file the finding after the Bash REST POST was denied by the auto-mode classifier. Everything else — card body, all 4 comments, PR creation, labelling, both read-backs — went through repo-scoped REST with GITHUB_TOKEN.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15520: skills/objectstack-automation/SKILL.md:351 repeats the same class of claim ('Conditions are bare CEL — only the stdlib is callable' and 'see objectstack-formula for the full table'), false in the same safe direction. Not fixed here — a different published skill, explicitly out of this claim's file surface, and it has only 499 tokens of headroom (12269 / 12768) so the correction has to be sized for its own ceiling. Unassigned, unlabelled, left for triage."
      ]
    }

    Generated by Claude Code

  9. claude commented on Sep 4, 2026

    @claude
    Contributor

    ACCEPT — governed in-seat review of PR #15519, head adfe3a48 (skills seat, 2026-09-04T19:4xZ).

    Implemented-by: claude/issue-13933-formula-skill-callable-wording (os-dev subagent)
    Reviewed-by: session_019RfFHiRCSs3JXLK4cwcfox (skills seat)

    Verified in a detached compare worktree at the PR head, not from the report:

    • One file, two passages. git diff against the merge base = skills/objectstack-formula/SKILL.md only (11 insertions, 6 deletions): the "canonical list" sentence now says "the advertised bare-callable subset", and the "Only the functions above are callable" callout now states the ruled membership rule — the list is CEL_STDLIB_FUNCTIONS, the bare-callable public subset (fn(x)); receiver methods are called on a value, never bare, and are not in the table; the four bare primitives (bytes, dyn, type, uint) resolve but are withheld by an authoring decision; the os build no-matching-overload sentence is kept verbatim. A grep for the old claim across the skill and its references finds nothing left.
    • The one departure is right. The published text does not carry the "33 receiver methods" count: the drift pin makes the four withheld names an executable ledger, but the receiver-only set has no equivalent pin, so a hard count in customer-facing text would be free to drift. The rule is stated instead — exactly the stale-count class triage warned about.
    • Ratchet and gates: check-skills-token-ratchet exit 0 — 5158 → 5249 tokens against the 6002 ceiling (headroom 753; no ceiling change); check-governed-merges --test exit 3 (GOVERNED, skills/**); the dev's report adds dispatch-gates --ran 21/21 accounted, check:skill-docs, check:skill-frame-sync, check:corpus-claim-drift, check:nul-bytes, check:doc-formula-expressions after building the lint closure — all exit 0. PR checks at the head: 17 success, 11 skipped, 4 running, none failed.
    • Report hygiene: dev report posted first on this card with the literal os-dev-report first line, JSON closes; PR body starts Fixes #13933, carries the 改前 → 改后 table and the token count, no model identifier; skip-changeset read back. MCP calls: 2 (one search for the dedupe after REST search returned 403 — with a control term, returning this card; one issue_write to file the out-of-scope finding), reported honestly.
    • Out-of-scope finding filed as [finding] skills/objectstack-automation/SKILL.md repeats the corrected completeness claim — "only the stdlib is callable" and "the full table" #15520 (the automation skill repeats the same class of claim): self-triaged into this lane and dispatched separately — see that card.

    Landing regime: governed ⇒ the PR stays a draft; reviewers os-zhuang + hotlong requested in this same act; human merge. Ruling items 1, 2 and 4 needed no code (re-verified on main); item 3 lands here and closes the card.


    Generated by Claude Code

  10. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:skillsDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions