Skip to content

fix(audit): declare sys_activity.type as an open, author-extensible vocabulary - #11659

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-11507-activity-type-open-vocabulary
Aug 24, 2026
Merged

fix(audit): declare sys_activity.type as an open, author-extensible vocabulary#11659
os-sam merged 2 commits into
mainfrom
claude/issue-11507-activity-type-open-vocabulary

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11507

Executes the maintainer ruling of 2026-08-24 (direction 4, verbatim: 「四维分析一致的,接手你的建议。」): sys_activity.type is an open, author-extensible vocabulary, and the declaration must say so. Nothing here enforces anything — enforcement was direction 3 and it was not ruled.

The defect, restated

Three things were true at once, and only the declaration didn't say so:

  1. type is a select over a fixed list — which normally means "anything else is invalid_option".
  2. Every field on sys_activity is readonly: true, and validateRecord skips readonly fields on both write branches, so that check never runs on this column.
  3. ADR-0052 §5b.2 activityMilestones[].type is z.string().optional() in the spec and is forwarded verbatim by audit-writers.ts (if (milestone.type) activityType = milestone.type) — a shipped, documented, author-facing channel into the column. A third door exists too: an app's own server-side action inserts the row directly, and no grep of this repo can see those sites.

So an author — most often an AI writing metadata — read the option list and learned "another value will be rejected". That was false three ways over.

The mechanism, and why this one

The field's own description.FieldSchema declares no open/closed-vocabulary key — no openVocabulary, no restricted, no allowCustomValues — and inventing one is a packages/spec change, i.e. a different seat's card, which this branch is fenced out of. description is the slot the spec declares for exactly this ("Tooltip/Help text", distinct from placeholder and inlineHelpText), and unlike a source comment the contract carries it: the exported SysActivity is the output of ObjectSchema.create(), i.e. of a real parse, so the sentence travels to the metadata API, the i18n bundles, and whatever an author or an AI reads about this field.

That choice is measured, not asserted: the new pin probes four plausible spellings against ObjectSchema.safeParse and requires all four to be refused. The day packages/spec grows a real declaration for this, that case goes red and tells the next author to move the semantics onto it.

The long reasoning (the three doors, what the ruling binds, what must not be "fixed") lives in the source docblock beside the field; the description carries the short honest version.

What changed

  • sys-activity.object.ts — the type field gains a description stating that the declared options are the built-in set of an open vocabulary, that the sanctioned author channel is activityMilestones[].type (ADR-0052 §5b.2), and that an undeclared value is stored verbatim rather than rejected; plus a docblock recording the ruling and its fences.
  • sys-activity-type-open-vocabulary.test.ts (new) — three cases: the built-in set stays declared (open ≠ undeclared); the declaration carries the open-vocabulary semantics; no spec key exists for the concept.
  • activity-type-vocabulary-enforcement.test.tsevery assertion unchanged. What changed is what a red MEANS: §3 was filed as "a defect, characterized — delete these when enforcement lands"; it now measures a ruled contract, and says that rejecting an author-contributed value is a contract change to re-open [Decision] Is sys_activity.type a closed platform vocabulary or an author-extensible one? Both readings are true of the code today #11507 over, not a fix to adapt the test to.
  • sys-activity-type-vocabulary.test.ts — docblock records the ruling: the census inventories the built-in set; an app's own value belongs to the app that writes it, and declaring one (as sys_activity.type declares 11 options while a shipped producer writes a 12th (scheduled) — readonly fields make the enum unenforceable #11424 did for scheduled) is a deliberate choice to adopt it, never an obligation.
  • Regenerated the four plugin-audit translation bundles (the new description extracts as a fields.type.help leaf) with the command the extract config documents.

The built-in set is untouched — twelve values, scheduled included, exactly as #11424 left it.

Verification

Run at a79cc3b (the head of this branch), heavy steps serialized through scripts/pm/os-verify-lock.sh:

  • Test-first, no mutate-and-restore. The pin was written on an otherwise-unmodified tree and predicted in writing before running. Predicted: expected 'undefined' to be 'string' on the description case, the other two green. Observed exactly that — Test Files 1 failed | 19 passed, one case red. After the declaration edit: Test Files 20 passed (20) · Tests 317 passed (317).
  • pnpm --filter @objectstack/plugin-audit typecheck — clean (tsc --noEmit, no diagnostics).
  • pnpm lint (whole repo, eslint . --no-inline-config) — command-exit 0. No narrowing was needed.
  • pnpm check:i18ncheck-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).
  • Bundle-leaf equality, checked directly rather than through the gate (see the note below): the fields.type.help leaf in all four bundles is byte-equal to the description the declaration carries — 411 chars in en, es-ES, ja-JP and zh-CN, compared against the string reconstructed from sys-activity.object.ts itself.
  • pnpm check:nul-bytesOK (scanned 6514 text file(s) … no raw ASCII control bytes).
  • Gate families derived from the real change set with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, all green: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:query-options-erasure (ratchet holds … none new), check:engine-double-contract (OK — 397 pinned), check:where-matcher (293 matcher(s) … none new), check:type-check-coverage (OK — 65/78 workspace packages type-checked), check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-ci-filter-parity, check-plugin-teardown-shape, docs-audit/check-affected-docs, release-rehearsal-clone --self-test.
  • Declared narrowing:check:type-check-debt (--re-measure) was not run — it needs the whole packages/** closure built. The structural half is green above, this package's own typecheck is green, its tsconfig include is src/**/* (so the new test file was type-checked), and the only plugin-audit ledger entry is packages/plugins/plugin-audit/scripts, a directory this diff does not touch. CI runs the farm regardless.

Found while working, filed rather than adapted

#11658RecordActivityProps.types in packages/spec is z.array(FeedItemType), a closed author-facing filter over the vocabulary the renderer maps this column onto. That is the ruling's "closed map" one level removed, in a package this branch is fenced out of, so it is filed unassigned for the spec seat instead of being quietly widened. The renderer half stays where the ruling put it: a card in the objectui lane, completing what objectstack-ai/objectui#5840 un-pinned. Neither is addressed here.

check:i18n does not compare an untranslated leaf against its source string. Found on this branch, and worth recording because the gate reported green across it. The first extract seeded the three non-en bundles from an earlier wording of the description; the wording was then revised and the bundles re-extracted, but --fill=default is fill-onlyen is the source locale and was rewritten, while the three non-empty non-en leaves were left holding the superseded draft, including two clauses the declaration no longer makes. check:i18n stayed green throughout, because it verifies key presence and shape, not that an untranslated leaf still matches the source it was filled from. The drift is also sticky: a later regeneration heals nothing, since those leaves are still non-empty. Repaired here by dropping the three stale leaves and re-running the extract config's own documented command — no hand-edited translation text — and verified by comparing all four leaves to the source string. Widening the gate is deliberately not part of this PR.

No consumer inside this repo was found reading sys_activity.type as a closed enum in a report, export or compliance path — the sweep for the eleven/twelve literals outside the declaration returns only the declaration itself, its two vocabulary tests and the generated translation bundles. So nothing surfaced that inverts the facets the ruling relied on.

Generated by Claude Code


Generated by Claude Code

…ocabulary (#11507)
The select's declared options are the platform BUILT-IN set with documented
open-vocabulary semantics — not a closed enum the runtime never enforces.
Maintainer ruling 2026-08-24, direction 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/audit-service.mdx(via sys_activity (symbol), sys_activity (literal))
  • content/docs/plugins/packages.mdx(via sys_activity (symbol), sys_activity (literal))
  • content/docs/ui/setup-app.mdx(via sys_activity (symbol), sys_activity (literal))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via sys_activity (symbol), sys_activity (literal))
  • content/docs/releases/v16.mdx(via sys_activity (symbol), sys_activity (literal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 7 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 c251ef4213b48da59bfedacde1dfb9cd32db81cfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from b18b98afd44750a3bc7b3a5a07ca97ad01cd4002 — the merge of head a79cc3bf6b9f733979d60b88f9e9943e079abc66 into base c251ef4213b48da59bfedacde1dfb9cd32db81cf, 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 b18b98afd44750a3bc7b3a5a07ca97ad01cd4002 && git checkout b18b98afd44750a3bc7b3a5a07ca97ad01cd4002
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c251ef4213b48da59bfedacde1dfb9cd32db81cf a79cc3bf6b9f733979d60b88f9e9943e079abc66 && git checkout -B drift-repro c251ef4213b48da59bfedacde1dfb9cd32db81cf && git merge --no-ff a79cc3bf6b9f733979d60b88f9e9943e079abc66
node scripts/docs-audit/affected-docs.mjs --json c251ef4213b48da59bfedacde1dfb9cd32db81cf

⚠️ 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 c251ef4213b48da59bfedacde1dfb9cd32db81cf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ed draft (#11507)
`--fill=default` is fill-only: the first extract seeded es-ES/ja-JP/zh-CN from
an earlier wording of the `sys_activity.type` description, and the re-extract
after the wording was revised left those non-empty leaves alone — so the three
carried two clauses the declaration no longer makes. Dropped the stale leaves
and re-ran the extract config's own documented command; all four now match the
source string byte for byte. No hand-edited translation text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
@os-samClaude

Copy link
Copy Markdown
Collaborator

PM review — PASS, flipped ready and armed for the merge queue at a79cc3b

Reviewed by the domain:services execution seat (session session_01APWX2AwT3a4xDcjPCe8bk4). Clause-② no: this changes a field's documentation slot, not accept/reject behaviour, and touches no packages/spec path. Verified independently rather than read off the report:

  • Head identity: a79cc3bf6b… is the same commit every check below was measured on, re-confirmed immediately before the flip. mergeable_state: clean.
  • Fences: 9 files, zero forbidden paths — no content/docs/releases/, no docs/adr/**, no packages/spec/**, no ratchet/baseline files.
  • No assertion removed anywhere in the diff. The two existing vocabulary suites keep every check they had; only what a red means changed, which is the point of the card.
  • The spec-key probe (openVocabulary / restricted / allowCustomValues / extensible against ObjectSchema.safeParse, all four required to be refused) makes the mechanism choice measured rather than asserted, and turns red the day the spec grows a real key. That is the right shape for a decision that had to be taken in description because no better slot exists yet.

The defect this review caught, and how it was closed

At bbe0b17 the three non-en bundles carried a superseded draft of the description — 602 chars against a 411-char source, with two clauses the declaration no longer makes. en matched. Cause: --fill=default is fill-only, so the re-extract after the wording was revised rewrote only the source locale and left the three non-empty leaves holding the old text.

Repaired at a79cc3b by dropping the three stale leaves and re-running the extract config's own documented command — no hand-edited translation text. Verified by this seat with git show <sha>:<path>: all four fields.type.help leaves now hash identically to the source description (sha256 a65396a6…), and the comparator was run against a deliberately altered string first to prove it can report a mismatch. Diff versus the reviewed head is exactly three files, one line each.

⚠️ Recording the reason that check was done by hand: CI cannot see this class of drift.check:i18n reported all bundles in sync across it, and the full suite was green at bbe0b17 — 31 checks, 29 success / 2 skipped / 0 failures, Lint & Repo Gates included. Filed as #11671 (unassigned, no pm:queue — grading is triage's field). ⛔ Deliberately not fixed here.

Docs Drift Check — dispositioned, no action

The advisory listed three hand-written pages naming sys_activity. Read on the merge commit the run itself named: all three are incidental mentions of the object ("query audit history against sys_audit_log/sys_activity", an ADR-0029 ownership list, a Setup-app aside). None makes any claim about type's vocabulary and none enumerates its values, so there is no prose drift to repair. ⛔ The two release-owned pages were read-only and untouched.

Worth noting for future readers of this bot: it is symbol-anchored and precision-first (#9192) — it lists a page because the page names a symbol the diff touched, and it explicitly covers hand-written docs only. It cannot see whether prose went false, and it correctly omits generated pages. (references/data/feed.mdx and references/data/object.mdx also name sys_activity on that tree and are absent from the list precisely because both carry AUTO-GENERATED headers — checked before assuming the instrument was incomplete.)

Reported rather than adapted — the watch-item, answered

The dispatch required that an in-repo consumer treating this column as a closed enum be reported, not quietly adapted, since that would be evidence against the ruling's premise. The dev found one and filed #11658 (RecordActivityProps.types is z.array(FeedItemType) — a closed author-facing filter over the vocabulary just ruled open) in packages/spec, a package this branch is fenced out of, instead of widening. Evidence added there by this seat: the docs already publish both halves of that contradiction — object.mdx documents the author channel as an open string, while feed.mdx renders FeedItemType under the literal heading ### Allowed Values.

⚠️#11658 and #11670 are unlabeled and unassigned, so no seat will scan them. Flagged for triage; ⛔ not graded by this seat.


Generated by Claude Code

Merged via the queue into main with commit 88b9d74Aug 24, 2026
35 checks passed
@os-sam
os-sam deleted the claude/issue-11507-activity-type-open-vocabulary branch August 24, 2026 11:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Is sys_activity.type a closed platform vocabulary or an author-extensible one? Both readings are true of the code today

2 participants

@os-sam@claude