Skip to content

Commit c5ccbd5

Browse files
os-zhuangclaude
andauthored
refactor(data-objectstack,plugin-chatbot,plugin-list)!: burn ledger batch 6 — 12 symbols stop wearing spec names (#3220)
objectui#3160, objectstack#4115 ledger batch 6. All three packages leave the ledger (29 -> 17 collisions, 14 -> 11 packages). Verdicts, per SYMBOL rather than per cluster — the batch's own triage note predicted `SecurityPolicy` and `DroppedFieldsEvent` were both spec hand copies, and only one of them was: @object-ui/data-objectstack - DroppedFieldsEvent -> re-export. The copy's comment said it mirrored the spec "so we don't pin a client type version" and widened `reason` to bare `string` "for forward-compatibility". The spec IS the client type version, and the widening deleted the only signal that would tell AdapterProvider's toast (it branches on `readonly_when`) that a third reason had appeared. - CacheStats -> MetadataCacheStats (spec's is ICacheService's) - MetadataSaveOptions -> MetadataClientSaveOptions (spec's writes a FILE) - SecurityPolicy -> SecurityManagerPolicy (spec's is supply-chain) - ValidationError -> DataApiValidationError, per the cross-package convention registered on objectstack#4115. The RUNTIME `name` stays 'ValidationError': normaliseClientError and @object-ui/react's error-message both sniff err.name, so that string is a wire contract. @object-ui/plugin-chatbot - PendingActionRow / PendingActionStatus -> re-export. Three drifts, each of which had DISABLED a compile-time check: `status: PendingActionStatus | string` (a union with string absorbs the literals), `[k: string]: unknown` (objectstack#4075 — makes every structural comparison answer "identical"), and created_at/updated_at, which the contract lacks and nothing reads. - Tool / MessageContent -> SKIP_PATH_SEGMENTS. They live in src/elements/, vendored Vercel AI Elements + two Shadcn primitives, the same class as the already-skipped components/src/ui/. The names ARE the upstream component API, so a rename is undone by the next re-sync. The hole a path skip opens is closed by a provenance test: every file there must carry its vendor banner, and the barrel must stay a pure re-export. @object-ui/plugin-list - ViewTab -> derived from the spec schema's INPUT side (pinned/isDefault/ visible carry .default()s and this reads authored metadata). Drops a renderer-side tolerance: `visible` accepted `string | boolean` and the tab bar compared it against the literal 'false', which no producer emits. - ListView / UserFilters -> ALLOW. The AuthProvider test, not "components are exempt": each is the RENDERER of the spec type whose name it shares and takes that type as a prop (ListViewProps.schema, UserFiltersProps.config), so neither declares a rival shape. @object-ui/types already aliases the spec's type as SpecListView from the other side. Verification: `--ledger` regenerated and set-diffed — removed is exactly these 12, added empty, all 11 other packages byte-identical (batch 7's symbols untouched). Guard green; turbo build 44/44 and type-check 78/78 --force after merging origin/main; data-objectstack 153, plugin-chatbot 251, plugin-list 284 and core 153 assertions green. Five mutation directions all red: re-forking ViewTab (guard by name+file, and the typetests pin), undoing the ValidationError rename, leaving a burned name in DEBT, deleting the ListView ALLOW entry, and dropping an authored `Tool` into the skipped vendored dir (guard blind by design, provenance test red). Pre-existing and unrelated, filed as objectui#3219: plugin-calendar's registration.test.tsx fails on main (an @object-ui/react vi.mock missing `subscribeDataChanges`); reproduced with this branch stashed. Claude-Session: https://claude.ai/code/session_01PRJtkgUAaVG11FsJQbvZWA Co-authored-by: Claude <noreply@anthropic.com>
1 parent 39033a3 commit c5ccbd5

21 files changed

Lines changed: 1153 additions & 122 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@object-ui/data-objectstack": major
3+
"@object-ui/plugin-chatbot": major
4+
"@object-ui/plugin-list": major
5+
---
6+
7+
Stop declaring 12 `@object-ui/data-objectstack` / `@object-ui/plugin-chatbot` /
8+
`@object-ui/plugin-list` symbols under names `@objectstack/spec` owns
9+
(objectui#3160, objectstack#4115 batch 6). All three packages leave the ledger.
10+
11+
**Breaking for importers of `@object-ui/data-objectstack`** — four exported
12+
names changed, because the spec exports the same name for a *different* thing:
13+
14+
| was | now | what the spec's same-named export actually is |
15+
|:--|:--|:--|
16+
| `CacheStats` | `MetadataCacheStats` | the platform `ICacheService` counters (`keyCount`, `memoryUsage`) |
17+
| `MetadataSaveOptions` | `MetadataClientSaveOptions` | options for writing a metadata item to a **file** (`format`, `path`, `indent`, `atomic`) |
18+
| `SecurityPolicy` | `SecurityManagerPolicy` | the package supply-chain policy (`autoScan`, licences, code signing, sandbox) |
19+
| `ValidationError` | `DataApiValidationError` | a plain `{ field, message, code? }` entry in a validation report |
20+
21+
Each pair is disjoint or nearly so — `MetadataSaveOptions` and `SecurityPolicy`
22+
share not one key with the spec type whose name they wore — so none of them was
23+
a dialect to reconcile; they were four unrelated concepts squatting on spec
24+
names. `DataApiValidationError` follows the `<what was validated>Validation<Error|Result>`
25+
convention registered on objectstack#4115 (`@object-ui/core` took
26+
`SchemaNodeValidationError` in batch 4). Its **runtime** `name` deliberately
27+
stays `'ValidationError'`: `normaliseClientError` and `@object-ui/react`'s
28+
error-message helper both sniff `err.name`, so that string is a wire contract,
29+
not a symbol.
30+
31+
**Breaking for importers of `@object-ui/plugin-chatbot`**`PendingActionRow`
32+
and `PendingActionStatus` are now re-exported from `@objectstack/spec/contracts`
33+
instead of hand-transcribed, which narrows them. The copies had drifted three
34+
ways, and each drift had **disabled a compile-time check** rather than merely
35+
differed from one:
36+
37+
- `status: PendingActionStatus | string` — a union with `string` absorbs the
38+
literals, so that annotation carried no information at all;
39+
- `[key: string]: unknown` — the objectstack#4075 mechanism: an index signature
40+
makes every structural comparison against the spec answer "identical", however
41+
far the copy has drifted;
42+
- `created_at` / `updated_at`, which the service contract does not carry and no
43+
consumer in this repo reads.
44+
45+
**Breaking for importers of `@object-ui/plugin-list`**`ViewTab` is derived from the spec's `ViewTabSchema`
46+
— from its **input** side, because `pinned` / `isDefault` / `visible` carry
47+
`.default()`s and this component is handed authored metadata, not parsed output.
48+
That removes a renderer-side tolerance the copy carried: `visible` accepted
49+
`string | boolean` and the tab bar compared it against the literal `'false'`, a
50+
spelling no producer emits. `label` also stops being required (the spec makes it
51+
optional; `name` is the identifier) and `filter` stops being `any`.
52+
53+
`ListView` and `UserFilters` keep their names as declared dialects: both are the
54+
React **renderers** of the spec types whose names they share, and each takes that
55+
spec type as a prop (`ListViewProps.schema`, `UserFiltersProps.config`) rather
56+
than restating its shape. `Tool` and `MessageContent` in `plugin-chatbot` are
57+
vendored Vercel AI Elements / Shadcn primitives — upstream's component API, not
58+
objectui's authored surface — so the guard now skips that directory the same way
59+
it already skips `components/src/ui/`, with a test that fails if any file there
60+
stops carrying its vendor banner.

packages/data-objectstack/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,12 @@ import {
179179
BulkOperationError, // Bulk operation failures with partial results
180180
ConnectionError, // Network/connection errors (503/504)
181181
AuthenticationError, // Authentication failures (401/403)
182-
ValidationError, // Data validation errors (400)
182+
DataApiValidationError, // Data validation errors (400). Its runtime `name` is
183+
// still 'ValidationError' — that string is the wire
184+
// discriminator shared with @objectstack/client. The
185+
// SYMBOL is prefixed because @objectstack/spec/kernel
186+
// owns `ValidationError` for a { field, message, code? }
187+
// record (objectui#3160).
183188
} from '@object-ui/data-objectstack';
184189
```
185190

packages/data-objectstack/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232
"dependencies": {
3333
"@object-ui/core": "workspace:*",
3434
"@object-ui/types": "workspace:*",
35-
"@objectstack/client": "^17.0.0-rc.1"
35+
"@objectstack/client": "^17.0.0-rc.1",
36+
"@objectstack/spec": "^17.0.0-rc.1"
3637
},
3738
"devDependencies": {
38-
"@objectstack/spec": "^17.0.0-rc.1",
3939
"tsup": "^8.5.1",
4040
"typescript": "^6.0.3",
4141
"vitest": "^4.1.10"

packages/data-objectstack/src/cache/MetadataCache.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,18 @@ interface CachedSchema {
1717
}
1818

1919
/**
20-
* Cache statistics for monitoring
20+
* Statistics reported by {@link MetadataCache.getStats} for monitoring.
21+
*
22+
* NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
23+
* interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
24+
* describes the platform's `ICacheService` — a server-side KV cache measured by
25+
* `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
26+
* of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
27+
* concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
28+
* Neither type has a key the other has, so this is a name collision, not a
29+
* dialect — deriving would have replaced every field.
2130
*/
22-
export interface CacheStats {
31+
export interface MetadataCacheStats {
2332
size: number;
2433
maxSize: number;
2534
hits: number;
@@ -218,7 +227,7 @@ export class MetadataCache {
218227
*
219228
* @returns Cache statistics including hit rate
220229
*/
221-
getStats(): CacheStats {
230+
getStats(): MetadataCacheStats {
222231
const total = this.stats.hits + this.stats.misses;
223232
const hitRate = total > 0 ? this.stats.hits / total : 0;
224233

packages/data-objectstack/src/errors.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
BulkOperationError,
1414
ConnectionError,
1515
AuthenticationError,
16-
ValidationError,
16+
DataApiValidationError,
1717
createErrorFromResponse,
1818
isObjectStackError,
1919
isErrorType,
@@ -189,20 +189,20 @@ describe('Error Classes', () => {
189189
});
190190
});
191191

192-
describe('ValidationError', () => {
192+
describe('DataApiValidationError', () => {
193193
it('should create validation error', () => {
194-
const error = new ValidationError('Invalid input');
194+
const error = new DataApiValidationError('Invalid input');
195195

196196
expect(error.message).toBe('Invalid input');
197197
expect(error.code).toBe('VALIDATION_ERROR');
198198
expect(error.statusCode).toBe(400);
199199
expect(error.name).toBe('ValidationError');
200200
expect(error).toBeInstanceOf(ObjectStackError);
201-
expect(error).toBeInstanceOf(ValidationError);
201+
expect(error).toBeInstanceOf(DataApiValidationError);
202202
});
203203

204204
it('should include field information', () => {
205-
const error = new ValidationError('Email is invalid', 'email');
205+
const error = new DataApiValidationError('Email is invalid', 'email');
206206

207207
expect(error.field).toBe('email');
208208
expect(error.details).toHaveProperty('field', 'email');
@@ -214,7 +214,7 @@ describe('Error Classes', () => {
214214
{ field: 'age', message: 'Must be a positive number' },
215215
];
216216

217-
const error = new ValidationError(
217+
const error = new DataApiValidationError(
218218
'Validation failed',
219219
undefined,
220220
validationErrors
@@ -225,7 +225,7 @@ describe('Error Classes', () => {
225225
});
226226

227227
it('should return empty array when no validation errors', () => {
228-
const error = new ValidationError('Validation failed');
228+
const error = new DataApiValidationError('Validation failed');
229229

230230
expect(error.getValidationErrors()).toEqual([]);
231231
});
@@ -286,7 +286,7 @@ describe('Error Helpers', () => {
286286
expect(error.code).toBe('NOT_FOUND');
287287
});
288288

289-
it('should create ValidationError for 400 status', () => {
289+
it('should create DataApiValidationError for 400 status', () => {
290290
const response = {
291291
status: 400,
292292
message: 'Bad request',
@@ -299,9 +299,9 @@ describe('Error Helpers', () => {
299299

300300
const error = createErrorFromResponse(response);
301301

302-
expect(error).toBeInstanceOf(ValidationError);
302+
expect(error).toBeInstanceOf(DataApiValidationError);
303303
expect(error.statusCode).toBe(400);
304-
expect((error as ValidationError).validationErrors).toEqual([
304+
expect((error as DataApiValidationError).validationErrors).toEqual([
305305
{ field: 'email', message: 'Invalid email' },
306306
]);
307307
});
@@ -379,7 +379,7 @@ describe('Error Helpers', () => {
379379
const bulkError = new BulkOperationError('create', 0, 1, []);
380380
const connError = new ConnectionError('timeout');
381381
const authError = new AuthenticationError();
382-
const validError = new ValidationError('invalid');
382+
const validError = new DataApiValidationError('invalid');
383383

384384
expect(isObjectStackError(metadataError)).toBe(true);
385385
expect(isObjectStackError(bulkError)).toBe(true);
@@ -409,7 +409,7 @@ describe('Error Helpers', () => {
409409

410410
it('should return false for non-matching error type', () => {
411411
const error = new MetadataNotFoundError('users');
412-
expect(isErrorType(error, ValidationError)).toBe(false);
412+
expect(isErrorType(error, DataApiValidationError)).toBe(false);
413413
});
414414

415415
it('should return true for base class check', () => {

packages/data-objectstack/src/errors.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,24 @@ export class AuthenticationError extends ObjectStackError {
159159
}
160160

161161
/**
162-
* Error thrown when data validation fails
162+
* Error thrown when the ObjectStack data API rejects a write as invalid.
163+
*
164+
* NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
165+
* class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
166+
* a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
167+
* manifest's validation report — and `@object-ui/types` re-exports it under that
168+
* name. This is a runtime `Error` subclass carrying an HTTP status plus a list
169+
* of such entries, so the two are not even the same KIND of thing.
170+
*
171+
* The name follows the convention registered on objectstack#4115 for this
172+
* family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
173+
* took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
174+
* the data API.
163175
*/
164-
export class ValidationError extends ObjectStackError {
176+
export class DataApiValidationError extends ObjectStackError {
165177
/**
166-
* Create a new ValidationError
167-
*
178+
* Create a new DataApiValidationError
179+
*
168180
* @param message - Human-readable error message
169181
* @param field - The field that failed validation (optional)
170182
* @param validationErrors - Array of validation error details
@@ -186,6 +198,12 @@ export class ValidationError extends ObjectStackError {
186198
...details,
187199
}
188200
);
201+
// The RUNTIME name stays `'ValidationError'` on purpose — it is the wire
202+
// discriminator this adapter shares with `@objectstack/client` and with
203+
// consumers that sniff `err.name` rather than `instanceof`
204+
// (`normaliseClientError` here, `@object-ui/react`'s `error-message`).
205+
// Renaming the TypeScript symbol is a source-level rename; renaming this
206+
// string would be a behaviour change nobody asked for.
189207
this.name = 'ValidationError';
190208
}
191209

@@ -232,7 +250,7 @@ export function createErrorFromResponse(response: Record<string, unknown>, conte
232250
return new ObjectStackError(message, 'NOT_FOUND', 404, details);
233251

234252
case 400:
235-
return new ValidationError(message, undefined, (response?.data as Record<string, unknown>)?.errors as Array<{ field: string; message: string }>, details);
253+
return new DataApiValidationError(message, undefined, (response?.data as Record<string, unknown>)?.errors as Array<{ field: string; message: string }>, details);
236254

237255
case 503:
238256
return new ConnectionError(message, (response?.config as Record<string, unknown>)?.url as string, details, 503);

packages/data-objectstack/src/index.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { ObjectStackClient, type QueryOptions as ObjectStackQueryOptions } from '@objectstack/client';
10+
import type { DroppedFieldsEvent } from '@objectstack/spec/data';
1011
import type {
1112
DataSource,
1213
BatchTransactionOperation,
@@ -39,7 +40,7 @@ import {
3940
MetadataNotFoundError,
4041
BulkOperationError,
4142
ConnectionError,
42-
ValidationError,
43+
DataApiValidationError,
4344
createErrorFromResponse,
4445
} from './errors';
4546

@@ -588,7 +589,7 @@ export function isConcurrentUpdateError(error: unknown): error is ConcurrentUpda
588589
*
589590
* Two shapes are recognised:
590591
* - `409` + `CONCURRENT_UPDATE` → {@link ConcurrentUpdateError};
591-
* - `400` + `VALIDATION_FAILED` → {@link ValidationError}, carrying the
592+
* - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
592593
* server's per-field entries so a form can mark the offending inputs
593594
* instead of showing one undirected toast.
594595
*/
@@ -621,7 +622,7 @@ export function normaliseClientError(error: unknown): unknown {
621622
})
622623
.filter((x): x is { field: string; message: string } => x !== null);
623624

624-
return new ValidationError(
625+
return new DataApiValidationError(
625626
typeof e.message === 'string' ? e.message : 'Validation failed',
626627
validationErrors[0]?.field,
627628
validationErrors,
@@ -721,16 +722,21 @@ export type BatchProgressListener = (event: BatchProgressEvent) => void;
721722
/**
722723
* One server-reported write-strip: caller-supplied fields the backend LEGALLY
723724
* removed from a write before persisting (a non-system caller cannot seed a
724-
* `readonly` field, a `readonlyWhen` predicate locked it, etc.). Mirrors the
725-
* framework `DroppedFieldsEvent` (spec `DroppedFieldsEventSchema`) structurally
726-
* so we don't pin a client type version — `reason` is kept as a widened string
727-
* for forward-compatibility with reasons added server-side.
725+
* `readonly` field, a `readonlyWhen` predicate locked it, etc.).
726+
*
727+
* THE spec type, re-exported (objectui#3160, objectstack#4115 ledger batch 6).
728+
* Until then this was a hand copy whose comment said it "mirrors the framework
729+
* `DroppedFieldsEvent` (spec `DroppedFieldsEventSchema`) structurally so we
730+
* don't pin a client type version", with `reason` widened from the spec's
731+
* `'readonly' | 'readonly_when'` to bare `string` "for forward-compatibility
732+
* with reasons added server-side". Both halves of that reasoning are the
733+
* failure mode this ledger exists to remove: the spec IS the client type
734+
* version, and a consumer-side widening of a producer's enum is precisely the
735+
* lenient fallback AGENTS.md #12 bans — it deletes the only compile-time signal
736+
* that would tell `AdapterProvider`'s toast wording (which branches on
737+
* `readonly_when`) that a new reason had appeared.
728738
*/
729-
export interface DroppedFieldsEvent {
730-
object: string;
731-
fields: string[];
732-
reason: string;
733-
}
739+
export type { DroppedFieldsEvent };
734740

735741
/**
736742
* Emitted after a create/update whose response carried `droppedFields`
@@ -3511,14 +3517,14 @@ export {
35113517
BulkOperationError,
35123518
ConnectionError,
35133519
AuthenticationError,
3514-
ValidationError,
3520+
DataApiValidationError,
35153521
createErrorFromResponse,
35163522
isObjectStackError,
35173523
isErrorType,
35183524
} from './errors';
35193525

35203526
// Export cache types
3521-
export type { CacheStats } from './cache/MetadataCache';
3527+
export type { MetadataCacheStats } from './cache/MetadataCache';
35223528

35233529
// v3.0.0 Deep Integration modules
35243530
export { CloudOperations } from './cloud';
@@ -3546,7 +3552,7 @@ export type {
35463552
MetadataClientConfig,
35473553
MetadataListOptions,
35483554
MetadataDraftHeader,
3549-
MetadataSaveOptions,
3555+
MetadataClientSaveOptions,
35503556
MetadataGetOptions,
35513557
MetadataDeleteOptions,
35523558
MetadataHistoryOptions,
@@ -3563,7 +3569,7 @@ export type {
35633569
} from './metadata-client';
35643570

35653571
export { SecurityManager } from './security';
3566-
export type { SecurityPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
3572+
export type { SecurityManagerPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
35673573

35683574
export { createDefaultCanvasConfig, snapToGrid, calculateAutoLayout } from './studio';
35693575
export type { StudioCanvasConfig, StudioPropertyEditor, StudioThemeBuilderConfig, StudioColorPalette, StudioTypographyPreset, StudioShadowPreset } from './studio';

0 commit comments

Comments
 (0)