Skip to content

Commit 987ff33

Browse files
docs(skills): teach that a save and os validate refuse titleFormat (#30096)
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J <!-- claude-projects-attribution: {"github_login":"huangyiirene"} --> _Requested by **Irene** · [project thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67x7p5hEJsCWtDpFXteTCt847)_ Fixes #29506 Clause-②: no Metadata-protocol: none The maintainer waived the model-tier mandate for `skills/**` while that tier had no quota; this PR was developed below it. ## 维护者速读 - 改了什么:两个技能不再把 `titleFormat` 讲成「已弃用但未移除」,改为「保存和 `os validate` 会拒绝它(`title-format-retired`),改用 `nameField`」;ADR-0079 D3 加一条带日期的修订说明,引用裁决 6574393684 和 PR #29499。决定本身不动。 - 为什么改:PR #29499 已让保存门和 `os validate` 拒绝写了 `titleFormat` 的对象,三处文本还在教旧规则(「advisory, not an error」)。 - 风险与回滚:纯文本 + changeset,无代码改动。两个技能文件都变小(data 6119 → 6116 tokens,formula 5956 → 5950),无 re-wrap,上限未动。回滚即 revert。 - 先看:`skills/objectstack-data/SKILL.md` 的 `titleFormat` 行;`docs/adr/0079-record-display-name.md` D3 下的 Amendment 块。 - 需要你做的:Tier H 受管面(`skills/**`),契约复审后需要 `GOVERNED_APPROVERS` 批准;PR 保持 draft。 ## What changes The refusal is read off main, not the card: `packages/lint/src/validate-record-title.ts` (`validateTitleFormatRetired`, severity `error`, rule `title-format-retired`, registered in `authoring-rules.ts` with `surfaces: CLI_AND_RUNTIME`) and `packages/spec/src/migrations/entries/semantic/18.object-title-format-refused.ts` ("`os validate`, `os build`, `os lint` and the metadata save gate (`422 INVALID_METADATA`) refuse the key as `title-format-retired`, an error"). - `skills/objectstack-data/SKILL.md`, the `titleFormat` row of the optional-properties table: - Before: `**Deprecated, not removed (ADR-0079)** — render-only: the server can't return or query it. Use `nameField` (it wins); a composite title follows the `nameField` row` - After: `**Refused (ADR-0079)**: a save and `os validate` refuse it (`title-format-retired`). Move it to `nameField`; a composite title follows the `nameField` row` - The render-only reason is dropped: its home is `os explain title-format-retired` (the lint fix line points there, `validate-record-title.ts` [#22161] comment) and ADR-0079 D3. "(it wins)" is dropped because precedence no longer matters once the key is refused. - `skills/objectstack-formula/SKILL.md`, the `template` row's carriers cell: `Object.titleFormat` (deprecated → `nameField`, ADR-0079) becomes `Object.titleFormat` (a save and `os validate` refuse it; move it to `nameField`, ADR-0079). Paid by deleting the sentence below the table "a template holds a path and a formatter, nothing else.", which restates the same row's Grammar cell (`{{ path }}` or `{{ path | formatter[:arg] }}`, no conditionals). "Move logic into a CEL field" stays; the two lines around the cut are otherwise byte-identical, no re-wrap. - `docs/adr/0079-record-display-name.md`: a dated amendment block directly under the D3 heading, citing ruling 6574393684 (#29007, letter B) and PR #29499 (`3557366c02`). It says the key is *refused, not removed* (the schema still parses it, so a stored object keeps loading until its next save), which keeps the wording consistent with Open question 3's remark that "retired" is used for a key that is still live. It names the "deprecated and lint-warned" and "advisory, not an error" sentences as describing the code before #29499, and notes D8's "never errors" no longer covers `title-format-retired`. D8 carries a one-line `Amended (2026-11-04, #29506)` pointer back to the D3 note (D8's own text is unchanged), and the status line gains `· **Amended** (2026-11-04, #29506, ruling 6574393684 …)` in the ADR-0045 form. D3's decision and its quoted citation are left as they are. - `.changeset/29506-title-format-refused-teaching.md`: `'@objectstack/skills': patch`, with `Clause-②: no` and `Metadata-protocol: none`. ### Token ratchet (no ceiling row moved, no re-wrap) | file | tokens before / after / ceiling | bytes before / after | |:--|:--|:--| | `skills/objectstack-data/SKILL.md` | 6119 / 6116 / 6123 | 24493 / 24482 | | `skills/objectstack-formula/SKILL.md` | 5956 / 5950 / 5997 | 23842 / 23819 | ## Why PR #29499 made a save and `os validate` refuse an authored `titleFormat` (ruling 6574393684). The skills still taught "deprecated, not removed" and the ADR still said "advisory, not an error", so an author following them would write a key the platform refuses. ## Verification Head `f27efd1d` (two commits on `d79eecfe`: `c2236e37`, then the S1 ADR follow-up). <details><summary>Local gates on f27efd1 (and on c2236e3 before it): 50 derived, 50 run with exit 0, 0 NOT MEASURED (`dispatch-gates --ran`)</summary> - `pnpm install --frozen-lockfile` and a full `pnpm build` (exit 0) ran first (on c2236e3; f27efd1 touches only the ADR, so the build was reused), then every command from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, each with a 900 s timeout and its exit code captured before any pipe. - All 50 exited 0, among them `check-skills-token-ratchet.mjs` (and `--self-test`), `check-adr-links.mjs`, `check:adr-anchors`, `check:adr-index`, `check:adr-symbol-anchors`, `check-changeset-no-major.mjs`, `check-empty-changeset.mjs`, `check:changeset-gate-self-tests`, `check-comment-mask-corpus.mjs`, `check:corpus-claim-drift`, `check:cross-package-test-inputs`, `check:doc-authoring`, `check:doc-formula-expressions`, `check:skill-docs`, `check:skill-identifier-liveness`, `check:skill-frame-sync`, `check:skill-compatibility`, `check:dts-closure`, `check:dual-build-cjs-loads` and `check:role-word`. - `--ran` printed: `50 derived famil(ies) accounted for — 50 run, 0 NOT-MEASURED`. - `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD`: exit 0. - Writes: two `git push` (c2236e3, f27efd1); one relay `pr_create`; no GitHub MCP write. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's repo-wide run. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the repo's own Chinese reading aid). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J8VVAkUoyAGLYtHWmyX2YW --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1f13eb3 commit 987ff33

4 files changed

Lines changed: 31 additions & 5 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): teach that a save and `os validate` refuse `titleFormat`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- `objectstack-data` (object properties table) and `objectstack-formula` (template carriers) no longer call `titleFormat` deprecated: since #29499 the metadata save gate and `os validate` refuse an object that declares it (`title-format-retired`), and it moves to `nameField`. The formula skill drops the sentence that restated the template grammar cell beside it.
11+
- ADR-0079 D3 gains a dated amendment note citing ruling 6574393684 and #29499; the decision is unchanged.

‎docs/adr/0079-record-display-name.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0079: The record display-name contract — `nameField` is the one canonical primary-title pointer
22

3-
**Status**: Accepted — implemented. **This file is a RETROACTIVE RECONSTRUCTION written 2026-08-08**, not a contemporaneous record; see [Provenance](#provenance-read-this-before-citing-this-file) before you cite it.
3+
**Status**: Accepted — implemented. **This file is a RETROACTIVE RECONSTRUCTION written 2026-08-08**, not a contemporaneous record; see [Provenance](#provenance-read-this-before-citing-this-file) before you cite it. · **Amended** (2026-11-04, [#29506](https://github.com/objectstack-ai/objectstack/issues/29506), ruling of record [`6574393684`](https://github.com/objectstack-ai/objectstack/issues/29007#issuecomment-6574393684) on #29007, letter B) — `title-format-retired` is an error since PR #29499; see the note under D3.
44
- **Decided**: 2026-06-28 (the decision), reconstructed 2026-08-08 (this file)
55
- **Reconstructed by**: #6634
66
- **Implemented by**: [#2434](https://github.com/objectstack-ai/objectstack/pull/2434) (foundation — `nameField` + resolver), [#2458](https://github.com/objectstack-ai/objectstack/pull/2458) (designate-only provisioning at the registry seam), [#2463](https://github.com/objectstack-ai/objectstack/pull/2463) (the author-time gate)
@@ -126,6 +126,18 @@ output, describe marks the old key deprecated."*
126126

127127
### D3 — `titleFormat` is retired in favour of `nameField`
128128

129+
> **Amendment (2026-11-04, #29506).** The diagnostic is no longer advisory.
130+
> Under the ruling of record on #29007 (6574393684, letter B), PR #29499
131+
> (`3557366c02`) made `title-format-retired` an **error**: the metadata save
132+
> gate (`422 INVALID_METADATA` on an active save, a draft promotion and a
133+
> restore) and `os validate` / `os build` / `os lint` refuse an object that
134+
> declares `titleFormat`, and the fix is `nameField`. The key is refused, not
135+
> removed: the schema still parses it, so a stored object that carries it keeps
136+
> loading and reading until its next save. The decision below stands; the
137+
> "deprecated and lint-warned" and "advisory, not an error" sentences and their
138+
> citation describe the code before #29499, and D8's "never errors" no longer covers this rule
139+
> (`packages/lint/src/validate-record-title.ts`, `validateTitleFormatRetired`).
140+
129141
`titleFormat` is deprecated and lint-warned, and an explicit `nameField` takes
130142
precedence over it. It **still parses** — existing metadata keeps loading — so
131143
the diagnostic is advisory, not an error:
@@ -222,6 +234,9 @@ seam (#2434's "Staged (deliberate)" section, kept as the `TODO(ADR-0079)` that
222234

223235
### D8 — The author-time gate is advisory by design
224236

237+
> **Amended (2026-11-04, #29506).** `title-format-retired` is an error since #29499; see the
238+
> amendment under D3. This section's "never errors" and its quotation now describe `title-unresolvable` alone.
239+
225240
`@objectstack/lint`'s `validate-record-title` reports two warnings —
226241
`title-format-retired` (D3) and `title-unresolvable` (`objectTitleCompleteness`
227242
returns `status: 'none'`) — and **never errors**:

‎skills/objectstack-data/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ the publish door rejects it before that:
8787
| `datasource` | `'default'` | Target datasource ID for virtualized data |
8888
| `nameField` | derived (e.g. `'name'`/`'title'`) | **Canonical** record-title field — the stored field used as the record's display name. Use a single text/email field; a composite title to import, sort or search by is a stored computed field (a non-`formula` field with an `expression`); a text formula is display-only |
8989
| `displayNameField` | — | **Deprecated** alias for `nameField` (still honored as a fallback) |
90-
| `titleFormat` | — | **Deprecated, not removed (ADR-0079)** — render-only: the server can't return or query it. Use `nameField` (it wins); a composite title follows the `nameField` row |
90+
| `titleFormat` | — | **Refused (ADR-0079)**: a save and `os validate` refuse it (`title-format-retired`). Move it to `nameField`; a composite title follows the `nameField` row |
9191
| `enable` | — | Capability flags (trackHistory, searchable, apiEnabled, etc.) |
9292
| `fieldGroups` | — | Ordered list of logical field groups for forms/detail pages (see [Field Groups](./references/examples-objects.md#field-groups-mvp)) |
9393
| `lifecycle` | `record` semantics (permanent) | Data retention/rotation/archival contract. **Required for append-only, high-write-rate objects** — a `telemetry`/`transient`/`event`/`audit` class must declare a bounding policy or parsing fails (see [Data Lifecycle & Retention](./rules/lifecycle.md)) |

‎skills/objectstack-formula/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,11 @@ a bare string (auto-wrapped) or their helper, and read the same variable scope.
456456
| Dialect | Helper | Grammar | Carriers |
457457
|:---|:---|:---|:---|
458458
| `cron` | `` cron`0 6 * * MON` `` | 5- or 6-field cron, or one of `@yearly` `@annually` `@monthly` `@weekly` `@daily` `@hourly` `@reboot` | `Job.schedule.expression` (canonical) |
459-
| `template` | `` tmpl`Hello {{ record.first_name }}` `` | `{{ path }}` or `{{ path \| formatter[:arg] }}` — double braces only, no conditionals; the formatter whitelist is `TEMPLATE_FORMATTERS`, exported from `@objectstack/formula` | `system/email-template` `subject` / `bodyHtml` / `bodyText`, `ai/model-registry` `promptTemplate.system` / `.user`, `Object.titleFormat` (deprecated → `nameField`, ADR-0079) |
459+
| `template` | `` tmpl`Hello {{ record.first_name }}` `` | `{{ path }}` or `{{ path \| formatter[:arg] }}` — double braces only, no conditionals; the formatter whitelist is `TEMPLATE_FORMATTERS`, exported from `@objectstack/formula` | `system/email-template` `subject` / `bodyHtml` / `bodyText`, `ai/model-registry` `promptTemplate.system` / `.user`, `Object.titleFormat` (a save and `os validate` refuse it; move it to `nameField`, ADR-0079) |
460460

461461
`shared/expression.zod.ts` declares both surfaces and their carriers.
462-
Missing template paths render as the empty string. Move logic into a CEL field:
463-
a template holds a path and a formatter, nothing else. A `currency` argument is a
462+
Missing template paths render as the empty string. Move logic into a CEL field.
463+
A `currency` argument is a
464464
literal code (`currency:EUR`); only an email template may read it from the send's
465465
data instead, `{{ amount | currency:@currency_code }}`.
466466

0 commit comments

Comments
 (0)