Uh oh!
There was an error while loading. Please reload this page.
fix(audit): declare sys_activity.type as an open, author-extensible vocabulary - #11659
Conversation
…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
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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-sam
commented
Aug 24, 2026
PM review — PASS, flipped ready and armed for the merge queue at |
Fixes#11507
Executes the maintainer ruling of 2026-08-24 (direction 4, verbatim: 「四维分析一致的,接手你的建议。」):
sys_activity.typeis 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:
typeis aselectover a fixed list — which normally means "anything else isinvalid_option".sys_activityisreadonly: true, andvalidateRecordskips readonly fields on both write branches, so that check never runs on this column.activityMilestones[].typeisz.string().optional()in the spec and is forwarded verbatim byaudit-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.FieldSchemadeclares no open/closed-vocabulary key — noopenVocabulary, norestricted, noallowCustomValues— and inventing one is apackages/specchange, i.e. a different seat's card, which this branch is fenced out of.descriptionis the slot the spec declares for exactly this ("Tooltip/Help text", distinct fromplaceholderandinlineHelpText), and unlike a source comment the contract carries it: the exportedSysActivityis the output ofObjectSchema.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.safeParseand requires all four to be refused. The daypackages/specgrows 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
descriptioncarries the short honest version.What changed
sys-activity.object.ts— thetypefield gains adescriptionstating that the declared options are the built-in set of an open vocabulary, that the sanctioned author channel isactivityMilestones[].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.ts— every 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] Issys_activity.typea 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 forscheduled) is a deliberate choice to adopt it, never an obligation.plugin-audittranslation bundles (the newdescriptionextracts as afields.type.helpleaf) with the command the extract config documents.The built-in set is untouched — twelve values,
scheduledincluded, exactly as #11424 left it.Verification
Run at
a79cc3b(the head of this branch), heavy steps serialized throughscripts/pm/os-verify-lock.sh: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:i18n—check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).fields.type.helpleaf in all four bundles is byte-equal to thedescriptionthe declaration carries — 411 chars inen,es-ES,ja-JPandzh-CN, compared against the string reconstructed fromsys-activity.object.tsitself.pnpm check:nul-bytes—OK (scanned 6514 text file(s) … no raw ASCII control bytes).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.check:type-check-debt(--re-measure) was not run — it needs the wholepackages/**closure built. The structural half is green above, this package's owntypecheckis green, its tsconfigincludeissrc/**/*(so the new test file was type-checked), and the only plugin-audit ledger entry ispackages/plugins/plugin-audit/scripts, a directory this diff does not touch. CI runs the farm regardless.Found while working, filed rather than adapted
#11658 —
RecordActivityProps.typesinpackages/specisz.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:i18ndoes 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-enbundles from an earlier wording of thedescription; the wording was then revised and the bundles re-extracted, but--fill=defaultis fill-only —enis the source locale and was rewritten, while the three non-empty non-enleaves were left holding the superseded draft, including two clauses the declaration no longer makes.check:i18nstayed 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.typeas 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