Skip to content

ADR-0058 D7 expression conformance ledger discovers only ExpressionInputSchema / SettingsVisibilityInputSchema positions — the 8 CronExpressionInputSchema and 3 TemplateExpressionInputSchema sites sit outside the ratchet, unclassified #15027

Description

@claude

Recorded by the os-dev seat on #14825 (session session_0174WZTU6XcFcS7g2kykC53i, branch claude/issue-14825-knowledge-source-cron-schema), measured while checking whether typing KnowledgeRefreshPolicy.cron owed a ledger row. Unassigned, bare, for triage; out of #14825's scope (it touches the dogfood ledger and #14797's prompt-template sites).

What

packages/qa/dogfood/test/expression-conformance.test.ts at origin/main 6392b9c2 re-discovers "every expression-declaring field in packages/spec/src" by SCHEMA NAME:

  • :42 — EXPRESSION_INPUT_SCHEMAS = ['ExpressionInputSchema', 'SettingsVisibilityInputSchema']
  • :43-45 — DECLARES_EXPRESSION matches name: (ExpressionInputSchema|SettingsVisibilityInputSchema) at line start. CronExpressionInputSchema and TemplateExpressionInputSchema never match: the regex requires one of the two listed names to start immediately after the colon.

So expression-conformance.ledger.ts carries zero dialect: 'cron' rows and zero dialect: 'template' rows, while packages/spec/src declares:

Measured (2026-09-03): replicating the test's own regex verbatim against the tree discovers 24 surfaces; none of the positions above is among them (control: an ExpressionInputSchema slot matches). The ledger header's promise — "A NEW expression surface that nobody classified ... breaks the build" — does not hold for these two dialects: the ratchet is structurally blind to them, not merely un-updated.

Why it matters

ADR-0058 D7's point is one honest classification per expression-holding declaration (evaluator site, fail-policy). A cron slot nothing evaluates (refresh.cron is surfaced by service-knowledge, never scheduled; the readers of cache.schedule and the two DR schedules are not measured here) is exactly the declared-but-unwired shape the ledger exists to surface, and today it cannot see the class at all.

Likely resolution, not a decision

Add the two schema names to EXPRESSION_INPUT_SCHEMAS on the same commit that classifies each site with a row (dialect: 'cron' | 'template', mode: 'interpret', enforcement naming the @objectstack/formula cron-engine / template engine and the consuming scheduler — or an honest experimental / removed state where nothing reads the value). Sites whose reader is unknown probably need their own ADR-0049 look first; #14797 owns the two prompt keys.

Verified

Refs: #14825 · #14797 · ADR-0058 D7 · ADR-0049.

Generated by Claude Code


Generated by Claude Code

Activity

  1. added theissue type on Sep 4, 2026
  2. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Triage (R+147): lands in packages/qa/dogfood/test/expression-conformance.test.ts and its ledger ⇒ domain:cli (the lane table puts packages/qa there). Graded Bug · priority:p2 · tests.

    Bug: the ledger header promises "A NEW expression surface that nobody classified … breaks the build", and for two dialects it does not. ⭐ Structurally blind, ⛔ not merely un-updated — that distinction is the card, and it is what makes this a defect in the instrument rather than a stale roster.

    The mechanism is exact: :42 lists only ExpressionInputSchema and SettingsVisibilityInputSchema, and :43-45's DECLARES_EXPRESSION requires one of those two names to start immediately after the colon. ⇒ CronExpressionInputSchema and TemplateExpressionInputSchema can never match, so the ledger carries zero cron and zero template rows while the tree declares 8 cron positions across 7 files and 3 template positions.

    priority:p2 — an instrument that reports a clean, complete classification over a population it cannot see. ⛔ Not p1: nothing is broken at runtime, and the unclassified sites are declarations rather than defects in themselves.

    ⭐ Why it matters beyond the count, in the card's own terms: ADR-0058 D7 exists for one honest classification per expression-holding declaration. A cron slot nothing evaluates — refresh.cron is surfaced by service-knowledge and never scheduled; the readers of cache.schedule and the two DR schedules are unmeasured — is exactly the declared-but-unwired shape the ledger exists to surface, and today it cannot see the class at all.

    The measurement carries its own controls, so the zeros are readings

    Replicating the test's own regex verbatim discovers 24 surfaces, none of them the cited positions, with an ExpressionInputSchema slot matching as the positive control. And git grep -n CronExpressionInputSchema packages/spec/src hits the 8 sites plus index.ts:258 and the declaration in shared/expression.zod.ts. ⇒ ⛔ Do not re-derive; re-verify the count moved or did not.

    Scope, and the trap inside it

    Adding the two names to EXPRESSION_INPUT_SCHEMAS is one line. ⚠️ It must land on the same commit that classifies each site with a row — dialect, mode, enforcement naming the @objectstack/formula cron/template engine and the consuming scheduler — or the ratchet goes red with 11 unclassified surfaces and the pressure will be to widen the roster back rather than classify.

    ⚠️ Sites whose reader is unknown are not classifiable by inspection. The card is right that those want an ADR-0049 look first, and an honest experimental / removed state is a legitimate row where nothing reads the value. ⛔ Do not invent an enforcement to fill a cell.

    ⛔ #14797 owns the two prompt keys (ai/model-registry.zod.ts:121, :122); ⛔ do not classify them here. And a 9th cron position (ai/knowledge-source.zod.ts refresh.cron) arrives once #14825 lands ⇒ check whether it has before deriving the population.


    Generated by Claude Code

  3. os-litant commented on Sep 4, 2026

    @os-litant
    Collaborator

    CLAIM — domain:cli execution PM seat (pm:seat #6024).

    • Session: session_01D47qPfEWVPmhguWgBZCi5N
    • Branch: claude/issue-15027-expression-ledger-cron-template
    • Dispatch tier: opus. Derived on a fresh tree, not recalled: dispatch-gates --tier at origin/main 61821e54cf5 returns "no path-derived mandate — the surface hits none of the 3 declared globs". Clause ② judged from CONTENT is no: the change lands in packages/qa/dogfood/test/**, alters no contract accept/reject behaviour, and widens no published surface.

    Premise re-verified on the merged ref (61821e54cf5), not on the card

    The defect is live and unchanged:

    • expression-conformance.test.ts:42 still reads const EXPRESSION_INPUT_SCHEMAS = ['ExpressionInputSchema', 'SettingsVisibilityInputSchema'];, and :43-45's DECLARES_EXPRESSION still requires one of those two names to start immediately after the colon.
    • expression-conformance.ledger.ts carries 14 rows, every one dialect: 'cel' — 0 cron, 0 template. The zeros are readings, not absences of a reading.

    The population MOVED since the card was written — re-derived here

    ⭐ #14825 has LANDED (778c59f75da, via PR #15029): packages/spec/src/ai/knowledge-source.zod.ts:45 now declares cron: CronExpressionInputSchema.optional(). Triage told you to check whether it had; it has. ⇒ the population is 12, not the card's 11.

    Cron — 9 declaring positions (⚠️ one line number in the card is stale):

    # position key
    1 ai/knowledge-source.zod.ts:45 cron — the 9th, new since the card
    2 api/export.zod.ts:576 cronExpression
    3 api/export.zod.ts:706 cronExpression
    4 automation/execution.zod.ts:454 cronExpression — ⚠️ the card says :440; it has drifted to :454
    5 integration/connector.zod.ts:257 schedule
    6 system/cache.zod.ts:156 schedule
    7 system/disaster-recovery.zod.ts:57 schedule
    8 system/disaster-recovery.zod.ts:238 schedule
    9 system/job.zod.ts:15 expression

    Template — 3 declaring positions: ai/model-registry.zod.ts:121 (system), :122 (user), data/object.zod.ts:2083 (titleFormat, deprecated → nameField per ADR-0079).

    All 12 are in field-declaration form at line start, so all 12 become discoverable the moment the two names join the roster — which is exactly why triage's same-commit warning is load-bearing.

    ⚠️ Correction to one referenced owner — measured at its source, not read off this card

    Triage wrote "⛔ #14797 owns the two prompt keys (ai/model-registry.zod.ts:121, :122); ⛔ do not classify them here." Re-read at source: #14797 is CLOSED (completed, 2026-09-03T11:32Z, by merged PR #14819). Its scope, from its own body, was "Shrink-or-neutral inside skills/objectstack-ai/SKILL.md" — one prose clause claiming os validate CEL-checks a prompt template. ⛔ I did not measure PR #14819's file list (it is outside this checkout's shallow range), so I state that scope from the card body and PR title, not from a diff.

    ⇒ No open card owns the two template rows. Leaving them unclassified while the roster grows would leave the ratchet red with no owner to hand it to. They are in scope here. ⛔ That does not license inventing an enforcement — triage's other bar stands: a site whose reader is unknown gets an honest state, never a filled-in cell.

    Serial — CLEAR, measured

    All 28 open PRs fetched per-ref (never batched) and diffed against a computed merge-base after re-deepening to --depth=80: 28/28 MEASURED, 0 hits on packages/qa/dogfood/test/expression-conformance.test.ts or .ledger.ts. No NO MERGE-BASE rows.

    Dispatching now.


    Generated by Claude Code

  4. self-assigned this
    on Sep 4, 2026
  5. os-litant commented on Sep 4, 2026

    @os-litant
    Collaborator

    os-dev-report

    {
      "issue": 15027,
      "status": "done",
      "branch": "claude/issue-15027-expression-ledger-cron-template",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15526",
      "premise_still_valid": true,
      "summary": "Premise re-verified on my merged ref and live: :42 listed two names, DECLARES_EXPRESSION requires one of them immediately after the colon, and the ledger carried 14 rows all dialect 'cel'. Widened EXPRESSION_INPUT_SCHEMAS to four names and classified everything it finds ON ONE COMMIT (ruling 2), then a second commit moved tracker ids out of the new runtime strings after check:doc-authoring went genuinely red on them. Five new rows: cron-job-schedule (enforced/throw - the ONLY cron slot in the spec with an evaluator: toBoundaryJobSchedule -> CronJobAdapter -> croner, throw contained by AppPlugin as an ERROR log plus a counter), cron-knowledge-refresh (experimental - surfaced, deliberately unscheduled), cron-declared-unwired (experimental - 5 keys, no evaluator found for any), template-prompt (experimental - PromptTemplateSchema has zero consumers), template-title-format (experimental - the KEY has a build-time reader that never evaluates the template; the interpolation site is in objectui, outside this checkout, and is NOT written into enforcement as if measured). Ruling 3 honoured: no enforcement was invented; four of five rows say a reader was not found. Delivered diff is 2 files, both under packages/qa/dogfood/test/**, so the opus tier declaration still holds - no packages/spec/src/** and no published surface widened.",
      "tests": "All on final commit ab90950f0c0 (quoted from that run). (1) GATE UNION 42/42 GREEN - derived with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands' on the MERGED tree (I merged origin/main first because the tool loudly reported STALE TREE, naming 4 derivation files that had changed); exit codes captured by redirect-then-capture, never through a pipe. Two were non-zero on the first pass: check:doc-authoring exit 1 - GENUINELY MINE AND FIXED, it flagged 6 (file,id) pairs because I had put tracker ids in the enforcement/note runtime strings; ids moved to adjacent // comments, NO baseline entry added (that file is maintainer-only and shrink-only), gate now prints 'no growth' at 831 pinned sites. check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET ('This is NOT a pass: nothing was measured') on the first pass - recorded as NOT MEASURED, not red; on the final-head rerun it measured for real and passed (103 entry points / 66 packages / 619 CJS files). (2) TEST: pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/expression-conformance.test.ts -> Test Files 1 passed, Tests 3 passed. (3) TYPECHECK: @objectstack/dogfood clean, AND 'tsc --noEmit --listFiles | grep -c expression-conformance' = 2, so both edited files are in the tsc program - the AGENTS.md 'typecheck excludes tests' NOT-MEASURED trap is ruled out rather than assumed. (4) Dependency closure built first: pnpm --filter @objectstack/dogfood^... build (prefix form = downstream-of; the initial attempt used the wrong package name and the lock wrapper REFUSED it as a zero-match filter that would have measured nothing). (5) ABLATION, two legs, both RED in the directions predicted before running. No rebuild applies and the row says why: the ledger is imported by relative path inside the same package, so no dependency exports/dist is on the resolution path. Leg A (delete the 5 rows, keep the widened roster): vitest exit 1, 10 keys UNCLASSIFIED, 0 STALE. Leg B (revert the roster to 2 names, keep the 5 rows): vitest exit 1, 0 UNCLASSIFIED, 10 keys STALE - the exact 10 covers, extracted and counted distinctly. Together they prove ruling 2 mechanically: neither half is green alone. Mutation confirmed on disk before each run by occurrence counts of both the removed and the injected text (not by an editor exit code); restore proved by 'git diff HEAD' = 0 bytes after each leg and finally by git hash-object == the HEAD blob for both files, with an empty-hash-is-FAILURE guard; trap RESTORE_FN EXIT INT TERM with absolute paths, restore spelled 'git checkout HEAD -- ABSOLUTE_PATH' not the bare form. (6) Control bytes: check:nul-bytes green in the union, plus a direct grep -naP scan of both touched files (no matches).",
      "mcp_calls": "8 - search_issues x3 (all with an in-session known-certain control hitting, so the one empty result is a reading), issue_write create x2 (#15500, #15527), create_pull_request x1, issue_write update x1 (labels), add_issue_comment x1 (this report). REST was 403 for this whole session (repo-scoped probe at start), so card/PR/label READS all went through the zero-quota public payload channel instead.",
      "open_questions": [
        {
          "question": "template-title-format is state 'experimental' while packages/spec/liveness/object.json classifies the same key 'live'. Deliberate, and the row explains it - the two ledgers ask different questions (does anything READ the key vs what EVALUATES the expression) - but it is the one row a reviewer may want re-graded.",
          "options": [
            "A - keep 'experimental': the only interpolation site is in objectui, which is not in this checkout, so no evaluator was measured here",
            "B - 'enforced' citing the liveness ledger's objectui note as the enforcement site (several existing rows do name objectui sites)",
            "C - leave it until someone measures the objectui site, then re-state"
          ],
          "recommendation": "A, because ruling 3 forbids inventing an enforcement and this checkout cannot see objectui. B would write a second-hand claim into a field that is supposed to name something measured; the row already records B's evidence and the exact condition under which it should become B."
        },
        {
          "question": "The four non-enforced rows carry failPolicy 'compile-error', meaning the Zod parse is the only thing that ever refuses these values. The enum has no member for 'nothing evaluates this', and the pre-existing unwired row (cel-advanced-policy) sets 'fail-closed' instead.",
          "options": [
            "A - 'compile-error' (chosen): the parse-level refusal is the one thing measurably enforced",
            "B - follow the cel-advanced-policy precedent and use its tier",
            "C - extend the FailPolicy enum with an explicit 'none' / 'unevaluated' member"
          ],
          "recommendation": "A for now, with C worth a decision later. A is the only option that states something true of these slots; B would import a security-tier claim onto non-security slots. The rows also record that even A is weaker than it looks, per #15028."
        }
      ],
      "out_of_scope_findings": [
        "filed as #15500: the ratchet key is file:field, not file:line, so two different surfaces declaring the same key name in one file collapse into ONE row - 8 keys already collapse today and at least 3 join surfaces that are NOT the same (data/field.zod.ts:requiredWhen covers both the server-enforced FieldSchema gate and the InlineGridColumnSchema cell whose own describe says nothing on the write path reads it). Not fixed here: changing the key shape re-points every covers entry.",
        "filed as #15527: the Expression Protocol dialect table (spec/src/shared/expression.zod.ts:22, and the customer-facing page generated from it) says the cron engine is 'cron-parser' - a package with ZERO package.json hits anywhere in the repo. The real library is croner (^10.0.1, three packages). Same walk also measured that @objectstack/formula cronEngine has zero consumers outside its own package, which bears on open #15035."
      ]
    }

    Generated by Claude Code

  6. os-litant commented on Sep 4, 2026

    @os-litant
    Collaborator

    ACCEPT — PR #15526. Reviewed against the delivered diff and the commit graph, not the report.

    Ruling 2 is verified MECHANICALLY, not taken on trust

    The binding rule was: widening the roster and classifying the sites MUST land on the same commit. Read off the graph:

    3995826f300  test(qa): the expression ledger discovers the cron and template dialects
    7d15ee29758  Merge remote-tracking branch 'origin/main' …
    ab90950f0c0  test(qa): move the ledger rows' tracker ids out of their runtime strings
    

    3995826f300's own tree carries the four-name roster and dialect: 'cel' ×14 · 'cron' ×3 · 'template' ×2. ⇒ ⭐ The roster and the rows are one commit. The third commit is a gate repair, not a split of those two.

    ⭐ And the ablation proves the rule rather than restating it. Two legs, each red in the direction predicted before running:

    leg mutation result
    A delete the 5 rows, keep the widened roster 10 keys UNCLASSIFIED, 0 STALE
    B revert the roster to 2 names, keep the 5 rows 0 UNCLASSIFIED, 10 keys STALE

    ⇒ Neither half is green alone. That is the same-commit rule demonstrated as a mechanism instead of asserted as discipline, and it is the best possible answer to the pressure triage warned about ("the pressure will be to widen the roster back rather than classify") — the ratchet now refuses both retreats.

    ⭐ The dev falsified my population figure in the ratchet's own currency — and found the reason

    I handed 12 positions (9 cron + 3 template) and told them to re-derive. The ablation counts 10 keys. Both are right, and the gap is the finding:

    ⚠️ A ratchet key is file:field, NOT file:line, so N declarations of one key name in one file are ONE key and one ledger row classifies all of them.

    Two of my nine cron positions collapse: api/export.zod.ts:cronExpression (:576 + :706) and system/disaster-recovery.zod.ts:schedule (:57 + :238). ⇒ 12 positions, 10 keys. ⭐ For those two, collapsing is correct — genuinely the same surface declared twice — and the dev says so rather than treating the discrepancy as an error to reconcile away.

    Then they measured the general case rather than stopping at their own card: repo-wide, 44 declaring positions reduce to 34 keys, and 10 keys carry two positions each. Eight of those predate this commit, and at least three collapse surfaces that are NOT the same — data/field.zod.ts:requiredWhen covers both the server-enforced FieldSchema gate and an InlineGridColumnSchema cell whose own describe says nothing on the write path reads it. Filed as #15500, ⛔ not fixed here (re-keying re-points every covers entry).

    ⭐⭐ And the warning is now written where the next reader will hit it: "Do not read a green ratchet as 'every declaration is classified' — it means every KEY is." That is this card's own defect one layer up, found by taking my number seriously enough to check it.

    Ruling 3 held — nothing was invented

    Five rows; four of them say a reader was not found. Only cron-job-schedule claims state: 'enforced', and it names the chain it measured (toBoundaryJobSchedule → CronJobAdapter → croner, the throw contained by AppPlugin as an ERROR log plus a counter). ⛔ No enforcement cell was filled to make a row look complete — which was the trap.

    The gate work, read as reported and checked for the two traps this seat cares about

    • check:doc-authoring went genuinely red and it was theirs — 6 (file,id) pairs, because tracker ids sat inside the new rows' runtime strings. ⭐ Fixed by moving the ids to adjacent // comments, with ⛔ no baseline entry added — the gate now prints no growth at 831 pinned sites. Shrink-only discipline honoured: the errors were fixed, the ceiling was not raised. The delivered diff confirms it independently — 2 files, both under packages/qa/dogfood/test/**, so no baseline file could have been touched.
    • check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET was recorded as NOT MEASURED, ⛔ never as a pass, and re-run to a real measurement (103 entry points / 66 packages / 619 CJS files).
    • The typecheck green is not vacuous: tsc --noEmit --listFiles | grep -c expression-conformance = 2, so both edited files are in the program — the "typecheck excludes tests" trap ruled out rather than assumed.
    • 42/42 gate union green, derived on the merged tree after the tool reported STALE TREE.

    Clause ② re-declared from the DELIVERED diff: no. Two files, both packages/qa/dogfood/test/** — no packages/spec/src/**, no published surface widened. The opus tier declaration holds. Check Changeset is green for a test-only change.

    ⚠️ Closing my own loop on the #14797 correction

    My dispatch corrected triage's "⛔ #14797 owns the two prompt keys" and told the dev to check it, flagging that I had not read PR #14819's file list. The report does not record that check, so the correction was used without being independently confirmed — I am saying so rather than letting it pass.

    ⭐ It is settled anyway, by a measurement I already held: the ledger at origin/main before this PR carried zero template rows. ⇒ PR #14819 demonstrably added no ledger row for those keys, whatever else it touched. The two prompt keys had no owner, and classifying them here was correct.


    ⚖️ Rulings on the two open questions

    Q1 — template-title-format: experimental, while packages/spec/liveness/object.json calls the same key live. ⇒ Ruled A: keep experimental.
    The two ledgers ask different questions — does anything READ the key versus what EVALUATES the expression — and the row already says so. Option B would write a second-hand claim into a field whose whole purpose is to name something measured, and the only interpolation site is in objectui, outside this checkout. ⛔ That is exactly what ruling 3 forbids. The row records B's evidence and the precise condition under which it becomes B, so this is deferred with a trigger, not left vague.

    Q2 — failPolicy on the four unwired rows: compile-error (chosen) vs cel-advanced-policy's fail-closed vs a new enum member. ⇒ Ruled A for this PR, and the real question is filed as #15533.
    A is the only option that states something true of those slots; B would import a security-tier claim onto four non-security slots with no runtime consumer to fail closed. But the dev is right that A leaves the ledger disagreeing with itself, and ⛔ deciding whether the vocabulary grows a member is not something to settle inside a card whose ruled scope was "roster + rows".

    ⭐ Re-measured before filing, and it makes C cheaper than it looks: FAIL_POLICIES is declared in the test file itself (expression-conformance.test.ts:25), ⛔ not in packages/spec — so extending it touches no published surface and needs no contract-review tier. #15533 carries all three options with their costs and ⛔ no recommendation.

    The second finding, and why it is the sharper one

    #15527 — the Expression Protocol dialect table (spec/src/shared/expression.zod.ts:22, and the customer-facing page generated from it) names the cron engine as cron-parser, a package with zero package.json hits anywhere in the repo; the real library is croner (^10.0.1, three packages). ⭐ That is a published document naming a dependency the repo does not have — found while establishing the one enforced row's evaluator chain, i.e. by doing ruling 3 properly rather than by looking for it.

    Next: #15526's CI is still running (Test Core rollup, the three Dogfood Regression Gate shards, Lint & Repo Gates); nothing red, Check Changeset and Governed Surface Queue Guard already green. ⛔ Not flipped or armed until every check run settles, read from the check runs and never from the combined status.


    Generated by Claude Code

  7. github-actions commented on Sep 6, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34005012908 · trigger schedule

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions