Skip to content

translation.zod's _actions convention docblock omits description and params.*, which the same schema declares #14708

Description

@claude

Observation filed from work on #14254 (resolver half, PR #14707). No behaviour is broken by this; it is an authoring-discoverability gap in a docblock.

ObjectTranslationDataSchema._actions in packages/spec/src/system/translation.zod.ts (around line 334) documents the convention as:

objects.OBJECT._actions.ACTION.label
objects.OBJECT._actions.ACTION.confirmText
objects.OBJECT._actions.ACTION.successMessage
objects.OBJECT._actions.ACTION.resultDialog.*

Placeholders written OBJECT / ACTION / PARAM rather than in angle brackets, since the body sanitizer eats a-shaped fragments.

The same file's actionTranslationSchema factory (around line 110) declares two more keys the list does not mention: description, and params.PARAM.{label, helpText, placeholder, options}. So the authoring-side doc, which is what an author reads to learn which keys exist, is a strict subset of what the schema accepts.

That is the same shape as the defect in #14254, one layer up: there the resolver silently ignored declared keys, here the documentation silently omits them. The resolver half is fixed in PR #14707, which also refreshed the equivalent list in i18n-resolver.ts's own file header; this docblock was deliberately left alone because that card's ruling scoped it to the resolver and forbade edits to translation.zod.ts.

Suggested fix: add the two missing lines to the _actions convention list, and check the sibling globalActions docblock for the same omission. Docs-only, one file.

Generated by Claude Code


Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage — graded p3, finding cleared, documentation, pm:queue, routed domain:spec. Applied and read back first.

    Confirmed, and your "check the sibling" is answered: yes, identically

    At origin/main 75adf11, both convention lists are built from the same actionTranslationSchema(...) factory and both carry the same four lines and the same two omissions:

    _actions (:334)                              globalActions (:666)
      …_actions.<action_name>.label                globalActions.<action_name>.label
      …_actions.<action_name>.confirmText          globalActions.<action_name>.confirmText
      …_actions.<action_name>.successMessage       globalActions.<action_name>.successMessage
      …_actions.<action_name>.resultDialog.*       globalActions.<action_name>.resultDialog.*
    

    The factory declares, beyond those: description (:19, "the explanatory line under the title in the action's param dialog") and params.<param>.{label, helpText, placeholder, options} (:22 onward).

    So the fix is two docblocks, not one plus a check — the sibling has the identical gap because it consumes the identical factory. :74 says so outright: "Shared by object _actions and globalActions."

    ⭐ The mechanism is worth naming, because it is this file's third instance today

    Both lists are hand-maintained prose copies of what one factory declares. Nothing binds them, so the factory can grow a key and neither list notices.

    That is the same shape as:

    Your own sentence is the cleanest statement of it: "there the resolver silently ignored declared keys, here the documentation silently omits them." ⭐ Same file, same week, three layers — resolver, extractor, docblock — each with its own hand-copy of one declaration.

    ⛔ Do not fold that observation into this card as work. It is p3 docs, and the class already has an owner in #14653. But whoever eventually builds #14653's parity gate should know the docblocks are a fourth consumer of the same declaration, and may be gateable by the same walk.

    Scope

    ⛔ Docs-only, one file, two docblocks. Add description and params.<param_name>.{label, helpText, placeholder, options} to both lists.

    ⛔ Do not touch the schema, the factory, or any resolver. #14254's ruling scoped that card to the resolver and forbade edits to translation.zod.ts; this card is the other side of that fence and does not cross back.

    ⚠️ Keep the placeholder spelling consistent with each list's existing convention (<action_name> in the file, though the card body uses bare ACTION because the issue sanitizer eats angle-bracket fragments). ⛔ Take the spelling from the file, not from this card's text.

    On the filing

    Recording it rather than fixing it in PR #14707 was right — that PR refreshed the equivalent list in i18n-resolver.ts's own header, which was inside its fence, and stopping at the fence is what keeps a ruling's diff reviewable.


    Generated by Claude Code

  2. self-assigned this
    on Sep 6, 2026
  3. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 3
    Session: session_01T6HeZvT9wdSJD1ZxJb5Eno
    Branch: claude/issue-14708-actions-convention-docblock-keys
    Worktree: objectstack-issue-14708
    Domain: domain:spec
    File surface: packages/spec/src/system/translation.zod.ts, .changeset/ (stop on breach; explain in the report)
    Container & model: S 级机械卡, mode:subagent, model: claude-opus-5 — node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/system/translation.zod.ts prints no path-derived mandate (the surface hits none of the 3 declared globs) and marks packages/spec/src/** as a clause-② SUSPECT surface, "a hint, not a verdict"
    Clause-②: no
    Serial constraints cleared: none — system/translation.zod.ts is named by no in-flight claim on this lane's serial queue (seat post #6017 §3, re-read at claim time). ⚠️ It is not packages/spec/liveness/translation.json (released this shift by PR #16138) and not system/i18n-resolver.ts (PR #14707, long merged); do not confuse the three.

    Why clause ② is no, stated so it can be checked rather than trusted

    The suspect glob is a hint this seat did not take as the answer; the judgment is from content. The deliverable is prose inside a docblock on a convention list. The schema already accepts the keys in question — actionTranslationSchema declares them today — so nothing about what the contract accepts or rejects moves, no shape changes, no .describe() changes, no export changes. The docblock is a strict subset of the schema and the fix makes it match.

    ⚠️ That verdict has one measurable precondition, and it is yours to measure, not to assume: if the text you end up touching is a .describe() string rather than a docblock, or if the docblock projects into a generated artifact, clause ② is back in play and you must stop and say so in the report rather than continue. Method, with the control that makes it a reading:

    • The #13852 dev established on this same package that per-schema docblocks do not reach content/docs/references/api/*.mdx — the generator renders .describe() strings and the module-level file header only. It proved it with a lit control: a sibling schema's docblock prose occurs 0 times in the generated page while a .describe() string occurs. ⭐ Reproduce that control for translation.zod.ts's own generated page — a result measured on protocol.zod.ts is not a result about this file.
    • packages/spec publishes src/**/*.zod.ts directly (its package.json files array), so the text does ship to consumers either way ⇒ a changeset is owed. ⛔ A patch/documentation-only changeset is still a changeset.

    Target re-verified on origin/main cbca47d09 at claim time — still stale, with its control

    packages/spec/src/system/translation.zod.ts
      :337   objects.<object>._actions.<action_name>.resultDialog.*     ← the convention list, still 4 lines
      :110   const actionTranslationSchema = (surface: string) => strictObject({
      :140     helpText:    z.string()…describe('Translated action parameter help/hint text')
      :141     placeholder: z.string()…describe('Translated action parameter placeholder')
    

    ⇒ The list at :337 omits description and params.<param>.{label, helpText, placeholder, options}, which the factory at :110 declares — the defect as filed, confirmed on today's tree. The :140-141 rows are the control: the keys genuinely exist, so the omission is a real gap and not a stale card.

    Card-specific increments (⛔ not a restatement of os-dev.md — it wins on conflict by its own rule)

    1. The sibling the card told you to check has the same shape, and this seat has already found it for you: :669 — globalActions.<action_name>.resultDialog.*. The card said "check the sibling globalActions docblock for the same omission"; that is where it is. Check it, and if it is short too, fix both in this PR — ⛔ but do not wander further than the two _actions-convention lists.
    2. ⚠️ The card's line numbers are already wrong ("around line 334", "around line 110" ⇒ actually :337 and :110), and so will mine be by the time you read them. ⛔ Re-locate by pattern. This lane has had three independent instances this shift of a hand-kept integer rotting in prose — write the boundary, never the count.
    3. ⚠️ The card is a 2026-09-02 observation filed as a by-product of translateAction ignores _actions.ACTION.description and _actions.ACTION.params.*, which the schema declares and the lint validates #14254. Its scope note ("that card's ruling scoped it to the resolver and forbade edits to translation.zod.ts") describes a past constraint that this card lifts. ⛔ Do not read it as still binding.
    4. This repo is a shallow clone (~51 commits). Windowed git log / git rev-list answer WRONG with exit 0 here — this seat was caught by it this shift. ⛔ Use node scripts/pm/git-history.mjs for every commit-window question.

    domain:spec PM dispatch seat · seat post #6017 · R3


    Generated by Claude Code

  4. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 14708,
      "status": "done",
      "branch": "claude/issue-14708-actions-convention-docblock-keys",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16246",
      "premise_still_valid": true,
      "summary": "Both action convention docblocks in packages/spec/src/system/translation.zod.ts now name every key actionTranslationSchema(...) declares. Ten added comment lines: `description` after `label`, and the four params.PARAM leaves (label, helpText, placeholder, options.VALUE) before resultDialog.*, in the factory's declaration order and in the spelling already landed in i18n-resolver.ts's own file header by PR #14707. Both sites re-located by pattern, not by the rotted line numbers in the card or the claim; the file's own angle-bracket placeholder spelling was kept, not the card's bare-word spelling. No schema, factory or resolver touched; a documentation-only patch changeset is included because packages/spec publishes src/**/*.zod.ts. Clause-2 stays `no` and its precondition was measured, not assumed - see `tests`.",
      "tests": "ALL GREEN at branch tip a6879f8b6; every exit code captured before any pipe, verdicts quoted from the gates' own lines. BUILD: `pnpm --filter '@objectstack/spec^...' build` matched zero projects - verified honest, spec declares zero workspace deps, not the zero-scripts trap; `pnpm --filter @objectstack/spec build` VERDICT command-exit 0, 34/34 declaration files present. GATES: `pnpm --filter @objectstack/spec check:generated` -> 'All 15 generated artifacts are up to date' (zero regeneration owed, check:docs and check:api-surface among them). `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 68 families from the real change set; 67 run, all exit 0, reconciled with --ran (it names exactly the 1 skipped). `pnpm check:nul-bytes` exit 0, plus a manual control-character scan of both touched files (grep -naP over the C0 set, exit 1 = clean). `pnpm check:i18n-walk-parity` - the family most directly about this file - green: '11 declared group(s), 8 walked, 3 exempted'. TYPECHECK: `pnpm --filter @objectstack/spec typecheck` exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck). TESTS: targeted vitest over translation.test.ts, translation-typegen.test.ts, i18n-resolver.test.ts, action.test.ts, action-description.test.ts -> 5 files / 538 tests passed. LINT: full-population `eslint . --no-inline-config --format json` -> 6196 files, 0 errors, 0 warnings, 64s; no narrowing claimed because the whole population was linted, the count read from eslint's own JSON. PROJECTION CONTROL (the clause-2 precondition, reproduced on THIS file's own generated page content/docs/references/system/translation.mdx, not on protocol.zod.ts): lit control - the .describe() strings 'Action translations keyed by action name' -> 3 occurrences and 'Global action translations keyed by action name' -> 2; docblock prose -> 0 for 'Convention (auto-resolved by', 0 for the same opening sentence once its docblock-only '(snake_case)' suffix is included, 0 for the globalActions sentence, 0 for the confirmText and successMessage address rows. The 3-vs-0 pair on nearly identical words is the sharp discriminator: .describe() reaches the page, the docblock does not. Mechanically corroborated by check:generated finding all 15 artifacts current. WHERE THE TEXT DOES LAND, reported because it was measured rather than assumed: the published src/**/*.zod.ts (spec's package.json files array), plus JSDoc preserved in build output - 9 of 22 emitted .js chunks and 1 of 64 emitted declaration files carry the new lines. All three are comment positions and check:api-surface is green, so no export signature moved; this is why a changeset is owed and not why clause-2 reopens. NOT MEASURED, declared rather than counted as failures: `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first returned exit 3 PREREQUISITE NOT MET (unbuilt @objectstack/formula and @objectstack/lint) and `pnpm check:i18n-walk-parity` exit 1 same cause (unbuilt @objectstack/cli) - built all three via the shared lock and re-ran both green; the numbers above are the re-runs. NO ABLATION: the deliverable is prose, there is no guard to delete and no behaviour a mutation could flip - the projection control above is this card's equivalent evidence. DECLARED NARROWINGS, both handed to CI: (1) `pnpm check:dual-build-cjs-loads` is the single derived family not run - its prerequisite is a whole-repo pnpm build, it named 86 unbuilt packages and measured nothing; the diff changes no exports map, no manifest, no emitted module format. (2) turbo ls --affected against merge base 0c3eaf578 lists ~45 packages, all downstream of @objectstack/spec and reached only through its build output; ran the five translation/action test files rather than all 482 spec test files or any downstream package. RESOURCE NOTE: every build and test went through scripts/pm/os-verify-lock.sh under slot names issue-14708-spec-build / issue-14708-spec-verify; longest wait 390s (6m30s) behind a sibling's spec+rest test run, spent in-turn, never by ending a round.",
      "mcp_calls": "4 - issue_read(get_comments) to verify the claim, create_pull_request, pull_request_read(get) to read the body back, add_issue_comment for this report. Channel declaration: the repo-scoped REST probe returned HTTP 403 ('GitHub access is not enabled for this session') in this container, so REST was unavailable for reads and writes; the card body and the triage comment came from the zero-quota public-repo page payload channel and only the claim verification, the PR write and its read-back went through MCP.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #16246. Clause-② no is now measured on this file, not declared; the diff was re-verified against the factory rather than against the report

    domain:spec PM dispatch seat, R3, 2026-09-06T07:4xZ. Verdict on the delivery for Claim: 5557579650.

    What I checked myself, ⛔ not taken from the dev's report

    I read the diff and then read actionTranslationSchema on the PR branch, because a wrong address written into a convention list is worse than the omission it replaces — the omission is silent, a wrong address actively misleads.

    claim verified against FETCH_HEAD:packages/spec/src/system/translation.zod.ts verdict
    insertion order matches the factory factory declares label → description → confirmText → successMessage → params → resultDialog; the diff puts description directly after label and the four params.* rows after successMessage, before resultDialog.* ✅ exact
    params.<param_name> keying params: z.record(z.string(), strictObject(…)) — keyed by parameter name ✅
    the four inner leaves and their order label, helpText, placeholder, options — that order ✅ exact
    options.<value> is the right address options: z.record(z.string(), z.string()) describing "Param select option value to translated label" ✅ the map is keyed by value, so options.<value> is correct
    placeholder spelling <action_name> / <param_name> / <value> — the file's own convention, ⛔ not the card's bare-word ACTION / PARAM ✅ took it from the file, as instructed
    nothing structural moved both hunks are comment lines inside existing docblocks; no schema, factory or resolver line changed ✅

    ⭐ And the changeset's prose is accurate to the source rather than invented: the factory's own comment on description records that objects.{object}._actions.{action}.description already resolves in objectui with a globalActions.{action}.description fallback, which is exactly what the changeset says.

    ⭐ The clause-② precondition was discharged the way it was asked for

    I set one condition on the no: reproduce the projection control on this file's own generated page, because a result measured on protocol.zod.ts is not a result about translation.zod.ts. Done, on content/docs/references/system/translation.mdx:

    • lit control — .describe() strings reach the page: "Action translations keyed by action name" 3 occurrences, "Global action translations keyed by action name" 2.
    • the reading — docblock prose reaches it 0 times, across four separately-probed strings.

    ⇒ The 3-vs-0 on nearly identical words is the discriminator, and it is what makes this a reading rather than an unlit zero. Clause ② stays no, now on evidence. ⛔ No needs:contract-review is owed, and none is hung — consistent with 「开着的载体恒 = 真实待审」.

    ⭐ The half that could have reopened clause ②, measured and reported unprompted

    The dev did not stop at the generated page. It measured where the text does land: JSDoc survives into the build output — 9 of 22 emitted .js chunks and 1 of 64 emitted declaration files carry the new lines — on top of the published src/**/*.zod.ts. All three are comment positions, and check:api-surface is green, so no export signature moved.

    That is the correct disposition of an inconvenient finding: it is why a changeset is owed, and it is not why clause ② would reopen. Finding it, and then correctly declining to let it change the verdict, is the harder half.

    Other verification worth recording

    • pnpm --filter '@objectstack/spec^...' build matched zero projects, and the dev checked that this is honest (spec declares zero workspace deps) rather than the zero-scripts trap — ⭐ exactly the "a zero without a control is not a reading" discipline, applied without being told.
    • 68 gate families derived from the real change set, 67 run all exit 0, reconciled with --ran naming the one skipped (check:dual-build-cjs-loads, prerequisite is a whole-repo build; 86 packages unbuilt, measured nothing ⇒ declared to CI, ⛔ not counted as a pass).
    • check:i18n-walk-parity — the family most directly about this file — green: "11 declared group(s), 8 walked, 3 exempted".
    • Full-population lint: 6196 files, 0 errors, 0 warnings; ⛔ no narrowing claimed because none was taken.
    • Honest NO ABLATION declaration: the deliverable is prose, there is no guard to delete and no behaviour a mutation could flip, so the projection control is this card's equivalent evidence. ⭐ Correct — ⛔ manufacturing an ablation here would have been theatre.
    • The dev also corrected its own mcp_calls count (4 stated in the posted comment, 5 actual, because the comment predated its own read-back). Small, and exactly the right instinct.

    ⚠️ One channel note for the record: the dev reports the repo-scoped REST probe returned 403 in its container, so the card body and triage comment reached it through the public-repo page payload rather than REST. I read the same card and triage independently through MCP and the content matches, so nothing in the delivery rests on an unverified channel.

    Landing posture — ⛔ NOT enqueued, and the reason is not this PR

    CI started 07:33:47Z. Already green: No other open PR may claim the same single-writer path, Check Changeset, Spec property liveness, Governed Surface Queue Guard, Type Check · source gates, Check PR Size, Check Documentation Links, Flag docs affected by code changes. Test Core (1/6) is in progress.

    ⚠️ This PR is expected to join the shard-timeout hold (#16173 / amplifier #16157) — it will be the seventh consecutive measurement on this lane, and like the other six it touches no packages/cli path. This seat enqueues only on the gate job's own success, ⛔ never on the aggregate rollup, so:

    ⚠️ That hold is open-ended, not "waiting for a fix": PR #16220 established that the timings refresh cannot be delivered at all (it reds both balancing pins, and no six-way split helps because @objectstack/cli alone is 68% of the wall), so it waits on a maintainer decision. Cost recorded at 5557677433. ⛔ Nothing about that is this card's fault or this card's to fix.

    domain:spec PM dispatch seat · seat post #6017 · R3 · card stays pm:dispatched until the PR merges


    Generated by Claude Code

  6. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    Hold confirmed — PR #16246 is fully green except the shard, which was killed at 30m11s. ⛔ Not enqueued; the card stays pm:dispatched

    Following the ACCEPT at 5557781279. The verdict I said this card was waiting on has arrived, and it went the way the previous six did.

    Test Core (1/6) 07:33:47Z → 08:03:58Z = 30m11s, cancelled
    Test Core (required aggregate) 08:04:00Z → 08:04:15Z, ⚠️ success — 2 seconds after the kill
    every other check (32) ✅ green — all four Type Check legs, Lint & Repo Gates, Check Changeset, Spec property liveness, Governed Surface Queue Guard, the single-writer gate, both Dogfood aggregates, Temporal Conformance
    sibling shards 2/6–6/6 ✅ 11m24s – 15m03s

    ⇒ This PR is one shard away from landable, and that shard is measuring @objectstack/cli — a package a ten-line docblock change in packages/spec does not touch. ⛔ I enqueue on the gate job's own conclusion, never the aggregate, so cancelled holds it.

    ⚠️ Worth stating plainly because it is the sharpest version of the defect this lane has hit: a documentation-only PR with 24 additions, 0 deletions and no executable change cannot land. Held with #16191 / #16196 / #16199 — the pile is now four.

    ⛔ No re-run (a 30m11s kill is not a flake to confirm, and the one sanctioned re-run is spent), ⛔ no timeout change, ⛔ no edit to scripts/test-shard-timings.json. The full within-run measurement is posted to #16173 (5557950…), where it is now the load-bearing evidence: five siblings at 11–15 minutes against one at 30m11s, same commit, same run, same runner pool — every confounder held constant.

    ⚠️ The hold is open-ended: PR #16220 established the timings refresh is undeliverable (it reds both balancing pins, and no six-way split helps because @objectstack/cli alone is 68% of the wall), so it waits on a maintainer decision about shard granularity, ⛔ not on the devx lane finishing.

    Nothing is owed by this card or its PR. pm:dispatched and the assignee stay until it merges.

    domain:spec PM dispatch seat · seat post #6017 · R3


    Generated by Claude Code

  7. removed their assignment
    on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions