Skip to content

Commit 5a3363b

Browse files
committed
feat(spec)!: remove dead author-facing metadata properties (#2377)
Removes spec properties that parsed but had no runtime consumer (ADR-0049 enforce-or-remove) — a false affordance, especially for AI-authored metadata. Follow-up slice to #2402. Verified dead against packages/spec/liveness/*.json and a repo-wide reader grep. Removed: - field: vectorConfig (+VectorConfigSchema+types), fileAttachmentConfig (+FileAttachmentConfigSchema+types), dependencies - object: versioning, softDelete, search, recordName, keyPrefix (+ their sub-schemas/types) — each now a rejecting tombstone in UNKNOWN_KEY_GUIDANCE - action: timeout - agent: planning.strategy, planning.allowReplan (keep planning.maxIterations) - dataset: measures.certified Ledgers, ledger README table, api-surface.json, and json-schema.manifest.json updated; spec tests/fixtures migrated (analytics/rest/lint/cli). Deferred the authoring-form-surfaced dead props (field.index/columnName/referenceFilters; object.tags/active/isSystem/abstract/enable.searchable/trash/mru; agent.tenantId) and action.type:'form' — they cascade into i18n/lint/UX and stay authorWarn'd. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011XchBKhKdjbhQZymt9Gn9B
1 parent e057f42 commit 5a3363b

28 files changed

Lines changed: 151 additions & 962 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: remove dead author-facing metadata properties (#2377, ADR-0049 enforce-or-remove)
6+
7+
Removes a batch of spec properties that parsed but had **no runtime consumer**
8+
authoring them was a false affordance (especially dangerous for AI-authored
9+
metadata). Verified dead against the liveness ledger (`packages/spec/liveness/*.json`)
10+
and a repo-wide grep of readers. This is the follow-up slice to #2402.
11+
12+
## Removed (each was `dead` + no reader anywhere)
13+
14+
- **field** (`field.zod.ts`): `vectorConfig` (+ `VectorConfigSchema` + types),
15+
`fileAttachmentConfig` (+ `FileAttachmentConfigSchema` + types), `dependencies`.
16+
Vector fields keep the live flat `dimensions` prop; file/image fields keep the
17+
live flat `multiple`/`accept`/`maxSize` siblings.
18+
- **object** (`object.zod.ts`): `versioning` (+ `VersioningConfigSchema`),
19+
`softDelete` (+ `SoftDeleteConfigSchema`), `search` (+ `SearchConfigSchema`),
20+
`recordName`, `keyPrefix`. Each is now a **rejecting tombstone** in
21+
`UNKNOWN_KEY_GUIDANCE` carrying the upgrade prescription.
22+
- **action** (`action.zod.ts`): `timeout` (server uses `body.timeoutMs`; no
23+
action-level timeout is enforced).
24+
- **agent** (`agent.zod.ts`): `planning.strategy`, `planning.allowReplan`
25+
(only `planning.maxIterations` is read by the runtime).
26+
- **dataset** (`dataset.zod.ts`): `measures.certified` (declared-but-unenforced
27+
governance flag — never compiled into the Cube).
28+
29+
Liveness ledgers, the ledger README table, and `api-surface.json` are updated;
30+
the removed sub-schema keys are dropped from `json-schema.manifest.json`.
31+
32+
## Migration
33+
34+
- **field/agent/dataset/action props**: authoring them is now silently stripped
35+
(they never did anything). Remove them. Vector → set flat `dimensions`;
36+
file/image → set flat `multiple`/`accept`/`maxSize`.
37+
- **object props**: `ObjectSchema.create()` now throws a located error naming the
38+
replacement — `versioning`/`softDelete` → hard deletes + `Field.trackHistory` /
39+
`lifecycle`; `search``searchableFields`; `recordName` → an `autonumber`
40+
`Field` designated as `nameField`; `keyPrefix` → remove (never had an effect).
41+
42+
## Deliberately NOT removed (dead, but entangled — a scoped follow-up)
43+
44+
`field.index`/`columnName`/`referenceFilters` and object
45+
`tags`/`active`/`isSystem`/`abstract`/`enable.searchable`/`enable.trash`/`enable.mru`
46+
and `agent.tenantId` are surfaced in the Studio metadata-authoring forms
47+
(`*.form.ts`) — removing them cascades into i18n bundle regeneration, so they are
48+
deferred. `action.type:'form'` has a dedicated build-time lint (`lint-view-refs.ts`)
49+
and a first-party showcase usage, so it needs a UX decision. `field.columnName`
50+
additionally has an ADR-0062 D7 lint. These stay `dead` + `authorWarn` in the
51+
ledgers.

packages/cli/src/utils/lint-liveness-properties.test.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
/**
1010
* These run against the REAL ledgers shipped by `@objectstack/spec` (the same
1111
* files the gate enforces), so they double as a contract test: if an
12-
* `authorWarn` annotation is removed from `versioning` / `columnName` / etc.,
12+
* `authorWarn` annotation is removed from `abstract` / `columnName` / etc.,
1313
* the matching assertion fails.
1414
*/
1515

@@ -18,9 +18,9 @@ const rules = (findings: { rule: string }[]) => findings.map((f) => f.rule);
1818
const paths = (findings: { message: string }[]) => findings.map((f) => f.message);
1919

2020
describe('lintLivenessProperties', () => {
21-
it('warns on a present dead object block (versioning)', () => {
22-
const findings = lintLivenessProperties(objStack({ versioning: { enabled: true } }));
23-
const v = findings.find((f) => f.message.includes('versioning'));
21+
it('warns on a present dead object prop (abstract)', () => {
22+
const findings = lintLivenessProperties(objStack({ abstract: true }));
23+
const v = findings.find((f) => f.message.includes('abstract'));
2424
expect(v).toBeDefined();
2525
expect(v!.rule).toBe(LIVENESS_DEAD_PROPERTY);
2626
expect(v!.where).toBe("object 'widget'");
@@ -71,7 +71,7 @@ describe('lintLivenessProperties', () => {
7171

7272
it('handles objects as a keyed record (not just arrays)', () => {
7373
const findings = lintLivenessProperties({
74-
objects: { widget: { name: 'widget', versioning: { enabled: true } } },
74+
objects: { widget: { name: 'widget', abstract: true } },
7575
});
7676
expect(rules(findings)).toContain(LIVENESS_DEAD_PROPERTY);
7777
});
@@ -112,9 +112,9 @@ describe('lintLivenessProperties', () => {
112112
expect(hits[0].where).toBe("flow 'f1'");
113113
});
114114

115-
it('warns on action.timeout (no runtime enforcement)', () => {
116-
const findings = lintLivenessProperties({ actions: [{ name: 'a1', timeout: 5000 }] });
117-
expect(paths(findings).some((m) => m.includes('`timeout`'))).toBe(true);
115+
it('warns on action.undoable (experimental — declared but not enforced)', () => {
116+
const findings = lintLivenessProperties({ actions: [{ name: 'a1', undoable: true }] });
117+
expect(paths(findings).some((m) => m.includes('`undoable`'))).toBe(true);
118118
});
119119

120120
it('warns on the security-shaped dead props (tool.permissions / permission.contextVariables)', () => {
@@ -131,13 +131,6 @@ describe('lintLivenessProperties', () => {
131131
expect(paths(perm).some((m) => m.includes('contextVariables'))).toBe(true);
132132
});
133133

134-
it('warns on dataset measure certified flag via array fan-out', () => {
135-
const findings = lintLivenessProperties({
136-
datasets: [{ name: 'd1', measures: [{ name: 'arr', certified: true }] }],
137-
});
138-
expect(paths(findings).some((m) => m.includes('measures.certified'))).toBe(true);
139-
});
140-
141134
it('stays silent on clean flat-collection items', () => {
142135
const findings = lintLivenessProperties({
143136
flows: [{ name: 'clean', nodes: [{ id: 'n1' }] }],

packages/lint/src/validate-widget-bindings.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function chartStack(widgetOverrides: Record<string, unknown> = {}) {
5050
object: 'expense_line',
5151
dimensions: [{ name: 'category', field: 'category' }],
5252
measures: [
53-
{ name: 'sum_amount', label: 'sum_amount', aggregate: 'sum', field: 'amount', certified: true },
53+
{ name: 'sum_amount', label: 'sum_amount', aggregate: 'sum', field: 'amount' },
5454
{ name: 'ticket_count', label: 'ticket_count', aggregate: 'count' },
5555
],
5656
}],

packages/rest/src/analytics-routes.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ describe('POST /analytics/dataset/query', () => {
5858

5959
expect(res.statusCode).toBe(200);
6060
expect(res.body).toEqual({ rows: [{ region: 'NA', revenue: 100 }], fields: [] });
61-
// dataset was schema-validated before reaching the service (certified default applied)
61+
// dataset was schema-validated before reaching the service
6262
const passedDataset = queryDataset.mock.calls[0][0];
63-
expect(passedDataset.measures[0].certified).toBe(false);
63+
expect(passedDataset.measures[0].name).toBe('revenue');
6464
expect(queryDataset.mock.calls[0][1]).toEqual(selection);
6565
});
6666

packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ const salesDataset = DatasetSchema.parse({
1616
{ name: 'close_month', label: 'Close Month', field: 'close_date', type: 'date', dateGranularity: 'month' },
1717
],
1818
measures: [
19-
{ name: 'revenue', label: 'Revenue', aggregate: 'sum', field: 'amount', certified: true, format: '$0,0.00' },
20-
{ name: 'deal_count', label: 'Deals', aggregate: 'count', certified: false },
21-
{ name: 'won_amount', label: 'Won', aggregate: 'sum', field: 'amount', certified: false, filter: { stage: 'won' } },
22-
{ name: 'win_rate', label: 'Win Rate', aggregate: 'sum', certified: false, derived: { op: 'ratio', of: ['won_amount', 'revenue'] } },
19+
{ name: 'revenue', label: 'Revenue', aggregate: 'sum', field: 'amount', format: '$0,0.00' },
20+
{ name: 'deal_count', label: 'Deals', aggregate: 'count' },
21+
{ name: 'won_amount', label: 'Won', aggregate: 'sum', field: 'amount', filter: { stage: 'won' } },
22+
{ name: 'win_rate', label: 'Win Rate', aggregate: 'sum', derived: { op: 'ratio', of: ['won_amount', 'revenue'] } },
2323
],
2424
});
2525

packages/services/service-analytics/src/__tests__/dataset-executor.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const dataset = DatasetSchema.parse({
5252
filter: { is_deleted: { $ne: true } },
5353
dimensions: [{ name: 'region', field: 'account.region', type: 'string' }],
5454
measures: [
55-
{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true },
55+
{ name: 'revenue', aggregate: 'sum', field: 'amount' },
5656
{ name: 'won_amount', aggregate: 'sum', field: 'amount', filter: { stage: 'won' } },
5757
{ name: 'win_rate', aggregate: 'sum', derived: { op: 'ratio', of: ['won_amount', 'revenue'] } },
5858
],

packages/services/service-analytics/src/__tests__/dataset-rls-integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const dataset = DatasetSchema.parse({
2525
object: 'opportunity',
2626
include: ['account'],
2727
dimensions: [{ name: 'region', field: 'account.region', type: 'string' }],
28-
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }],
28+
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }],
2929
});
3030

3131
/**

packages/services/service-analytics/src/__tests__/query-dataset.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const dataset = DatasetSchema.parse({
1111
object: 'opportunity',
1212
include: ['account'],
1313
dimensions: [{ name: 'region', field: 'account.region', type: 'string' }],
14-
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }],
14+
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }],
1515
});
1616

1717
function service(captured: { sql: string; params: unknown[] }[]) {
@@ -103,7 +103,7 @@ describe('AnalyticsService.queryDataset', () => {
103103
const priced = DatasetSchema.parse({
104104
name: 'sales_priced', label: 'Sales', object: 'opportunity', include: [],
105105
dimensions: [{ name: 'stage', field: 'stage', type: 'string' }],
106-
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue', format: '0,0', currency: 'USD', certified: true }],
106+
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue', format: '0,0', currency: 'USD' }],
107107
});
108108
const svc = new AnalyticsService({
109109
queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }),
@@ -165,7 +165,7 @@ describe('AnalyticsService.queryDataset', () => {
165165
const labeled = DatasetSchema.parse({
166166
name: 'sales2', label: 'Sales', object: 'opportunity', include: ['account'],
167167
dimensions: [{ name: 'region', field: 'account.region', type: 'string', label: 'Region' }],
168-
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue', certified: true }],
168+
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue' }],
169169
});
170170
const result = await service([]).queryDataset(
171171
labeled,
@@ -180,7 +180,7 @@ describe('AnalyticsService.queryDataset', () => {
180180
const dated = DatasetSchema.parse({
181181
name: 'sales3', label: 'Sales', object: 'opportunity', include: [],
182182
dimensions: [{ name: 'closed', field: 'close_date', type: 'date' }],
183-
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }],
183+
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }],
184184
});
185185
const svc = new AnalyticsService({
186186
queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }),
@@ -198,7 +198,7 @@ describe('AnalyticsService.queryDataset', () => {
198198
const byAccount = DatasetSchema.parse({
199199
name: 'sales_acct', label: 'Sales', object: 'opportunity', include: [],
200200
dimensions: [{ name: 'account', field: 'account', type: 'lookup', label: 'Account' }],
201-
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }],
201+
measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }],
202202
});
203203
const svc = new AnalyticsService({
204204
queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }),

packages/spec/api-surface.json

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,6 @@
321321
"FieldReferenceSchema (const)",
322322
"FieldSchema (const)",
323323
"FieldType (type)",
324-
"FileAttachmentConfig (type)",
325-
"FileAttachmentConfigInput (type)",
326-
"FileAttachmentConfigSchema (const)",
327324
"Filter (type)",
328325
"FilterCondition (type)",
329326
"FilterConditionSchema (const)",
@@ -460,7 +457,6 @@
460457
"ScriptBodySchema (const)",
461458
"ScriptValidation (type)",
462459
"ScriptValidationSchema (const)",
463-
"SearchConfigSchema (const)",
464460
"Seed (type)",
465461
"SeedIdentity (type)",
466462
"SeedIdentitySchema (const)",
@@ -485,8 +481,6 @@
485481
"SetOperatorSchema (const)",
486482
"ShardingConfig (type)",
487483
"ShardingConfigSchema (const)",
488-
"SoftDeleteConfig (type)",
489-
"SoftDeleteConfigSchema (const)",
490484
"SortNode (type)",
491485
"SortNodeSchema (const)",
492486
"SpecialOperatorSchema (const)",
@@ -506,11 +500,6 @@
506500
"VALID_AST_OPERATORS (const)",
507501
"ValidationRule (type)",
508502
"ValidationRuleSchema (const)",
509-
"VectorConfig (type)",
510-
"VectorConfigInput (type)",
511-
"VectorConfigSchema (const)",
512-
"VersioningConfig (type)",
513-
"VersioningConfigSchema (const)",
514503
"WindowFunction (const)",
515504
"WindowFunctionNode (type)",
516505
"WindowFunctionNodeSchema (const)",

packages/spec/json-schema.manifest.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,6 @@
789789
"data/FieldNode",
790790
"data/FieldReference",
791791
"data/FieldType",
792-
"data/FileAttachmentConfig",
793792
"data/FilterCondition",
794793
"data/FormatValidation",
795794
"data/FullTextSearch",
@@ -844,7 +843,6 @@
844843
"data/SSLConfig",
845844
"data/ScriptBody",
846845
"data/ScriptValidation",
847-
"data/SearchConfig",
848846
"data/Seed",
849847
"data/SeedIdentity",
850848
"data/SeedLoadResult",
@@ -855,7 +853,6 @@
855853
"data/SelectOption",
856854
"data/SetOperator",
857855
"data/ShardingConfig",
858-
"data/SoftDeleteConfig",
859856
"data/SortNode",
860857
"data/SpecialOperator",
861858
"data/StateMachineValidation",
@@ -865,8 +862,6 @@
865862
"data/TimeUpdateInterval",
866863
"data/TransformType",
867864
"data/ValidationRule",
868-
"data/VectorConfig",
869-
"data/VersioningConfig",
870865
"data/WindowFunction",
871866
"data/WindowFunctionNode",
872867
"data/WindowSpec",

0 commit comments

Comments
 (0)