Skip to content

test(service-automation): characterize a config-less wait / boundary_event node at run time - #17937

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-17843-wait-node-absent-config-measurement
Sep 13, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-17843-wait-node-absent-config-measurement

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #17843

Clause-②: no

Refs objectstack-ai/objectui#9109 (that card's grade depends on the reading below; nothing over there gates this one).

What this is

A measurement, converted from a source reading into an observed run. Executor behaviour is
deliberately unchanged — the card says so itself, and the ?? 'timer' fallback in
wait-node.ts carries a comment declaring it intentional.

The triage seat answered the card from source (comment 5651444086). This PR runs it. The run
agrees with that reading on the wait half, so the p1 escalation on objectui#9109 stands and
needs no rollback. The boundary_event half — which triage explicitly did not measure — comes
out the opposite shape, which is exactly why the card refused to extrapolate.

The four readings, as recorded

All four come from real engine.execute() runs through AutomationEngine, with the node-type
vocabulary sealed (what AutomationServicePlugin does at kernel:bootstrapped), a job service
present, an InMemorySuspendedRunStore attached, and one recording logger behind both the engine
and the plugin context so "any log line at any level" is answerable.

1. wait, no config block at all

node:                { id: 'pause', type: 'wait', label: 'Wait' }
executor return:     { success: true, suspend: true, correlation: 'timer:pause', output: undefined }
engine result:       { success: true, status: 'paused', runId: 'run_caa5b321-...' }
scheduled jobs:      []
stored variables:    { previous: null, $runId: 'run_caa5b321-...', $flowName: 'f', $flowLabel: 'F' }
                     -- no 'pause.waitUntil' key
log lines during run: []
nodes that ran:      [ 'before' ]        -- the run reached the node and never got past it

⇒ the 'timer' default applies, the timer branch computes no deadline, no wake-up job is armed
even though a job service was available
, no waitUntil is persisted, and not one log line
is emitted at any level
. The run parks until something external calls resume(runId).

⚠️ One correction to the source reading's rendering, found only by running it: output is a
present key carrying undefined, not an absent key. A JSON dump of the return value drops it
and reads as the second. The test pins it with toStrictEqual for that reason — toEqual cannot
tell those two apart.

2. CONTROL — the same node WITH eventType: 'timer' and timerDuration: 'PT1H'

executor return:     { success: true, suspend: true,
                       correlation: 'flow-wait:run_ab5a389c-...:pause',
                       output: { waitUntil: '2026-09-13T07:38:41.591Z' } }
scheduled jobs:      [ { name: 'flow-wait:run_ab5a389c-...:pause',
                         schedule: { type: 'once', at: '2026-09-13T07:38:41.591Z' } } ]
stored variables:    ... 'pause.waitUntil': '2026-09-13T07:38:41.591Z'

⇒ every channel that read zero above reads non-zero here, so the zeros are a reading of the
absent-config path and not of an inert harness. The one channel that does not discriminate is
status: 'paused' — both halves park the run — and the test records that too, so a later reader
does not mistake it for a signal.

3. boundary_event, no config block at all — measured separately, NOT extrapolated

registered node types: [ 'mark', 'wait' ]      -- nothing registers 'boundary_event'
engine result:       { success: false, status: 'failed',
                       error: "No executor registered for node type 'boundary_event'" }
summary node 'b':    { nodeType: 'boundary_event', status: 'failure', runs: 1, failures: 1 }
at vocabulary seal:  warn -- "Flow 'f' references node type(s) with no registered executor or
                     descriptor ... these nodes fail at execution time with NO_EXECUTOR."
                     meta: { unknownTypes: [ 'boundary_event' ], knownTypes: [...] }
during the run:      info -- "[automation] run flow=f run=... status=failed ... failed=1"
nodes that ran:      [ 'before' ]

⇒ ⭐ the opposite of the wait half: loud, and loud twice. The type has no executor at all
(installBuiltinNodes seeds twelve packs, none of them this one; the package README files
boundary_event as the BPMN interop representation rather than the native authoring model), so
the dispatch fails before any config block could be read. There is no silent suspension on this
branch to find.

4. CONTROL — boundary_event with a fully populated boundaryConfig

Fails identically (success: false, same error string, same step failure) ⇒ the block is
never consulted. A third control puts a registered type in the same slot in the same flow and
the run completes (ran: [ 'before', 'b', 'after' ]), so the failure above is the node type, not
the fixture.

Premise check

The card's premise holds on the tip, verified rather than assumed, and is pinned in the test:
FlowNodeSchema accepts a wait node with no waitEventConfig at all, refuses one whose
block is present with eventType omitted, and accepts a boundary_event node with no
boundaryConfig. So the documents measured above are ones an author can really save.

The durable form

packages/services/service-automation/src/builtin/absent-config-node-characterization.test.ts —
named, titled and headed as a characterization, with the header stating in as many words that
every assertion is a photograph and not an endorsement, that a red here means "the recorded
behaviour moved" (possibly on purpose), and that whether a duration-less timer wait should warn or
refuse is a product question for a successor card rather than anything this file blesses.

Verification

  • pnpm --filter @objectstack/service-automation test — 133 files, 1571 tests, all pass (the
    new file is 7 of them).
  • pnpm --filter @objectstack/service-automation typecheck — exit 0.
  • pnpm --filter @objectstack/service-automation build — exit 0; dependency closure built first
    via pnpm --filter '@objectstack/service-automation^...' build.
  • Gate families derived from the diff with scripts/pm/dispatch-gates.mjs and reconciled with
    --ran: 53 derived, 51 run green, 0 unrun, 2 NOT MEASURED —
    check:dual-build-cjs-loads and check:type-check-debt, both exit 3
    (PREREQUISITE NOT MET: each reads built output for the whole workspace, which lint.yml builds
    before invoking them). Neither is a finding, and this diff emits nothing into any dist/ for
    them to read.
  • pnpm lint (the repo-wide eslint . --no-inline-config) — exit 0, whole population, no
    narrowing claimed.
  • pnpm check:nul-bytes green, plus a direct control-character scan of the added file
    (grep -naP over the C0 set plus DEL) — no matches.

Gate log and reconciliation taken at d5e7d32c3, the final commit.

Changeset

skip-changeset, measured rather than assumed: the package publishes
files: ["dist", "README.md", "CHANGELOG.md"], and after a real build a probe symbol unique to the
added file appears in 0 of those paths, while the positive control rearmSuspendedWaitTimers
appears in 2 files under dist/. Nothing already published moves.

Clause-② re-derived from the delivered diff: no. The diff is exactly one added file, a
*.test.ts under src/, outside files[] and unreachable from the published entry
(exports: { "." } resolves into dist/). It adds no exported symbol reachable from a published
entry and no key on an already-published payload — the file declares no exports at all, and in any
case the grading test is reachability, not the appearance of the word export in a diff.

Acceptance notes

Out of scope for this card, noted rather than filed:

  • The silent permanent suspension itself is confirmed, and is deliberately not repaired here.
    The card states it is not a request to change anything, and the dispatching seat reserved the
    successor question — should a duration-less timer wait warn or refuse? — for itself.
  • output being a present-but-undefined key rather than an absent one is invisible to a JSON
    rendering of the return value. An observation about how the value reads, not a defect.
  • A paused run emits no run-level log line at all, where a completed or failed run gets an
    [automation] run flow=... status=... line. Consistent with "paused is not terminal"; recorded
    because it is the other half of why the absent-config wait is silent end to end.
  • boundary_event parses clean and then fails the run with NO_EXECUTOR. Not filed: the engine
    treats "a node type nothing registered" as a declared, handled state with a startup warning that
    names the type, and the package README declares this type interop-only. Loud by design, not a
    trap.

Generated by Claude Code

