Skip to content

fix(plugin-audit): the activity record label is the record's ADR-0079 title, with the id as the floor - #21896

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21878-activity-label-display-field
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21878-activity-label-display-field

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21878

Clause-②: no

What changes

plugin-audit's record-change mirror wrote each activity row's record label (record_label, and the label inside the created / deleted / generic updated summary) from a fixed list of field names (name, subject, title, full_name, label, first_name, company, email), falling back to the record id. That list was a second display-name dialect beside ADR-0079's resolveDisplayField, which the same file already used for referenced-record titles. An object titled by company_name therefore showed its record id on every activity row.

Now, in packages/plugins/plugin-audit/src/audit-writers.ts:

  • titleFieldOf(objectName) is the ONE answer for "which field titles a record of this object in an activity row": resolveDisplayField over the registered definition (the cached getObjectDef lookup the file already had, no second lookup), flooring to the id when nothing resolves, when the resolver lands on id, when the title field is a credential (collectMaskedReadFields) or internal (collectInternalReadFields). Those three skips were inline in resolveLookupTitles; they moved here, and resolveLookupTitles now asks titleFieldOf too.
  • recordLabel(record, titleField, id) reads that field's value from the same masked ledger view as before. A null, structured or blank value floors to the id; it never borrows another populated field. The fixed key list is deleted, not extended.
  • The field answer the activity row declares as the label's source (the declaration the read-side redaction keys on) is the resolved field.

No packages/spec edit, no change to the read-side gating, no change to the comment hooks, no backfill: rows already written keep their stored label.

Measured, per the dispatch's hypotheses

  • H1, held. recordLabel(record, id) at audit-writers.ts:299 (base e085a8c3) picked from the 8-key list and took no object definition. One caller, the activity mirror. Red pin on a company_name-titled object: with the key list put back (ablation below) the new pin reads expected '3JO4ITH0my2YkCtR' to be 'ARDCOMPANYONE41' — the card's symptom.
  • H2, held. The one caller already had getObjectDef (the cached engine.getSchema lookup the referenced-title path and the milestone path use). It is reused; no second lookup.
  • H3, measured. Over the bundled example objects, each registered through a real ObjectQL registry so the answer is the materialized nameField (registered answer equals authored answer for all 32): 27 keep their label field; 5 change. The "before" column models the list as "first list key the object declares".
object before after
crm_contact full_name (a formula with no returnType: 'text', so ADR-0079 does not derive it) first_name, its registered nameField
crm_opportunity_line_item id product
showcase_expense_line id merchant
showcase_ext_order id customer_id
showcase_invoice_line id description

The same measurement over 50 platform objects (@objectstack/platform-objects): 24 move from the id to their declared nameField, and 3 change field to it — sys_email_template name to label, sys_invitation email to display_title, sys_scim_connection_credential label to connection_id. In every case the new field is the one the record page titles the record by. Objects titled by name / title / subject are unchanged.

  • H4, held. activity-field-redaction.ts redactActivityRows serves record_label (and the summary that interpolates it) whole only when every declared source field is served to the reader. The declaration now names the resolved field, so a reader not served company_name is served neither; pinned in the unit file and at the REST door.
  • H5, held. Nothing rewrites stored rows; only rows written from now on change.

Tests

All at head e0da4f8a5d.

  • pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2: 40 files, 641 tests passed.
  • pnpm --filter @objectstack/plugin-audit typecheck: exit 0, test layer included (check:test-typecheck OK, 0 debt). Run at 1088327afd; git diff 1088327afd e0da4f8a5d -- packages/plugins/plugin-audit is empty.
  • Dogfood, --project isolated: the new activity-label-display-field.dogfood.test.ts plus the sibling activity-field-values.dogfood.test.ts: 2 files, 14 tests passed, against a rebuilt plugin-audit dist/. pnpm --filter @objectstack/dogfood typecheck: exit 0.
  • New unit file audit-record-label-display-field.test.ts (real kernel, ObjectQL, SQLite driver, AuditPlugin; the security service is the one stand-in): company_name labels on create / update / delete with company_name declared as the source; name, title, subject labelled as before; an explicit nameField wins over name; a blank title and an empty name beside a populated title both floor to the id; a credential title floors to the id; a reader not served company_name gets neither label nor summary, and the control keeps both.
  • Fixture triage in audit-writers.test.ts: four localized-summary cases wrote a record of person_qualification, an object the fake engine never declared, and passed only because the key list guessed name from the record. Disposition: declare the object (titled by name), since a real engine writes only registered objects. Assertions unchanged.
  • Narrowed lint: eslint --no-inline-config --format json over the four changed TypeScript files: 4 files, 0 errors, 0 warnings. All four sit inside the config's linted population (--print-config resolves each). eslint.config.mjs enables no type-aware linting, so this diff cannot move a verdict on an untouched file. pnpm lint itself is CI's.

Ablation: the fixed key list put back

Through scripts/ablation-replace.mjs (anchor hit 1, blob 47497d1d8eed to 61d82281885f), committed fix first.

  • Unit, source-resolved: 7 red, 4 green, as predicted. Red: both company_name label cases, the explicit nameField case, the empty-name floor, the credential floor, and both read-side cases (the label at rest was the id). Green, as predicted: name / title / subject, the blank-company_name floor, the registry control. Restore: blob after restore 47497d1d8eed equals HEAD, git diff HEAD empty.
  • Dogfood, dist-resolved: mutate, rebuild plugin-audit, ablation-dist-preflight found the marker in both JS bundles, then 2 red (the company_name label case; the withheld reader was served a record_label) and 2 green (the name case, the empty-title floor). Restore: source blob equals HEAD, rebuilt, ablation-dist-preflight --absent exit 0 (marker absent from all 6 built files, tree clean), then the green dogfood run above.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at e0da4f8a5d: 70 commands, all run, and the --ran reconciliation reads 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN. Two first readings were build-state, not this diff: check:dts-closure exited 1 on @objectstack/hono's declarations while my own closure build was rewriting that dist/ (re-run: 0, 169/169 present), and check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET, on packages this worktree had not built yet (re-run after the full build: 0).

Acceptance notes

  • crm_contact's activity label moves from the full name to the first name, because the example's full_name formula declares no returnType: 'text' and its registered title is first_name. The activity label now agrees with the record page. Whether the example should title contacts by full_name is the example's call. Carrier: none.
  • A formula title (display_title on several platform objects) is declared as the label's only source, though its value is computed from other fields. A reader served the formula field is served the same value by the data plane, so the label serves nothing the data plane does not.
  • The writer's per-installation object-definition cache (getObjectDef, getFieldDefs) is never invalidated, so a definition changed at runtime may be read stale by the label, milestones and enable.activities alike. This was read from the source, not measured, and it predates this change. Carrier: none.
  • origin/main advanced 4 commits past the base e085a8c3; none touches a file of this diff or what the gate derivation reads. Not merged; the queue rebuilds on current main.

Generated by Claude Code

claude added 7 commits October 5, 2026 15:21
…le with the id floor

Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-audit, touching 4 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 607463d736046d8d692d64cf16ea6804d1609f38.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 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 607463d736046d8d692d64cf16ea6804d1609f38 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cbc4bbfbd3eb57be033b4a19c9436d39cba6570d — the merge of head e0da4f8a5d07a1461ca1a23f8a90df0e80383115 into base 607463d736046d8d692d64cf16ea6804d1609f38, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cbc4bbfbd3eb57be033b4a19c9436d39cba6570d && git checkout cbc4bbfbd3eb57be033b4a19c9436d39cba6570d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 607463d736046d8d692d64cf16ea6804d1609f38 e0da4f8a5d07a1461ca1a23f8a90df0e80383115 && git checkout -B drift-repro 607463d736046d8d692d64cf16ea6804d1609f38 && git merge --no-ff e0da4f8a5d07a1461ca1a23f8a90df0e80383115

node scripts/docs-audit/affected-docs.mjs --json 607463d736046d8d692d64cf16ea6804d1609f38

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 607463d736046d8d692d64cf16ea6804d1609f38 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 17:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 17:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Re-queued once · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T17:30Z

The merge queue removed this PR at 2026-10-05T17:28Z. Its merge group's TypeScript Type Check aggregate failed only because the Type Check · consumer gates lane was cancelled. That lane hit the job's 20-minute execution limit inside "Build workspace packages", before any gate step ran, after its checkout alone took about 6.5 minutes (17:08 → 17:14:35). No test, typecheck or gate reported a finding.

  • The same lane passed on this PR's head e0da4f8a within the limit. The merge groups queued just before and after this one passed.
  • It died before any check body ran, so this is the one re-run the rules allow. If the group fails again, the seat treats it as real and does not re-queue.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit b238856 Oct 5, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21878-activity-label-display-field branch October 5, 2026 18:09
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…decision in words instead of a tracker number (stage 21) (objectstack-ai#21907)

Part of objectstack-ai#20749
Clause-②: no

Stage 21 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the second name-ordered `ui/` group: the test files
directly under `packages/spec/src/ui/` from
`component-record-blocks.test.ts` to `dashboard.test.ts`. Those files
carried 103 messages and 109 tracker-shaped ids, citing 58 records. 105
of those ids now either state what their record decided, in words (form
D), or are dropped where the title already says it. Four stay: they are
CSS colour literals in two widget fixtures, not citations (below). Text
only: no assertion, identifier, test count or code comment changes, and
no file is renamed.

## Census at the base (`1e18a0735c`)

Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`),
`census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs`
(md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5
`dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages
10 to 20 used. A literal counts as a test title when its folded message
is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` /
`.only` chains included. Everything else is an "other" string.

The base is `1e18a0735c`, stage 20's landing and the claim's base. Both
instruments read **764 messages / 807 ids in 159 files**, the seat's
reading and stage 20's head reading.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `ui/` (this PR: 7 of the 53 files) | 53 | 300 / 318 | 284 / 302 | 16 /
16 |
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| `ai/` | 1 | 2 / 2 | 0 | 2 / 2 |
| `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 |
| **total** | **159** | **764 / 807** | **710 / 752** | **54 / 55** |

The group reads **103 messages / 109 ids in 7 files**, the seat's
figures file for file:

| file (under `ui/`) | messages / ids | titles | other |
|:--|--:|--:|--:|
| `component-record-blocks.test.ts` | 6 / 6 | 6 / 6 | 0 |
| `component-reference-rail.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `component-type-vocabulary.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `component.test.ts` | 65 / 70 | 65 / 70 | 0 |
| `dashboard-chart-structure-refusal.test.ts` | 2 / 2 | 0 | 2 / 2 |
| `dashboard-compareto.test.ts` | 5 / 5 | 5 / 5 | 0 |
| `dashboard.test.ts` | 20 / 21 | 17 / 18 | 3 / 3 |
| **7 files** | **103 / 109** | **98 / 104** | **5 / 5** |

`component-report-items-action-members-typed.pin.test.ts` sits in the
same name range and carries no id. The five "other" strings are the four
colour literals and `dashboard.test.ts:205`.

- **Controls.** Lit: `ui/view.test.ts`, outside the group, reads 43 ids
at the base and at the head. Dark: `component.test.ts` reads 0 at the
head while 200 of its comment lines still carry a number. Planted in
scratch copies of head files: an id put into a
`component-record-blocks.test.ts` title reads 1 / 1, and an id put into
a `dashboard-compareto.test.ts` comment reads 0.
- **A wider pattern** (any `#` plus digits) reads the same as the gate
pattern in 6 of the 7 files at the base. `component.test.ts` reads one
more: `decision batch objectstack-ai#77`, a two-digit batch number the gate pattern
does not count. It leaves with the id beside it, as stages 9, 13 and 15
did with theirs.
- **At the head:** 665 messages / 702 ids in 154 files. The 7 files read
4 / 4 (the four colour literals), `ui/` reads 201 / 213, and no other
file moved.

## How the area was chosen

`ui/` has no subdirectory test file with an id, so it is taken in
name-ordered file groups near the ~100-id bound. Stage 20's re-cut named
this group at 109 ids, and this census reads 109, so no re-cut was
needed.

**Named for the next stages** (cut from the head census, 665 / 702):
- `ui/` 213 ids. One is stage 20's kept
`component-props-unknown-members.pin.test.ts:322`, four are this group's
colour literals, and 208 sit in the 45 files after `dashboard.test.ts`.
The next group nearest 100 runs from
`dataset-filter-nested-relation-list.test.ts` to
`view-inline-object-binding.test.ts`: 29 files, 96 messages / 102 ids, 7
of them "other". Cutting two files earlier gives 98. The last `ui/`
group is then the 16 files from `view-item-config-type.test.ts` to
`widget.test.ts`: 100 messages / 106 ids, `view.test.ts` alone 43.
- `api/` 201, two stages. `system/` 165, two. The files directly in
`src/`, 120, one.
- The three docblock needles plus the kept `:322`, one stage, with an
at-tier review.

## The five "other" strings: four kept, one rewritten

- **`'objectstack-ai#111'` / `'objectstack-ai#222'` at
`dashboard-chart-structure-refusal.test.ts:94` and
`dashboard.test.ts:124` are kept.** They are three-digit CSS hex
colours, not placeholders for a record: `colors: ['objectstack-ai#111', 'objectstack-ai#222']` in a
widget's `chartConfig`, and `palette: ['objectstack-ai#111', 'objectstack-ai#222']` inside the
free-form `options` bag. They cite nothing. But they are not input and
expected value at once, the case stage 12 dropped. Each is input that
the schema under test reads (`ChartConfigSchema.colors` is a
string-array or string-map union), and the assertions read other things:
the first parse must succeed, the second asserts `options.stacked`. So
by the claim's rule they stay, and are reported. They match the gate
pattern only because a short hex colour can be all digits. They are the
only four such literals in the whole census.
- **`dashboard.test.ts:205`** is the label argument of the file's
`orderPin` helper, which passes it to `it()`. So it is a test title one
call down, and no assertion reads it. It is rewritten and declared to
the text-only tool.

No string in the group is a needle (an id that is the expected value of
an assertion over a docblock or another file's text). The three known
needles are in `ai/` and `contracts/`.

## What each id became

- **21 literals (21 ids)** now state a decision in words.
- **15 literals (15 ids)** get their subject back in words, where the
number stood for a thing, such as "the objectstack-ai#11661 keys".
- **63 literals (69 ids)** drop a number the title already explains.

Every cited record was read with its comments through REST: 54 answer
200 and 4 answer 404. Four citations are cross-repo: `ui#6206` (also
spelled `ui#6206-B`), `ui#6207` and `objectui#8221` were read from
objectui and answer 200. `framework#2501` was read under the
repository's current name, as stage 18 read `framework#2536`. objectstack-ai#5042 is a
pull request, `objectstack-ai#4001` batch 14. The four 404s were read from what
landed, through the commits the stage-5 comment sweep (objectstack-ai#20576)
re-anchored them to:
- **objectstack-ai#6276:** `78f0be872`, which declares the record picker's flat `sort`
/ `limit` (maintainer ruling 2026-08-08, direction A).
- **objectstack-ai#9972:** `60e0f900a`, which records the live read point of
`page:tabs` `items[].icon` and its accept pin.
- **objectstack-ai#11507:** `88b9d749a`, which declares `sys_activity.type` an open,
author-extensible vocabulary (maintainer ruling 2026-08-24, direction
4).
- **objectstack-ai#11658:** `1a6a19c31`, which opens `RecordActivityProps.types` to
author-contributed kinds, executing that ruling.

Each of those titles already carried its decision, so the number is
dropped. `component.test.ts:3037` also gets its subject back: "the
ruling" becomes "the open-vocabulary ruling".

**Stated in words:**

| record | literal (under `ui/`) | now reads | the decision |
|:--|:--|:--|:--|
| objectstack-ai#8744 | `component-record-blocks.test.ts:355` | "the
`record:discussion` / `record:chatter` pair — one shared row" |
`record:discussion` is wired to the chatter row on purpose: one
renderer, one accept face. |
| objectstack-ai#7702 | `component.test.ts:63` | "accepts a header without title — the
synthesized shape, headed from the record" | Ruling A/B: `title` becomes
optional, and an omitted title means the renderer derives the heading
from the record. |
| objectstack-ai#6776 | `component.test.ts:89`, `:262` | "PageHeaderProps recordChrome
/ showStar / showCopyId — declared because the header renderer reads
them"; "PageAccordionProps variant — declared because the accordion
renderer reads it" | Route A: declare the five author keys objectui's
renderer reads. |
| objectstack-ai#6776 | `component.test.ts:235` | "PageTabsProps tabStyle — renamed
from `type`, which collides with the component `type`" | Route A: rename
`type` to `tabStyle`, because in a flat node `type` is the component's
own dispatch key. |
| objectstack-ai#6946 | `component.test.ts:122` | "PageHeaderProps icon is retired —
no renderer reads it" | Maintainer ruling: retire three keys with zero
renderer read points, this one among them. |
| objectstack-ai#5775 | `component.test.ts:406`, `:2932` | "… items[].value /
items[].count — declared because the tabs renderer reads them";
"RecordPathProps stages[].terminal — declared because the path renderer
reads it" | Ruling A: retire the dead keys and declare the keys the
renderers honour. |
| objectstack-ai#5775 | `component.test.ts:649` | "PageContainerProps — page:section /
page:footer / page:sidebar compose through `children`" | Same ruling:
the three containers declare `children` as their one composition key,
and `body` is not declared. |
| objectstack-ai#11289 | `component.test.ts:768` | "preserves the section presentation
keys the renderer honours, verbatim" | Direction 1: declare `hideEmpty`
/ `collapsible` / `showBorder`; the renderer is unchanged. |
| objectstack-ai#11661 | `component.test.ts:889` | "still refuses `title`,
deliberately withheld as a second spelling of `label`" | Three more
renderer-honoured keys are declared; `title` stays out because `label`
already names the heading slot. |
| objectstack-ai#9220, objectstack-ai#9249 | `component.test.ts:2078`, `:2142`, `:2899`, `:2908` |
"Interactive Elements — element:filter (retired, no renderer)", the same
for `element:form`, and "… through the kept map row (retired, no
renderer)" twice | Both elements are retired at element grain: no
renderer or reader in any repository. |
| objectstack-ai#4001 | `component.test.ts:3325` | "batch A, unknown keys refused —
the prescriptions, each backed by a measured producer" | Every
authorable surface refuses unknown keys; spelled as stage 15 spelled
`objectstack-ai#4001 batch D`. |
| objectstack-ai#7751 | `component.test.ts:3419` | "object-* block props schemas —
declared, so the props gate has a schema to dispatch" | Ruling A: the
`object-*` block family enters `ComponentPropsMap`. |
| `ui#6207` | `component.test.ts:3468` | "object-grid `data` takes the
ViewDataSchema provider object — the two spec authorities converged" |
Option A: `object-grid.data` converges on `ViewDataSchema`, and the bare
array is refused. |
| objectstack-ai#19228 | `component.test.ts:4499` | "row caps on the object-bound
blocks — a bound view fills `limit` only when the authored one is not a
usable cap" | Ruling D: one row bound per view; the component face keeps
an undefaulted `limit`, which a bound view's page size fills whenever
the authored one is not a usable cap (the gate's
`!isUsableRowLimit(authored)`). |
| objectstack-ai#4614 | `dashboard.test.ts:419` | "date-range preset vocabulary — one
source, in the spec" | Ruling A: the preset names move into the spec as
their one source, and a date filter's default is checked against them. |
| objectstack-ai#16458 | `dashboard.test.ts:850` | "control — `columns` still declares
no default … (the renderer infers it from widget spans)" | Item 4 was
not landed: a `.default(12)` would retire the renderer's span inference
and switch every auto-flow dashboard to the positioned grid. |

**Subject back in words** (15 literals): "the objectstack-ai#5068 gate" becomes "the
props gate" (`component-reference-rail.test.ts:34`); "the three objectstack-ai#18305
blocks" / "object blocks" become "object-map / object-gantt /
object-tree" (`component-type-vocabulary.test.ts:88`,
`component.test.ts:4295`); the four "objectstack-ai#11661 keys" titles name
`defaultCollapsed` / `icon` / `description` (`component.test.ts:832`,
`:848`, `:860`, `:875`), because id-free sibling titles already say "the
section presentation keys"; "objectstack-ai#18639 scope fences" becomes "scope fences
of the `columns` widening"; "the twin of the objectstack-ai#14406 census pin" becomes
"the twin of the census pin that no door refuses the rule array"; "the
objectstack-ai#7750 specimen shape" becomes "the my-work specimen shape"; the four
`objectstack-ai#5011 —` prefixes in `dashboard-compareto.test.ts` become `compareTo —`
where the title needs a subject, and go where it already has one
(`:61`); "the words objectstack-ai#5042 measured" becomes "every word authors were
measured spelling … reaches …".

**Dropped where already stated** (63 literals, 69 ids). A number goes
only where the title already says its decision. Examples:
"ComponentPropsMap[\"record:alert\"] (objectstack-ai#8744)"; "user:profile is not
author-placeable (objectstack-ai#14159, ruling B)"; "ai:chat_window is retired,
refused by name (objectstack-ai#21504)"; the six "(objectstack-ai#6276)" picker titles, each already
naming what the declaration does; "the four `object-*` `sort` doors —
one sort orthography, the array (objectui#8221, decision batch objectstack-ai#77,
option B; objectstack-ai#18305)"; the `[objectstack-ai#4876]`, `[objectstack-ai#5010]`, `[objectstack-ai#17779]`, `[objectstack-ai#20958]` and
`[objectstack-ai#21293]` prefixes on `dashboard.test.ts`, each in front of the rule it
names; "drill branch (objectstack-ai#5022): …", whose sibling labels already read "…
branch: …". `批 17` stays as a batch label in stage 18's form, and
`ADR-0021` / `ADR-0049` style citations are untouched.

**No file is renamed.** None of the 7 file names carries a number.

## Readers

- **Test-name filters:** none. No tracked script, workflow or package
config passes `-t` / `--testNamePattern` (the hits are `mapfile -t`,
`docker build -t`, `type -t`, `lsof -t`, and a preflight's option
vocabulary).
- **Snapshots:** none. No `__snapshots__` directory is tracked under
`packages/spec`, and none of the 7 files calls a snapshot matcher.
- **Projects:** all 7 files run in the `local` project;
`packages/spec/vitest.repo-tests.json` lists none of them.
- **By substring:** every old literal, its id-bearing fragment and a
window around each id (301 needles) was searched with `git grep` at the
base, across the tracked tree outside its own file. No gate, doc,
filter, snapshot or `scripts/check-*.mjs` self-test reads one. The 6
hits:
- a code comment in `lint/src/validate-component-props.test.ts:540`
("the objectstack-ai#7750 specimen shape");
- a QA checklist `source` entry,
`docs/qa/platform-checklist/areas/dashboards.json:406`, which anchors on
`dashboard-compareto.test.ts#dashboard` and describes it in its own
words ("objectstack-ai#5011 — compareTo converged on …"). The checklist gate looks up
the `dashboard` symbol, which this PR leaves in the file, and reads none
of the titles. `pnpm check:platform-checklist` exits 0 at the head;
- a sibling title in `service-analytics`
(`dataset-compare-dimension-resolution.test.ts:74`, "objectstack-ai#5011 — …");
- three hits on one same-id title in this card's later `ui/` stage,
`ui/view.test.ts:4236`, the visibility twin of the message-order
describe, citing objectstack-ai#6416 / objectstack-ai#6619.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each changed string leaf must sit in a test-call title
position or on a declared line, must carry a tracker id before, and must
carry no `#` plus digits after. This stage declares one line,
`dashboard.test.ts:205`.

- **Result:** 7 of 7 files SAME on all three legs, with the per-file
counts predicted in writing before the run.
- **Totals:** 99 changed string leaves in 99 literals: 98 titles and 1
declared. The diff's `+` and `-` lines are exactly the 99 planned lines
as multisets, and every file keeps its line count.
`dashboard-chart-structure-refusal.test.ts` is untouched.
- **Controls (13 of 13 as predicted, on scratch copies, each anchor hit
once):** identifier rename DIFF; numeric literal DIFF; comment edit
COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten
title given a new id VIOLATION; a title that was id-free at base edited
VIOLATION; one title reverted to base SAME; an `it.each` row given an id
VIOLATION; an undeclared `orderPin` label changed VIOLATION; a title
re-split into a `+` chain DIFF; the declared label reverted to base
SAME; the declared label given a new id VIOLATION; a kept colour literal
edited VIOLATION. The first run predicted VIOLATION for the
declared-label revert: putting `(objectstack-ai#5022)` back where it was reproduces
the base text exactly, so SAME is the right answer, and a control that
gives the label a new id was added. That run read 11 of 12.
- **Templates and tables:** no `.each` title, `%s` / `$name` placeholder
or table row changes.

**Test counts:** the 7 files were run at the base, in a separate base
worktree, and at the head, with `--project local --project repo`. Both
sides read 677 tests in 7 files, all passed, with the same count and
status sequence per file in 7 of 7. 469 full test names change, and each
changed name equals the base name with the planned replacements applied
(0 mismatches). One full name repeats 5 times on both sides: an
`it.each` row in `dashboard.test.ts` whose printed name is cut at the
same point for 5 rows. Only its describe prefix changed.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the
6 touched files are in it, and no `*.test.ts` at all. The controls
`src/ui/component.zod.ts` and `dist/index.mjs` are in it.
- In the built `dist/`, a new phrase and an old literal each read in 0
files. The control `Unrecognized key` reads in 42.

So this PR publishes nothing, and no changeset is added.

## Verification (at `ec96327761`)

- `pnpm turbo run build` over all packages: 71 / 71, through the shared
verify lock (`VERDICT command-exit 0`).
- `@objectstack/spec`:
  - `vitest run --project local`: 618 files, 18450 passed, 1 todo.
- `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246
errors / 135 pinned signatures held). Its program holds all 7 group
files, counted with `tsc --listFilesOnly -p tsconfig.test.json`.
- `check:generated`: all 15 generated artifacts up to date, against the
`dist/` the build above wrote.
- **Gates:** `dispatch-gates --commands` derived 79 families, the same
set as stage 20, and all 79 exit 0. `--ran` reconciles: 79 derived, 79
run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded.
The five roster families whose rosters sit under a touched directory
were also run, and each exits 0: `check:meta-url-spelling`,
`check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`
and `check:filter-alias-parity`. So does `check:platform-checklist`, for
the checklist entry above.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 7 files
reads 0 errors and 0 warnings. The population comes from ESLint's own
config: 7 configured, 0 ignored. No file sets `parserOptions.project` or
`projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 198 changed lines (+99 /
-99).
- A control-byte scan over the 6 changed files finds none.
- **Review round 1, at `69ea423302`:** two titles reworded, one line
each: the row-cap describe (`component.test.ts:4499`) now states the
gate's guard, and the offset-alias title
(`dashboard-compareto.test.ts:160`) reads straight. Text-only proof
against the base: 7 of 7 SAME, 99 changed leaves (98 title, 1 declared).
The 7 files at base and head: 677 / 677 passed, count and status
sequence identical in 7 of 7, 469 changed full names, 0 mismatches
against the plan. ESLint over the 7 files: 0 errors, 0 warnings. The
group census still reads 4 / 4. `typecheck` exit 0, and
`check:nul-bytes` OK. The derived gate set is the same 79 families; they
were not re-run for a two-literal change.

## `main` since the base

Re-fetched just before this PR opened, `origin/main` was four commits
past the base (`cab6396715`: objectstack-ai#21875, objectstack-ai#21896, objectstack-ai#21893, objectstack-ai#21900). None
touches any of the 7 files. objectstack-ai#21893 touches 8 `packages/spec` files under
`automation/` and `migrations/`, and the census over `packages/spec/src`
at `866683f96f` (after objectstack-ai#21893) still reads 764 / 807 with no file moved,
so the merged tree reads this PR's 665 / 702. objectstack-ai#21900 touches two more
`packages/spec` files, both non-test migration entries, which this
census does not count. So `main` was not merged. `git merge-tree` onto
`cab6396715` is clean.

## Acceptance notes

- **The four colour literals** stay, as above. They are the only
digit-only hex colours in the census, so the card's end state will read
them unless a later stage changes the fixture values, which would be a
fixture change rather than a text change.
- **Same-id test titles in this card's later stages** go with those
stages: 21 lines in `packages/spec/src`, for example
`system/i18n-resolver.test.ts:1982` ("(objectstack-ai#20940)"), `ui/page.test.ts:696`
("(ui#6206-B, objectstack-ai#15442)"), `ui/view-strictness-batch18.test.ts:91` ("objectstack-ai#4001
批 18 — …"), `ui/view.test.ts:4236` ("(objectstack-ai#6416 / objectstack-ai#6619)") and
`ui/view.test.ts:4793` ("(objectstack-ai#19228)").
- **Same-id test titles in other packages** are their lanes' test-string
shares. A search of `describe` / `it` / `test` lines outside
`packages/spec/src` finds 40 lines citing ids this PR handled, in 10
packages: `lint` 25 (8 files), `service-analytics` 3 (1), `cli` 2 (1),
`platform-objects` 2 (1), `plugin-audit` 2 (2), `plugin-security` 2 (2),
and one each in `plugin-sharing`, `rest`, `service-automation` and
`spec/scripts`. Examples:
`lint/src/validate-component-props.test.ts:783` ("… are dispatched
(objectstack-ai#8744)"),
`service-analytics/src/__tests__/dataset-compare-dimension-resolution.test.ts:74`
("objectstack-ai#5011 — …"), `rest/src/meta-types-schema-titles.test.ts:131` ("objectstack-ai#16458
— …"). The two `[objectstack-ai#6206]` hits in `plugin-security` and `plugin-sharing`
cite objectstack#6206, a different record from `ui#6206`.
- **Code comments still carry ids** in these files and their sources,
for example the `objectstack-ai#9198 tombstone` comment in `component.test.ts`, the
`objectstack-ai#19228` header above `component.test.ts:4499`, and the `objectstack-ai#6416` /
`objectstack-ai#5955` docblock above `dashboard.test.ts:163`. Comments are not this
card's share, and none is touched here.

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

---------

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

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants