diff --git a/.changeset/sys-activity-type-open-vocabulary.md b/.changeset/sys-activity-type-open-vocabulary.md new file mode 100644 index 0000000000..6983ad2434 --- /dev/null +++ b/.changeset/sys-activity-type-open-vocabulary.md @@ -0,0 +1,43 @@ +--- +'@objectstack/plugin-audit': patch +--- + +Say out loud that `sys_activity.type` is an open, author-extensible vocabulary +— the declared options are the platform's **built-in** set, not a closed enum + +An author reading the declaration learned "writing another value will be +rejected". That was false in three independent ways, and the declaration was +the only place that did not say so. + +1. Every field on `sys_activity` is `readonly: true`, and `validateRecord` + skips readonly fields on both write branches, so the `invalid_option` check + a `select` normally implies **never runs** on this column. +2. ADR-0052 §5b.2 `activityMilestones[].type` is `z.string().optional()` in + the spec and is forwarded verbatim by the audit writer + (`if (milestone.type) activityType = milestone.type`) — a shipped, + documented, author-facing channel straight into the column. +3. An app's own server-side action writes the column directly + (`ctx.api.object('sys_activity').insert({ type: … })`); no grep of this + repository can see those sites. + +Maintainer ruling, 2026-08-24 (#11507, direction 4 of four): the column **is** +an open vocabulary, ADR-0052 §5b.2 **stays** a sanctioned write path, and +every closed map over this vocabulary is now the bug. The status quo was the +one option more dangerous than either end state — most of all to an AI writing +metadata, which reads the declaration and believes it. + +So the declaration now carries the semantics, in the field's own +`description` — the slot the spec declares for exactly this and, unlike a +source comment, one the contract carries wherever the metadata goes (the +metadata API, the i18n bundles, whatever an author or an AI reads about this +field). No new schema concept was invented: `FieldSchema` has no +open/closed-vocabulary key, and the pin measures that rather than asserting +it, so the day `packages/spec` grows one this declaration is told to move. + +Nothing about enforcement changed — that was direction 3 and it was **not** +ruled. `validateRecord` is untouched, the built-in set is unchanged (twelve +values), and both existing vocabulary tests keep every assertion they had. +What changed in them is what a red MEANS: the two cases that used to be filed +as "a defect, characterized — delete these when enforcement lands" now measure +a ruled contract, and say that rejecting an author-contributed value is a +contract change to re-open #11507 over, not a fix to adapt them to. diff --git a/packages/plugins/plugin-audit/src/activity-type-vocabulary-enforcement.test.ts b/packages/plugins/plugin-audit/src/activity-type-vocabulary-enforcement.test.ts index 466ca948e3..2a18c6cc3c 100644 --- a/packages/plugins/plugin-audit/src/activity-type-vocabulary-enforcement.test.ts +++ b/packages/plugins/plugin-audit/src/activity-type-vocabulary-enforcement.test.ts @@ -39,6 +39,17 @@ * keeps writing, the row keeps landing, every assertion below stays green) and * red THERE. Breaking a writer is red here and green there. Neither file alone * covers this object. + * + * ## 2026-08-24 — what the ruling on #11507 changed about this file + * + * Nothing about the measurements; everything about what they MEAN. #8203 wrote + * §3 as "a defect, characterized", with the instruction to delete those cases + * once enforcement landed. The maintainer ruled (direction 4) that this column + * is an OPEN, author-extensible vocabulary: the declared options are the + * platform's BUILT-IN set, ADR-0052 §5b.2 stays a sanctioned write path, and an + * author-contributed value landing verbatim is the contract. So §3 is no longer + * a characterized defect — it is the only end-to-end measurement of the ruled + * behavior, and it stays. See the §3 header for what a red there now means. */ import { describe, it, expect } from 'vitest'; @@ -233,8 +244,11 @@ describe('[#8203] sys_activity.type — the writers emit declared values', () => expect( DECLARED_TYPES, `audit-writers.ts wrote sys_activity.type '${t}', which the object does not ` - + 'declare. Nothing rejects it — the field is readonly, so `validateRecord` ' - + 'skips it — so the row lands and the contract denies it (#8203).', + + 'declare as a built-in. Nothing rejects it — the field is readonly, so ' + + '`validateRecord` skips it — so the row lands unannounced. The vocabulary is ' + + 'open to AUTHORS (#11507); the platform writing outside its own built-in set ' + + 'is still a finding, because that set is what the platform promises to write, ' + + 'label and offer as a filter. Declare the value, or stop writing it (#8203).', ).toContain(t); } }); @@ -297,19 +311,34 @@ describe('[#8203] CONTROL — a writable select rejects the undeclared value', ( }); // --------------------------------------------------------------------------- -// 3. The finding — the identical write is ACCEPTED when the field is readonly +// 3. The ruled contract — an author-contributed value is stored verbatim // --------------------------------------------------------------------------- -describe('[#8203] the declared vocabulary is unenforceable while the field is readonly', () => { +describe('[#8203/#11507] an author-contributed type is accepted — the open-vocabulary contract', () => { /** - * ⚠️ These two cases assert a DEFECT, characterized. They are the card's - * observation made mechanical, and they are written to go red the day it is - * fixed — which is the correct signal, not a false alarm. + * ⚠️ These two cases used to be labelled "a DEFECT, characterized", with the + * instruction: go red when enforcement lands, then delete them. That + * instruction is RETIRED, and deleting them now would delete the only + * end-to-end measurement of a ruled contract. + * + * Maintainer ruling, 2026-08-24, #11507 (direction 4): `sys_activity.type` is + * an OPEN, author-extensible vocabulary. The declared options are the + * platform's built-in set; ADR-0052 §5b.2 `activityMilestones[].type` stays a + * sanctioned write path; an author-contributed value landing verbatim is what + * the platform means, not a hole in it. Directions 2 and 3 were considered and + * NOT ruled. + * + * So a red here no longer reads "the fix landed". It reads: something has + * started REJECTING an author-contributed value — which is direction 3, a + * shipped authoring surface turned into a rejection path. Do not adapt these + * cases to it and do not weaken them; re-open #11507, because that is a + * maintainer call and not a test-fixing exercise. * - * If one fails with "expected 'not_a_declared_type' … received a rejection", - * enforcement has landed (the engine-wide direction #8203 names, in - * `record-validator.ts`). That is the fix: delete these two cases, keep §1 - * and the census file, and close #8203. + * The mechanism is unchanged and still worth knowing: every field on this + * object is `readonly`, and `validateRecord` skips readonly fields on both + * branches, so the option check never runs. §2 is the control proving the + * validator runs at all — which is what makes the acceptance below a + * measurement rather than a test that forgot to assert. */ it('a direct write of an undeclared type into sys_activity is accepted verbatim', async () => { const { engine, storeFor } = await boot(); @@ -321,18 +350,22 @@ describe('[#8203] the declared vocabulary is unenforceable while the field is re expect(DECLARED_TYPES).not.toContain(UNDECLARED); expect( activityTypes(storeFor), - 'sys_activity.type no longer accepts an undeclared option. If this is because ' - + 'readonly-field option enforcement landed, that is the fix #8203 describes — ' - + 'retire this case and its neighbour and close the card.', + 'sys_activity.type no longer accepts an undeclared option. Per the 2026-08-24 ' + + 'ruling on #11507 this column is an OPEN vocabulary: a value outside the ' + + 'built-in set is legitimate and is stored verbatim, so a rejection here is a ' + + 'CONTRACT CHANGE (direction 3, considered and not ruled), not a fix. Re-open ' + + '#11507 instead of adapting this case.', ).toEqual([UNDECLARED]); }); /** - * The same hole reached through a REAL, shipped authoring surface rather than + * The same path reached through a REAL, shipped authoring surface rather than * a hand-made insert: `activityMilestones[].type` is `z.string().optional()` - * in the spec, so any metadata author can name any string, and it lands in a - * column whose enum denies it. This is the authoring-time version of the - * defect and the one an AI-written metadata app would hit first. + * in the spec, so any metadata author can name any string and it lands. This + * is the authoring-time face of the open vocabulary, and the one an AI-written + * metadata app meets first — which is exactly why the declaration now says so + * in its own `description` (#11507), instead of showing that author a list + * that reads closed. */ it('a milestone declaring an undeclared type writes it — the authoring-surface hole', async () => { const { engine, storeFor } = await boot(); @@ -342,10 +375,11 @@ describe('[#8203] the declared vocabulary is unenforceable while the field is re expect(DECLARED_TYPES).not.toContain('escalated_to_legal'); expect( activityTypes(storeFor), - 'a milestone-declared `type` outside the sys_activity.type enum no longer reaches ' - + 'the row. If option enforcement (or a spec-level constraint on ' - + '`activityMilestones[].type`) landed, that is the fix #8203 describes — retire ' - + 'this case and close the card.', + 'a milestone-declared `type` outside the built-in sys_activity.type set no longer ' + + 'reaches the row. ADR-0052 §5b.2 is a SANCTIONED author write path and the ' + + '2026-08-24 ruling on #11507 kept it one, so option enforcement here — or a ' + + 'spec-level constraint on `activityMilestones[].type` — breaks shipped author ' + + 'metadata by design. Re-open #11507 before changing this.', ).toEqual(['created', 'escalated_to_legal']); }); }); diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity-type-open-vocabulary.test.ts b/packages/plugins/plugin-audit/src/objects/sys-activity-type-open-vocabulary.test.ts new file mode 100644 index 0000000000..5f1abcb5c4 --- /dev/null +++ b/packages/plugins/plugin-audit/src/objects/sys-activity-type-open-vocabulary.test.ts @@ -0,0 +1,156 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { ObjectSchema } from '@objectstack/spec/data'; +import { SysActivity } from './index.js'; + +/** + * #11507 — the declaration of `sys_activity.type` must say what the column + * actually is: an OPEN, author-extensible vocabulary whose declared options are + * the platform's BUILT-IN set. + * + * ## The ruling this file executes + * + * Maintainer, 2026-08-24, on #11507 (direction 4 of the four the card framed), + * verbatim: 「四维分析一致的,接手你的建议。」 Recorded on the card as: + * + * > the column is an open, author-extensible vocabulary. […] make the + * > declaration honest (the select's declared options become the built-in set + * > with documented open-vocabulary semantics — not a closed enum the runtime + * > never enforces); ADR-0052 §5b.2 stays a sanctioned write path […] + * > Downstream: every closed map over this vocabulary is now the bug. + * + * Directions 2 (rule the producer non-conformant) and 3 (enforce the vocabulary) + * were NOT ruled. Nothing here should be read as a step toward either. + * + * ## Why the declaration was dishonest, in one paragraph + * + * Three things were true at once. The field is a `select` over a fixed list — + * which normally means "anything else is `invalid_option`". Every field on this + * object is `readonly: true` and `validateRecord` skips readonly fields on both + * write branches, so that check never runs. And ADR-0052 §5b.2's + * `activityMilestones[].type` (`z.string().optional()` in `object.zod.ts`) is a + * shipped, documented, author-facing channel that forwards ANY string into the + * column — `audit-writers.ts`: `if (milestone.type) activityType = milestone.type`. + * An author (a human, and far more often an AI writing metadata) who reads the + * declaration builds the model "writing another value will be rejected", and + * that model is false. The status quo was more dangerous than either end state, + * which is what the four-facet analysis said and what the ruling adopted. + * + * ## The mechanism, and why this one + * + * `FieldSchema` has no key that means "open vocabulary" — no `openVocabulary`, + * no `restricted`, no `allowCustomValues` (measured below, third case, so the + * next author does not have to guess). Adding one is a `packages/spec` change + * and therefore a different seat's card, not something to invent here. The slot + * the spec DOES declare for exactly this is the field's own `description` + * ("Tooltip/Help text", `field.zod.ts` — the documentation slot, distinct from + * `placeholder` and `inlineHelpText`), and it is carried BY THE CONTRACT: the + * exported `SysActivity` is the output of `ObjectSchema.create()`, i.e. of a + * real parse, so what this file reads is metadata that ships — to the metadata + * API, to the i18n bundles, to whatever an author or an AI reads about this + * field — and not a source comment that stops at the file boundary. + * + * So: the source docblock carries the reasoning, and the `description` carries + * the contract. This file pins the second, because only the second travels. + */ + +/** The `type` field as it is actually declared (post-parse). */ +function typeField(): { type?: string; description?: unknown; options?: unknown } { + return ((SysActivity as { fields?: Record> }) + .fields?.type ?? {}) as { type?: string; description?: unknown; options?: unknown }; +} + +/** Option values declared by the `type` select field. */ +function typeValues(): string[] { + const options = (typeField().options ?? []) as Array; + return options.map((o) => (typeof o === 'string' ? o : String(o.value))); +} + +describe('[#11507] sys_activity.type is an OPEN vocabulary and the declaration says so', () => { + /** + * The half of the ruling that is easy to lose: "open" does NOT mean + * "undeclared". The declared options are the BUILT-IN set — the values the + * platform itself writes and the values a picker/filter offers — and they + * stay declared. A future author who reads "open vocabulary" and deletes the + * option list would take the built-in set, the labels, the i18n leaves and + * the census pin with it. + */ + it('keeps a declared built-in set — an open vocabulary is not an absent one', () => { + const field = typeField(); + expect( + field.type, + 'sys_activity.type stopped being a `select`. The #11507 ruling made the vocabulary ' + + 'OPEN, not undeclared: the declared options are the platform built-in set and ' + + 'they stay. Widening the column to a bare `text` deletes the built-in set, its ' + + 'labels and its i18n leaves, and leaves authors nothing to extend FROM.', + ).toBe('select'); + expect( + typeValues().length, + 'sys_activity.type declares no options. See above: open ≠ undeclared (#11507).', + ).toBeGreaterThan(0); + }); + + /** + * The load-bearing assertion, and the deliverable of #11507. The three + * markers are the three things an author must be able to learn FROM THE + * DECLARATION ITSELF: + * - the declared list is the BUILT-IN set (not the whole legal set); + * - the vocabulary is OPEN (an author may contribute a value); + * - the sanctioned way to do that is ADR-0052 §5b.2, which stays a write + * path per the ruling — not a rejection path. + * + * Asserted as markers rather than as an exact string: the wording is meant to + * be improvable, the three facts are not. + */ + it('declares open-vocabulary semantics in the CONTRACT, not only in a source comment', () => { + const description = typeField().description; + const hint = + 'sys_activity.type carries no open-vocabulary documentation in its declaration. ' + + 'Per the 2026-08-24 maintainer ruling on #11507 this column is an OPEN, ' + + 'author-extensible vocabulary: the declared options are the BUILT-IN set, an ' + + 'author-contributed value (ADR-0052 §5b.2 `activityMilestones[].type`, or an ' + + "app action's own `insert`) is legitimate, and it is stored verbatim — nothing " + + 'rejects it, because every field here is `readonly` and `validateRecord` skips ' + + 'readonly fields. A bare option list without that sentence tells an author — ' + + 'most often an AI writing metadata — that another value would be REJECTED, ' + + 'which is false. Put it back in `description` (the contract carries it; a ' + + 'source comment does not).'; + + expect(typeof description, hint).toBe('string'); + const text = String(description); + expect(text.length, hint).toBeGreaterThan(0); + for (const marker of [/built-in/i, /open vocabulary/i, /ADR-0052/]) { + expect(marker.test(text), `${hint}\nMissing from the description: ${marker}`).toBe(true); + } + }); + + /** + * WHY the mechanism above is prose in `description` rather than a declared + * flag: there is no flag. Measured, not assumed — and written so it goes RED + * the day the spec grows one, which is the day this declaration should move + * the semantics into it (and the day the objectui-side consumer can read the + * openness mechanically instead of being told). + * + * Note what this does NOT claim: that such a key should not exist. Declaring + * one is a `packages/spec` decision and belongs to the spec seat. + */ + it('has no declared spec key for open/closed vocabulary — `description` is the available slot', () => { + const probes = ['openVocabulary', 'restricted', 'allowCustomValues', 'extensible']; + for (const key of probes) { + const candidate = JSON.parse(JSON.stringify(SysActivity)) as { + fields: Record>; + }; + candidate.fields.type[key] = true; + const parsed = ObjectSchema.safeParse(candidate); + expect( + parsed.success, + `FieldSchema now accepts \`${key}\` on a field. If \`packages/spec\` grew a real ` + + 'open/closed-vocabulary declaration, this file is the pin that says so: move ' + + "sys_activity.type's open-vocabulary semantics onto that key (keeping the " + + 'description as help text), and tell the objectui consumer card — a machine-' + + 'readable flag is what lets a renderer stop guessing (#11507).', + ).toBe(false); + } + }); +}); diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts b/packages/plugins/plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts index 3570674de6..fd215cdb47 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts @@ -23,6 +23,30 @@ import { SysActivity } from './index.js'; * shows both halves, against a control that proves the measurement can fail. * This file is the DECLARATIVE half: the writer census, written as literals. * + * ## 2026-08-24 — the #11507 ruling, and what this census now inventories + * + * The paragraph above described the state of the code; the maintainer then + * ruled what it MEANS (direction 4): `sys_activity.type` is an OPEN, + * author-extensible vocabulary, the declared options are the platform's + * BUILT-IN set, and ADR-0052 §5b.2 stays a sanctioned author write path. The + * declaration now says so in its own `description` + * (`sys-activity.object.ts`), pinned by `./sys-activity-type-open-vocabulary.test.ts`. + * + * Two consequences for THIS file, and no others — every assertion below is + * unchanged: + * + * - The census inventories the BUILT-IN set. A value an app contributes + * through the milestone door or its own action does not belong here just + * because it exists; it belongs to the app that writes it. Declaring one + * (as #11424 did for `scheduled`) is a deliberate choice to take it into the + * platform's set — the platform then owns its label, its i18n leaves and its + * filter — never an obligation created by the mere fact that someone wrote + * it. + * - "An undeclared value is written silently" is no longer a defect to be + * fixed by enforcement. It is the ruled contract. What this file still + * guards is the OTHER direction: that the built-in set stays honest about + * the platform's own writers. + * * The two halves are not redundant, and the asymmetry is the reason both * exist. Narrowing the enum away from a live writer changes NO behavior — the * writer keeps writing, the row keeps landing, every behavioral assertion stays diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index acd3c75fbe..f5986c56af 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -54,6 +54,50 @@ export const SysActivity = ObjectSchema.create({ group: 'Event', }), + /** + * The activity kind — an OPEN, author-extensible vocabulary whose declared + * options are the platform's BUILT-IN set. Maintainer ruling 2026-08-24 on + * #11507 (direction 4 of the four that card framed), which the `description` + * below carries into the contract; this comment carries the reasoning. + * + * ## Why the declaration used to lie + * + * A `select` over a fixed list normally means "anything else is + * `invalid_option`". Here it never could: + * + * 1. Every field on this object is `readonly: true`, and `validateRecord` + * skips readonly fields on BOTH write branches + * (`objectql/src/validation/record-validator.ts`), so the option check + * never runs. An undeclared value is stored silently. + * 2. ADR-0052 §5b.2 `activityMilestones[].type` is `z.string().optional()` + * in `object.zod.ts` and is forwarded verbatim by `audit-writers.ts` + * (`if (milestone.type) activityType = milestone.type`) — a shipped, + * documented, author-facing channel into this column. + * 3. An app's own server-side action reaches the column directly + * (`ctx.api.object('sys_activity').insert({ type: … })`). No grep of + * THIS repository can see those; both measured sites live in + * objectstack-ai/hotcrm (#11424, read at 5eee1bd). + * + * So an author — most often an AI writing metadata — who read the option + * list learned "another value will be rejected", which was false in three + * independent ways. The ruling closes that by moving the declaration, not + * the runtime: the options are the built-in set, author-contributed values + * are legitimate, and ADR-0052 §5b.2 STAYS a write path (turning it into a + * rejection path was direction 3, which was NOT ruled). + * + * ## What that binds + * + * - Do not "fix" this by enforcing the enum on system-owned writes, and do + * not narrow `activityMilestones[].type`. Either is direction 3; re-open + * #11507 first. + * - Keep the built-in set declared and censused — open is not undeclared. + * The writer census lives in `sys-activity-type-vocabulary.test.ts`, and + * it inventories BUILT-IN values only; an author's value belongs to the + * app that writes it, not to this list. + * - Downstream, every CLOSED map over this vocabulary is now the bug: a + * consumer must render an unknown value, not drop the row. (objectui's + * feed-kind map is the known one; its pin is a card in that lane.) + */ type: Field.select( [ 'created', @@ -71,6 +115,13 @@ export const SysActivity = ObjectSchema.create({ ], { label: 'Type', + description: + 'Activity kind. The declared options are the platform BUILT-IN set of an open ' + + 'vocabulary, not a closed enum: metadata authors may contribute their own values ' + + '(sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an ' + + 'undeclared value is stored verbatim rather than rejected. Consumers must render ' + + 'an unknown value instead of assuming this list is exhaustive (maintainer ruling ' + + '2026-08-24, #11507).', required: true, readonly: true, searchable: true, diff --git a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts index 66b58183d2..8864dff8f8 100644 --- a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts @@ -121,6 +121,7 @@ export const enObjects: NonNullable = { }, type: { label: "Type", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", options: { created: "created", updated: "updated", diff --git a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts index 89135d97d3..ebb9cffe40 100644 --- a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts @@ -121,6 +121,7 @@ export const esESObjects: NonNullable = { }, type: { label: "Tipo", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", options: { created: "Creado", updated: "Actualizado", diff --git a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts index dbf617dacf..3d5e41290c 100644 --- a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts @@ -121,6 +121,7 @@ export const jaJPObjects: NonNullable = { }, type: { label: "タイプ", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", options: { created: "作成", updated: "更新", diff --git a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts index 109cb1212f..f33475c9ba 100644 --- a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts @@ -121,6 +121,7 @@ export const zhCNObjects: NonNullable = { }, type: { label: "类型", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", options: { created: "已创建", updated: "已更新",