…ry_event` node at run time

Converts a source reading into an observed run. `@objectstack/spec` accepts a
`wait` node whose `waitEventConfig` block is absent entirely — the state a
freshly created node is in — while refusing one whose block is present with
`eventType` omitted. What the executor then does with the accepted document had
never been measured.

Recorded, not endorsed:

  - `wait`, no config block: returns `{ success: true, suspend: true,
    correlation: 'timer:<nodeId>', output: undefined }` — the `'timer'` default
    applies, the timer branch computes no deadline, so no wake-up job is armed
    (with a job service present), no `waitUntil` is persisted, and not one log
    line is emitted at any level. The run parks until an external `resume()`.
  - CONTROL, the same node with `eventType: 'timer'` + `timerDuration: 'PT1H'`:
    one `once` job armed, `waitUntil` written, correlation carrying the job's
    name — every channel that read zero reads non-zero, so the zeros are a
    reading of the absent-config path and not of an inert harness.
  - `boundary_event`, no config block: a different execution branch entirely, so
    measured separately rather than extrapolated. No executor is registered for
    the type, so the run fails loudly — `success: false`, `status: 'failed'`,
    `No executor registered for node type 'boundary_event'` — and the sealed
    vocabulary names it before any run.
  - CONTROL, a fully populated `boundaryConfig`: fails identically, proving the
    block is never consulted; and the same flow with a registered type in that
    slot runs to completion, proving the fixture is sound.

Executor behaviour is deliberately unchanged: the `?? 'timer'` fallback carries
a comment declaring it intentional, and whether a duration-less timer wait
should warn or refuse is a product question for a successor card.

Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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.

Coarse fallback — 0 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 84e6b05b6d295f1c744d236921300f447cf7791e → packageMentionDocs.

@os-project-manager os-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 13, 2026 — with Claude
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Contract review

Head reviewed: d5e7d32c3d7df102819816cd5c4b170a62013e62

Implemented-by: claude/issue-17843-wait-node-absent-config-measurement (mode:subagent — the branch, not a session)
Reviewed-by: session_01URLHobLUJB9K1ABV6ofdjj (domain:services execution seat)

① Clause-② — re-derived from the DELIVERED diff, ⛔ not inherited from the claim-time prediction and ⛔ not accepted from the report

Measured in-seat at the reviewed head:

limb of the mechanical floor reading
diff shape git diff --name-status origin/main... ⇒ one line, A packages/services/service-automation/src/builtin/absent-config-node-characterization.test.ts
new exported symbol reachable from the published entry the added file declares 0 exports (grep -cE '^\s*export' = 0); src/index.ts references it 0 times (19 ^export lines, none of them this file); package.json publishes files: ["dist","README.md","CHANGELOG.md"] and exports: { "." } resolving to ./dist/index.js / ./dist/index.d.ts ⇒ src/** is not published at all
new key on an already-published payload no runtime payload is touched — the diff adds no non-test source
changeset git diff --name-only origin/main... -- .changeset/ ⇒ empty, consistent with the skip-changeset label

⇒ Clause-②: no. Both limbs fail. ⭐ The test is reachability from the published entry, ⛔ not the presence of the word export in the diff — recorded because this file would answer no on either reading and that coincidence must not be mistaken for the rule.

② Scope fence — held

The dispatch fenced this card to measure, ⛔ do not repair. Verified: the diff contains no non-test file, so no executor behaviour moved. The 'timer' fallback in wait-node.ts is untouched.

⭐ And the fence earned its keep. The dispatch forbade extrapolating boundary_event from wait; measured separately, it came out the opposite shape — no executor is registered for the type at all, so it fails loudly (NO_EXECUTOR, plus a warn at vocabulary seal naming unknownTypes: ['boundary_event']), and a fully populated boundaryConfig fails identically, proving the block is never consulted. Had the half been generalised, this PR would have reported a silent hang on a branch that is loud by design.

③ Measurement discipline — the controls discriminate

⛔ A zero whose control also reads zero is not evidence. Every channel that reads zero on the absent-config wait reads non-zero on the populated-config control: wake jobs 0 → 1, waitUntil absent → present, correlation inert timer:pause → the job's own name. ⭐ And the one channel that does not discriminate (status: 'paused', true of both halves) is recorded as such, so a later reader cannot misread it as a signal. boundary_event carries its own anti-vacuity control: a registered type in the same slot of the same flow runs to completion.

⭐ The delivery also corrected the source reading it was sent to confirm: output is a present key carrying undefined, not an absent key — a distinction a JSON dump erases and toEqual cannot see, which is why the test pins it with toStrictEqual. The behaviour is unchanged; only its rendering was imprecise. That is the kind of finding only a real run produces, and it is exactly why the card refused to close on source alone.

Gates

53 families derived and reconciled with --ran: 51 green, 0 unrun, 2 NOT MEASURED — check:dual-build-cjs-loads and check:type-check-debt, both exit 3 (PREREQUISITE NOT MET), each reading whole-workspace built output that lint.yml builds before invoking them. ✅ Correctly reported as NOT MEASURED, ⛔ not as passes — exit 3 is a statement about the tree, never a verdict.

Verdict: PASS

⚠️ This record binds to the head it names. If d5e7d32c3 moves, this record is re-issued against the new head — re-verifying alone is not sufficient, and the carrier gate reads a clear bound to a stale head as unbacked.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 13, 2026 07:30
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 225197c Sep 13, 2026
40 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-17843-wait-node-absent-config-measurement branch September 13, 2026 07:52
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…erence (objectstack-ai#17943)

Fixes objectstack-ai#17656

`skills/objectstack-query/rules/filters.md` taught the pre-ADR-0053
upper-bound rule: that the `$lte` form of `{current_year_end}` on a
`datetime` column "stops at midnight on the 31st", and that an author
should hand-write a strict-less-than `{next_year_start}` detour.
`packages/spec/src/data/calendar-day.ts` (ADR-0053 D-D, its table row at
line 20) states the opposite: `$lte`, a `$between` max and a `dateRange`
end mean the WHOLE day and compile to the next calendar day, so the
detour is unnecessary. Because this file is instructions an agent
executes, the wrong rule was being copied into every query written from
it.

## What changed

One block, `skills/objectstack-query/rules/filters.md` lines 249-251,
replaced by the same rule stated by REFERENCE to `calendar-day.ts` /
ADR-0053 D-D, mirroring the vocabulary PR objectstack-ai#17651 landed in
`packages/spec/src/data/date-macros.zod.ts` ("WHOLE day", "reaches the
final instant of Dec 31", "⛔ Do NOT hand-write"). The compile rule is
not restated here: restating it is how the three copies came about
(triage ruling 5651107129 on this card).

## Acceptance greps (whitespace-flattened, content-anchored, on
`be0de0b`)

| anchor | before (`84e6b05`) | after (`be0de0b`) |
|---|---|---|
| `stops at midnight on the 31st` | 1 | 0 |
| `half-open` | 1 | 0 |
| `calendar-day.ts` or `ADR-0053` | 0 | 1 |
| lit control `current_year_end` | 2 | 1 |

## Two readings for `skills/**` (governed; line budget net +0)

| reading | before (`84e6b05`) | after (`be0de0b`) |
|---|---|---|
| `rules/filters.md` lines | 251 | 251 |
| `rules/filters.md` bytes, and tokens as ceil(bytes/4) | 8582, 2146 |
8591, 2148 (ceiling 2149, headroom 1) |
| catalog `skills/**/SKILL.md` line sum (11 files) | 6134 | 6134 |
| `skills/objectstack-query/**` line sum (every file) | 1133 | 1133 |

Re-wrap was not used as currency: the three old lines (206 bytes) became
three new lines (215 bytes); the +9 bytes sit inside the 14-byte slack
the ratchet left. `node scripts/check-skills-token-ratchet.mjs` before:
`filters.md` 2146/2149; after: 2148/2149, exit 0 ("34 authored bundle
file(s) within their ceilings"); `--self-test`: 65 cases pass.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (change set taken from git off merge base
`84e6b05`; 1 path), run on `be0de0b`, every exit code captured before
any pipe, reconciled with `--ran` in the `command :: exit N` form: 23
derived, 23 run, 0 unrun.

| command | exit |
|---|---|
| `node scripts/check-ci-filter-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `node scripts/check-doc-route-spelling.mjs --advisory` | 0 |
| `node scripts/check-doc-route-spelling.mjs --self-test` | 0 |
| `node scripts/check-skills-token-ratchet.mjs` | 0 |
| `node scripts/check-skills-token-ratchet.mjs --self-test` | 0 |
| `node scripts/report-test-timings.mjs --self-test` | 0 |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 |
| `pnpm check:agent-test-spelling` | 0 |
| `pnpm check:corpus-claim-drift` | 0 |
| `pnpm check:cross-package-test-inputs` | 0 |
| `pnpm check:doc-authoring` | 0 |
| `pnpm check:driver-memory-census` | 0 |
| `pnpm check:nul-bytes` | 0 |
| `pnpm check:pm-governed-merges` | 0 |
| `pnpm check:refd-timer-probe` | 0 |
| `pnpm check:role-word` | 0 |
| `pnpm check:skill-compatibility` | 0 |
| `pnpm check:skill-frame-sync` | 0 |
| `pnpm check:skill-identifier-liveness` | 0 |
| `pnpm check:watch-hint-literal` | 0 |

`check:doc-formula-expressions` first exited 3, PREREQUISITE NOT MET
(`@objectstack/formula` and `@objectstack/lint` not built; the gate says
"nothing was measured", not a finding). After `turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint` under the
verify lock (4 tasks, 2m49s, waited 0s) the re-run is the row above:
exit 0, verdict line: `✓ check:doc-formula-expressions (field-level
`*When`, objectstack-ai#11407): 14 predicate(s) on a statically determinable field
layer judged clean; 6 skipped as undeterminable.`

No package build or test is owed: the diff touches no package (`turbo ls
--affected` would list none; the root-level gates above are outside that
graph, which is why they were derived from the path instead). Repo-wide
`pnpm lint` is CI's run.

## Changeset

Measured: no package `files[]` names or globs `skills/`; the only
consumer inside `packages/` is `create-objectstack`, which pulls skills
into a scaffolded project at scaffold time via `npx skills add
objectstack-ai/objectstack/skills` (from git, not from an npm tarball).
Nothing any released npm package ships moves, so `skip-changeset` is
applied as a label and read back.

## Premise checks

- P1 to P5 of the dispatch held on `84e6b05`: the content of lines
249-251, `calendar-day.ts` line 20, the `next_year_start|midnight` grep
hitting only :206 (alias list) and :250-:251 with `current_year_end` = 2
as lit control, PR objectstack-ai#17651's landed wording, and no open PR touching
`skills/objectstack-query/**`. The one PR opened after 06:58Z, objectstack-ai#17937,
touches a single test file under `packages/services/service-automation`.
- One correction: `filters.md` itself carried no `⛔` (0 before the
edit). The marker is in use in the same package (`rules/pagination.md`:
2) and in 6 catalog files, and it is the shape PR objectstack-ai#17651 and the triage
ruling asked to mirror, so it is used here.
- The decision-frame block on this tree matches the dispatch's md5
(`3327d02c56f8a0eca88569dad2270f32`); no fork was hit, so the frame was
not needed.

## Acceptance notes

No out-of-scope findings. Not filed: line 206 still lists
`{next_year_start}` because that is the token alias list, not the rule
(P3); and the file's widest line is 94 columns, a pre-existing style
matter with no bearer.

## Governed surface

`skills/**` is governed (Prime Directive objectstack-ai#14): this PR stays a DRAFT for
the maintainer's hand merge. No seat flips it ready, queues it, arms
auto-merge or submits a review on it.

## 维护者速读(草稿)

**改了什么**:把 `skills/objectstack-query/rules/filters.md`
末尾三行的日期上界规则换掉。原文告诉 agent「`{current_year_end}` 在 datetime 列上只到 31
日零点,要改写成下一年一月一日的严格小于」;新文按 ADR-0053 D-D 引用
`packages/spec/src/data/calendar-day.ts`:上界就是整天,平台已自动补到当天最后一刻,⛔
不要手写绕路。行数 251 → 251,token 2146 → 2148(上限 2149)。

**为什么改**:这个文件是 agent
照做的指令,不是给人读的散文;错规则会被逐条执行,每条查询都带一个多余且与平台行为相反的绕路。同一句错话已在
`date-macros.zod.ts`(PR objectstack-ai#17651)与 `packages/core`
头注释(objectstack-ai#17655,另一张卡)分别修正;本卡是三份拷贝里最后一份,改法按分诊裁决只引用、不重述。

**风险与代价(含回滚)**:纯文档面,不动任何运行时代码与测试;唯一代价是该文件的 token 余量从 3 降到 1。回滚 = revert
本 commit(单文件三行)。

**席位意见**:

**你要做的**:确认措辞后人工直合(governed 面,⛔ 不入队、不 auto-merge);本 PR 首行的关单句会在合并时关闭
objectstack-ai#17656。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_

Co-authored-by: os-project-manager <pm@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…skill (objectstack-ai#17946)

Fixes objectstack-ai#17787

## What changed

`skills/objectstack-i18n/SKILL.md` :196–:201 — one sentence, re-flowed
within its own six lines. It grouped `translation-target-unknown` and
`translation-option-key-unknown` under the single word "warnings"; on
the tree the first is raised at `error` and fails the run, the second
stays a warning. The sentence now names each id with its own severity.
No other file is touched.

Before (flattened):

> `os validate` / `os lint` / `os compile` check this direction and
report it as warnings (`translation-target-unknown`,
`translation-option-key-unknown`): a key naming an object, field, view,
tab, action, param, section, validation rule, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names
that do. …

After (flattened):

> `os validate` / `os lint` / `os compile` check this direction:
`translation-target-unknown` is an **error** and fails the run;
`translation-option-key-unknown` is a warning. A key naming an object,
field, view, tab, action, param, section, validation rule, app, nav
item, dashboard, widget or flow screen that does not exist is listed
alongside the names that do. …

The tail of the paragraph (「A bundle keyed to something since renamed
still parses — …」) is content-identical; only line breaks moved.

## Severities re-measured on the tree (card acceptance item 2)

Measured at `origin/main` `84e6b05` (this branch's base), not against
the card:

| id | severity on the tree | evidence |
|---|---|---|
| `translation-target-unknown` | `error` |
`packages/lint/src/validate-translation-references.ts` :31 (severity
note), :151 `TRANSLATION_TARGET_UNKNOWN_SEVERITY = 'error'`, :155–:156
(field docblock), emitted at :813 and :1265; test
`validate-translation-references.test.ts` :306 「raises
`translation-target-unknown` at `error`, selectable by its exact id」 |
| `translation-option-key-unknown` | `warning` | same file :1210 and
:1229 `severity: 'warning'` beside `rule:
TRANSLATION_OPTION_KEY_UNKNOWN`; test :327 「leaves
`translation-option-key-unknown` at `warning`」 |

The card's wording is still current: the option-key rule has not moved
since the triage grading.

## Acceptance greps (both directions, this branch at `1974cee`)

- `grep -c 'report it as warnings' skills/objectstack-i18n/SKILL.md` →
`0`
- lit control: `grep -c 'translation-option-key-unknown'
skills/objectstack-i18n/SKILL.md` → `1`
- `error` inside the same sentence as `translation-target-unknown`: see
the flattened "After" sentence above — 「… check this direction:
`translation-target-unknown` is an **error** and fails the run; …」

## Line and token readings for `skills/**`

| reading | before (`84e6b05`) | after (`1974cee`) | delta |
|---|---|---|---|
| `skills/objectstack-i18n/SKILL.md` lines | 494 | 494 | +0 |
| `skills/objectstack-i18n/SKILL.md` bytes | 18830 | 18850 | +20 |
| `skills/objectstack-i18n/SKILL.md` ratchet tokens (ceil bytes/4) |
4708 (ceiling 6338, headroom 1630) | 4713 (ceiling 6338, headroom 1625)
| +5 |
| whole package: all `skills/*/SKILL.md` lines summed | 6134 | 6134 | +0
|
| whole `skills/objectstack-i18n/` directory lines | 524 | 524 | +0 |
| ratchet bundle total (whole shipped tree, tokens) | 139594 | 139599 |
+5 |

+0 lines is met by re-flowing only the six lines of the edited sentence
(new widths 91/81/87/92/88/68; the sentence's own pre-edit maximum was
92, at line 200). Nothing outside the sentence was re-wrapped and no
content was deleted to pay for anything. The ratchet prices tokens, not
lines; the ceiling is untouched.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `1974cee` (24 commands — the same 24 the
dispatch clue list named), each run in the foreground with its exit code
captured before any pipe, then reconciled with `--ran`: `24 derived, 24
run, 0 NOT-MEASURED, 0 UNRUN`.

| command | exit |
|---|---|
| `node scripts/check-ci-filter-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `node scripts/check-doc-route-spelling.mjs --advisory` | 0 |
| `node scripts/check-doc-route-spelling.mjs --self-test` | 0 |
| `node scripts/check-skills-token-ratchet.mjs` | 0 |
| `node scripts/check-skills-token-ratchet.mjs --self-test` | 0 |
| `node scripts/report-test-timings.mjs --self-test` | 0 |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 |
| `pnpm --filter @objectstack/spec run check:skill-docs` | 0 |
| `pnpm check:agent-test-spelling` | 0 |
| `pnpm check:corpus-claim-drift` | 0 |
| `pnpm check:cross-package-test-inputs` | 0 |
| `pnpm check:doc-authoring` | 0 |
| `pnpm check:driver-memory-census` | 0 |
| `pnpm check:nul-bytes` | 0 |
| `pnpm check:pm-governed-merges` | 0 |
| `pnpm check:refd-timer-probe` | 0 |
| `pnpm check:role-word` | 0 |
| `pnpm check:skill-compatibility` | 0 |
| `pnpm check:skill-frame-sync` | 0 |
| `pnpm check:skill-identifier-liveness` | 0 |
| `pnpm check:watch-hint-literal` | 0 |

- `check:doc-formula-expressions`: the first run exited 3 (`PREREQUISITE
NOT MET` — `@objectstack/formula` and `@objectstack/lint` are not built
in a fresh worktree), which measures nothing. After `pnpm exec turbo run
build --filter=@objectstack/formula --filter=@objectstack/lint` (the
remedy the gate itself prescribes; run under
`scripts/pm/os-verify-lock.sh`, `VERDICT command-exit 0`) it exited 0.
The build left the tree clean (`git status --porcelain` printed
nothing).
- `check:skill-docs` reads only the SKILL.md frontmatter (the catalog);
this body edit moves no generated listing — it reports in sync, nothing
regenerated.
- Not owed, not run: no workspace package is touched (`skills/` is not
one), so there is no dependency-closure build and no package `test` /
`typecheck` for this diff. The 14 "pending-changeset" families the
derivation lists apply only once a changeset exists; this PR ships none
(measured below).

## Changeset: `skip-changeset`, measured

`skills/**` reaches customer projects at scaffold time through
`packages/create-objectstack/src/skills-install.ts` (`SKILLS_CATALOG =
'objectstack-ai/objectstack/skills'`, pulled from the git catalog by the
skills CLI) — not inside any npm tarball. Measurement on `1974cee`: 70
non-private `package.json` manifests under `packages/`, `examples/` and
`apps/` (node_modules excluded); 0 whose `files[]` mentions `skills`;
positive control: 70 whose `files[]` mentions `dist`.
`create-objectstack` itself ships `["dist", "README.md",
"CHANGELOG.md"]`. No released package's `files[]` ships this path, so
nothing published moves ⇒ `skip-changeset`, applied with the additive
labels endpoint and read back.

## Governed surface

`skills/**` is governed (AGENTS.md Prime Directive objectstack-ai#14). This PR stays a
draft: no seat flips it ready, queues it, arms auto-merge or submits a
review — the maintainer lands it by hand.

## Acceptance notes

- Other carriers of the literal id, re-checked on `84e6b05` with `git
grep -n 'translation-target-unknown' origin/main -- skills content docs
examples`: `examples/app-showcase/test/seed.test.ts:94` (already says
the rule fails) and
`docs/audits/2026-07-app-metadata-reference-integrity-assessment.md:143`
(a dated audit table of counts). Neither is false; nothing filed.
- Open PRs: one was opened after the dispatch's serial reading (objectstack-ai#17937);
its file list does not touch `skills/`.
- noted, not filed: the skill nowhere else states what an
`error`-severity finding does to a run, which is why the sentence spells
out "fails the run" instead of relying on the severity word alone. No
carrier.

## 维护者速读(草稿)

**改了什么**:`skills/objectstack-i18n/SKILL.md` 里的一句话(第 196–201 行)。原句把
`translation-target-unknown` 与 `translation-option-key-unknown`
一起称为「warnings」;现在分别写明:前者是 **error**,会让 `os validate` / `os lint` / `os
compile` 这次运行失败;后者仍是 warning。只改这一句,文件行数 494 → 494。

**为什么改**:PR objectstack-ai#17777 已把 `translation-target-unknown` 提到 error
级(`packages/lint/src/validate-translation-references.ts`),技能文件却还说它是
warning。技能是 AI 作者最先读、而且不会去复核的那份说明——写错等于教 AI 把一个会让构建失败的问题当成可忽略的提示。

**风险与代价(含回滚)**:纯文档改动,不发布任何 npm 包(实测 70 个非 private 包的 `files[]` 无一包含
`skills/`),无运行时影响。技能 token 棘轮 +5(上限 6338,余量 1625),不动上限。回滚 = revert 这一个
commit。

**席位意见**:(留空,待席位定稿)

**你要做的**:确认这句表述与你对两个规则严重度的预期一致,然后人工合并(受管面 `skills/**`,不走队列)。若
`translation-option-key-unknown` 日后也提级,这句话需要再改一次。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_

Co-authored-by: os-project-manager <pm@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants