Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/7709-edit-mode-chip-sample-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@object-ui/i18n': minor
'@object-ui/app-shell': minor
---

**The maker's edit-mode starter offers sample data, not an automation v1 cannot
build (objectui#7709).** Bound to an existing app (`?package=`), the maker's
empty state offered four starters: add a field, add an object, add a dashboard,
and 「加一个自动化 —— 审批、状态流转或通知」. Approval, status flow and
notification are all refused by ADR-0112 v1 (cloud#1956 / PR #1970), and the
measured behaviour on the sibling chips was not a refusal but a silent degrade
into a view — so the product recommended an automation and would have handed
back a page.

Rewording it was not available: asking for a field, a view or a dashboard
duplicates one of the three chips beside it. The fourth chip is now
`addSampleData` —「给现有对象补一批贴近真实的示例数据,好拿去演示。」 — in all
ten packs and in the call-site `defaultValue` fallback, which is a second copy
of the same string. The three surviving chips all add STRUCTURE; what an app
that already has objects most often lacks is DATA, and `seed` is on v1's
authoring whitelist. A note beside the keys in every pack and at the call site
says this chip's automation wording comes back when ADR-0112 v2 re-adds flows
and actions, and the retired sentence for each pack is kept in the guard suite
so v2 has it verbatim.
14 changes: 13 additions & 1 deletion packages/app-shell/src/console/ai/AiChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2611,12 +2611,24 @@ function genericSuggestions(t: TranslationFn): string[] {
// ADR-0057 A1.b — edit-mode starters: when the build surface is bound to an
// existing app (`?package=`), nudge toward INCREMENTAL changes to that app
// rather than describing a new system from scratch.
//
// objectui#7709 — same rule as `metadataAssistantSuggestions()` above: these
// are the PRODUCT's own recommendations, so they may only ask for what
// ADR-0112 v1 BUILDS. The fourth chip used to be `addAutomation` ("an
// approval, a status flow, or a notification") and every capability it named
// is refused by v1 (cloud#1956 / PR #1970), so it now asks for sample data —
// `seed` IS on v1's whitelist, and having no data is what an existing app most
// often lacks. REVERT: when ADR-0112 v2 re-adds flows and actions, THIS chip's
// automation wording comes back as `addAutomation`. The `defaultValue`s below
// are byte-equal to the `en` pack.
function editAppSuggestions(t: TranslationFn): string[] {
return [
t('console.ai.suggestions.editApp.addField', { defaultValue: 'Add a field to one of the objects.' }),
t('console.ai.suggestions.editApp.addObject', { defaultValue: 'Add a new object and relate it to an existing one.' }),
t('console.ai.suggestions.editApp.addDashboard', { defaultValue: 'Add a dashboard for the key metrics.' }),
t('console.ai.suggestions.editApp.addAutomation', { defaultValue: 'Add an automation — an approval, a status flow, or a notification.' }),
t('console.ai.suggestions.editApp.addSampleData', {
defaultValue: 'Fill the existing objects with realistic sample records so I can demo the app.',
}),
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7709 — the CALL-SITE half of the maker's EDIT-mode chips.
*
* `packages/i18n/src/__tests__/makerEditChips-v1-scope-7709.test.ts` guards the
* ten packs. This guards the other copy of the same four strings: the
* `defaultValue` fallbacks in `editAppSuggestions()`, which are what a host
* with no I18nProvider (and any locale that ever loses the key) actually
* renders. cloud#1984 measured that shape on the sibling family — the packs
* were reworded and the fallbacks kept promising a status workflow, invisibly,
* on precisely the surface with the least i18n — so the two copies are pinned
* to each other here rather than trusted to stay in step.
*
* So: the edit-mode branch is reached at all, byte-equality with the `en` pack,
* and the same automation/approval-vocabulary ban.
*/
import { describe, it, expect } from 'vitest';
import { builtInLocales } from '@object-ui/i18n';
import { buildAgentSuggestions } from '../AiChatPage.js';

/** A `t` that misses every key — i.e. what a provider-less host resolves. */
const missEverything = (_key: string, options?: Record<string, unknown>): string =>
String(options?.defaultValue ?? _key);

/** The `en` pack's edit-mode chip block. */
const enChips = (
builtInLocales.en as {
console: { ai: { suggestions: { editApp: Record<string, string> } } };
}
).console.ai.suggestions.editApp;

const CHIP_KEYS = ['addField', 'addObject', 'addDashboard', 'addSampleData'] as const;

/** The English half of the i18n suite's banned vocabulary. @see makerEditChips-v1-scope-7709 */
const BANNED_EN = ['alert', 'remind', 'notif', 'automat', 'workflow', 'trigger', 'schedule', 'approv'];

const editChips = () => buildAgentSuggestions('build', 'Build', missEverything, true);

describe('editAppSuggestions — the build agent`s four edit-mode chips (objectui#7709)', () => {
it('renders the edit-mode starters when the surface is bound to an app', () => {
const chips = editChips();
expect(chips).toHaveLength(4);
// The `editing` flag is what selects this family; without it the from-
// scratch five are rendered and this suite would be guarding nothing.
expect(buildAgentSuggestions('build', 'Build', missEverything)).toHaveLength(5);
});

it('every `defaultValue` is byte-equal to the en pack', () => {
expect(editChips()).toEqual(CHIP_KEYS.map((k) => enChips[k]));
});

it('no fallback promises autonomous behaviour', () => {
for (const chip of editChips()) {
const hits = BANNED_EN.filter((term) => chip.toLowerCase().includes(term));
expect(hits, chip).toEqual([]);
}
});

it('the control: the retired wording WOULD have been flagged', () => {
// Non-vacuity, same reasoning as the pack-side suite: a banned list that
// has stopped matching anything passes the assertion above in silence.
const retired = 'Add an automation — an approval, a status flow, or a notification.';
const hits = BANNED_EN.filter((term) => retired.toLowerCase().includes(term));
expect(hits).toContain('automat');
expect(hits).toContain('approv');
expect(hits).toContain('notif');
});
});
157 changes: 157 additions & 0 deletions packages/i18n/src/__tests__/makerEditChips-v1-scope-7709.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7709 — the maker's EDIT-mode start chips, the sibling family of the
* five guarded by `makerStartChips-v1-scope-1984.test.ts`.
*
* `editAppSuggestions()` renders four starters when the build surface is bound
* to an EXISTING app (`?package=`). Three of them ask for structure — a field,
* an object, a dashboard — and all three are inside ADR-0112 v1's whitelist.
* The fourth was `addAutomation`: 「加一个自动化 —— 审批、状态流转或通知」,
* and approval / status flow / notification are ALL refused by v1 (cloud#1956 /
* PR #1970). Worse than a refusal, measured on the sibling chips: the model
* silently DEGRADES such a request into a view, so the chip promised an
* automation and delivered a page.
*
* The product ruling (epic cloud#1955, 2026-09-05) replaced it rather than
* dropping it: the three surviving chips all add STRUCTURE, and what an
* existing app most often lacks is DATA — `seed` is on v1's whitelist
* (`V1_METADATA_TYPES` in cloud `service-ai-studio/src/authoring-whitelist.ts`,
* commented "sample data"), so the fourth chip now asks for sample records.
*
* Same three-part shape as the 1984 suite, and the third part is what keeps the
* other two honest:
*
* 1. Every shipped pack carries all four chips, non-empty, and no fifth.
* 2. No chip in any pack uses vocabulary that promises autonomous behaviour or
* an approval, scanned with that pack's OWN banned list — an English-only
* scan would have declared the five non-Latin packs clean without reading a
* character of them (AGENTS.md i18n forensics rule).
* 3. A NON-VACUITY control: each pack's banned list is re-run against the
* `addAutomation` wording that pack actually shipped before this card, and
* must flag it.
*
* REVERT the wording (and relax this suite) when ADR-0112 v2 re-adds flows and
* actions — the note beside the keys in every pack says so, and RETIRED below
* is where that pack's automation sentence is kept.
*/
import { describe, it, expect } from 'vitest';
import { builtInLocales } from '../locales';

type LocaleCode = keyof typeof builtInLocales;
const LANGS = Object.keys(builtInLocales) as LocaleCode[];

/** The four chips `editAppSuggestions()` renders, in order. */
const CHIP_KEYS = ['addField', 'addObject', 'addDashboard', 'addSampleData'] as const;

/** The pack's edit-mode chip block, reached through the shape the call site reads. */
const chipsOf = (lang: LocaleCode) =>
(
builtInLocales[lang] as {
console?: { ai?: { suggestions?: { editApp?: Record<string, string> } } };
}
).console?.ai?.suggestions?.editApp;

/**
* Vocabulary that promises the product will ACT on its own — an automation, an
* approval, a status workflow, an alert, a reminder, a notification, a trigger,
* a schedule. Per locale, because the whole point is that the check reads each
* pack in its own script; substring matching with no ASCII word classes, since
* five of the ten packs are non-Latin and CJK has no word boundaries.
*
* The automation half is the `makerStartChips-v1-scope-1984` list verbatim (one
* v1 boundary, two families reading it). The approval half is added here: this
* chip's retired wording led with 审批 / approval, which that list did not name.
*
* Stems, not whole words: `automat` covers automate/automatic/automation and
* their Romance cognates, `notifi` covers notify/notification/notificación,
* `aprob` covers aprobación/aprobar.
*/
const BANNED: Record<LocaleCode, readonly string[]> = {
en: ['alert', 'remind', 'notif', 'automat', 'workflow', 'trigger', 'schedule', 'approv'],
zh: ['提醒', '预警', '警报', '自动', '流转', '通知', '触发', '定时', '审批'],
de: ['automat', 'erinner', 'warnung', 'alarm', 'benachrichtig', 'workflow', 'auslös', 'ablauf', 'freigabe', 'genehmig'],
fr: ['automat', 'alerte', 'rappel', 'notifi', 'workflow', 'flux', 'déclench', 'approbation', 'approuv'],
es: ['automat', 'alerta', 'recordar', 'recordatorio', 'notifi', 'flujo', 'disparador', 'aprob'],
pt: ['automa', 'alerta', 'lembr', 'notifi', 'fluxo', 'gatilho', 'aprova'],
ru: ['автомат', 'оповещ', 'уведомл', 'напомин', 'триггер', 'процесс', 'поток', 'согласован', 'утвержд'],
ja: ['自動', '通知', 'アラート', 'リマイン', 'ワークフロー', 'フロー', 'トリガー', '承認'],
ko: ['자동', '알림', '알람', '워크플로', '흐름', '트리거', '승인'],
ar: ['أتمتة', 'مؤتمت', 'تنبيه', 'إشعار', 'تذكير', 'سير عمل', 'موافقة', 'اعتماد'],
};

/** Every banned term this text hits, lower-cased for the case-insensitive scripts. */
function bannedHits(lang: LocaleCode, text: string): string[] {
const haystack = text.toLocaleLowerCase(lang === 'zh' ? 'zh' : undefined);
return BANNED[lang].filter((term) => haystack.includes(term.toLocaleLowerCase()));
}

/**
* The `addAutomation` sentence each pack shipped BEFORE this card. Kept as the
* control sample AND as the record of what ADR-0112 v2 restores — not as a
* fixture to revert to today.
*/
const RETIRED: Record<LocaleCode, string> = {
en: 'Add an automation — an approval, a status flow, or a notification.',
zh: '加一个自动化 —— 审批、状态流转或通知。',
de: 'Füge eine Automatisierung hinzu — eine Freigabe, einen Statusablauf oder eine Benachrichtigung.',
fr: 'Ajoute une automatisation — une approbation, un flux de statuts ou une notification.',
es: 'Añade una automatización — una aprobación, un flujo de estados o una notificación.',
pt: 'Adicione uma automação — uma aprovação, um fluxo de status ou uma notificação.',
ru: 'Добавь автоматизацию — согласование, процесс статусов или уведомление.',
ja: '自動化を追加してください — 承認、ステータスフロー、または通知。',
ko: '자동화를 추가해 주세요 — 승인, 상태 흐름 또는 알림.',
ar: 'أضف أتمتة — موافقة أو سير حالات أو إشعارًا.',
};

describe('maker edit-mode chips — every shipped pack carries all four (objectui#7709)', () => {
it('covers all ten built-in packs', () => {
expect(LANGS).toHaveLength(10);
});

it.each(LANGS)('%s defines a non-empty string for every chip', (lang) => {
const block = chipsOf(lang);
expect(block, `${lang} has no console.ai.suggestions.editApp block`).toBeTruthy();
for (const key of CHIP_KEYS) {
expect(typeof block![key], `${lang}.${key}`).toBe('string');
expect(block![key].trim().length, `${lang}.${key} is empty`).toBeGreaterThan(0);
}
// No chip beyond the four the call site renders, and — the half that
// matters here — no `addAutomation` left behind in any pack.
expect(Object.keys(block!).sort()).toEqual([...CHIP_KEYS].sort());
});

it.each(LANGS)('%s: the four chips are four DIFFERENT asks', (lang) => {
// The replacement had to avoid duplicating the three chips beside it —
// that is precisely why "reword it like the other five" was not available
// and this became its own product call.
const block = chipsOf(lang)!;
const values = CHIP_KEYS.map((k) => block[k].trim());
expect(new Set(values).size, `${lang}: ${values.join(' | ')}`).toBe(CHIP_KEYS.length);
});
});

describe('maker edit-mode chips — none promises autonomous behaviour (objectui#7709)', () => {
it.each(LANGS)('%s: no chip uses that pack`s automation/approval vocabulary', (lang) => {
const block = chipsOf(lang)!;
for (const key of CHIP_KEYS) {
expect(bannedHits(lang, block[key]), `${lang}.${key}: "${block[key]}"`).toEqual([]);
}
});

it.each(LANGS)('%s: the banned list still flags the wording this pack retired', (lang) => {
// Non-vacuity. A green run above means nothing unless this is green too.
expect(bannedHits(lang, RETIRED[lang]).length, `${lang} control: "${RETIRED[lang]}"`).toBeGreaterThan(0);
});

it('the replaced chip asks for DATA, in the two languages the card quoted', () => {
expect(chipsOf('en')!.addSampleData).toContain('sample records');
expect(chipsOf('zh')!.addSampleData).toContain('示例数据');
});
});
12 changes: 11 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1643,11 +1643,21 @@ const ar = {
availableObjects: "اذكر كائنات البيانات المتاحة.",
recentActivity: "لخّص نشاطي الأخير.",
},
// objectui#7709 — the edit-mode starters, shown when the maker is bound
// to an EXISTING app (`?package=`). Same rule as the five above: they
// may only ask for what ADR-0112 v1 BUILDS. The fourth chip used to be
// `addAutomation` ("an approval, a status flow, or a notification") and
// every capability it named is refused by v1 (cloud#1956 / PR #1970), so
// it now asks for sample data — `seed` IS on v1's whitelist, and having
// no data is what an existing app most often lacks. REVERT: when
// ADR-0112 v2 re-adds flows and actions, THIS chip's automation wording
// comes back as `addAutomation`; the retired string is pinned for every
// pack in `packages/i18n/src/__tests__/makerEditChips-v1-scope-7709.test.ts`.
editApp: {
addField: "أضف حقلًا إلى أحد الكائنات.",
addObject: "أضف كائنًا جديدًا واربطه بكائن موجود.",
addDashboard: "أضف لوحة معلومات للمؤشرات الرئيسية.",
addAutomation: "أضف أتمتة — موافقة أو سير حالات أو إشعارًا.",
addSampleData: "املأ الكائنات الحالية ببيانات نموذجية واقعية تصلح للعرض التوضيحي.",
},
},
changesTitle: "تأكيد التغييرات",
Expand Down
12 changes: 11 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1636,11 +1636,21 @@ const de = {
availableObjects: "Liste die verfügbaren Datenobjekte auf.",
recentActivity: "Fasse meine letzten Aktivitäten zusammen.",
},
// objectui#7709 — the edit-mode starters, shown when the maker is bound
// to an EXISTING app (`?package=`). Same rule as the five above: they
// may only ask for what ADR-0112 v1 BUILDS. The fourth chip used to be
// `addAutomation` ("an approval, a status flow, or a notification") and
// every capability it named is refused by v1 (cloud#1956 / PR #1970), so
// it now asks for sample data — `seed` IS on v1's whitelist, and having
// no data is what an existing app most often lacks. REVERT: when
// ADR-0112 v2 re-adds flows and actions, THIS chip's automation wording
// comes back as `addAutomation`; the retired string is pinned for every
// pack in `packages/i18n/src/__tests__/makerEditChips-v1-scope-7709.test.ts`.
editApp: {
addField: "Füge einem der Objekte ein Feld hinzu.",
addObject: "Füge ein neues Objekt hinzu und verknüpfe es mit einem bestehenden.",
addDashboard: "Füge ein Dashboard für die wichtigsten Kennzahlen hinzu.",
addAutomation: "Füge eine Automatisierung hinzu — eine Freigabe, einen Statusablauf oder eine Benachrichtigung.",
addSampleData: "Fülle die vorhandenen Objekte mit realistischen Beispieldatensätzen, damit ich die App vorführen kann.",
},
},
changesTitle: "Änderungen bestätigen",
Expand Down
12 changes: 11 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1996,11 +1996,21 @@ const en = {
availableObjects: 'List the available data objects.',
recentActivity: 'Summarize my recent activity.',
},
// objectui#7709 — the edit-mode starters, shown when the maker is bound
// to an EXISTING app (`?package=`). Same rule as the five above: they
// may only ask for what ADR-0112 v1 BUILDS. The fourth chip used to be
// `addAutomation` ("an approval, a status flow, or a notification") and
// every capability it named is refused by v1 (cloud#1956 / PR #1970), so
// it now asks for sample data — `seed` IS on v1's whitelist, and having
// no data is what an existing app most often lacks. REVERT: when
// ADR-0112 v2 re-adds flows and actions, THIS chip's automation wording
// comes back as `addAutomation`; the retired string is pinned for every
// pack in `packages/i18n/src/__tests__/makerEditChips-v1-scope-7709.test.ts`.
editApp: {
addField: 'Add a field to one of the objects.',
addObject: 'Add a new object and relate it to an existing one.',
addDashboard: 'Add a dashboard for the key metrics.',
addAutomation: 'Add an automation — an approval, a status flow, or a notification.',
addSampleData: 'Fill the existing objects with realistic sample records so I can demo the app.',
},
},
// objectui#3546 slice four — the AI console surfaces: the /ai chat page's app switcher,
Expand Down
Loading
Loading