Skip to content

Commit 61282f9

Browse files
os-zhuangclaude
andauthored
fix(platform-objects): drop the never-implemented runtime option from sys_setting.scope (#6036) (#6700)
`sys_setting.scope` declared four cascade layers while the platform only ever had three. `SpecifierScopeSchema` is `z.enum(['global','tenant','user'])`, `SettingsService` never mentions `'runtime'`, and its `scopeRank()` switch handles only those three — so no code path could write such a row and none could read one back. The sibling `sys_setting_audit.scope` already declared only three. Removed rather than implemented (ADR-0049 enforce-or-remove, remove route). A new pin compares the object's option list against `SpecifierScopeSchema` directly, so a future divergence in either direction lands as a red test. Gated on a measurement, not the zero-write-path prediction: a real engine booted over the platform objects and driven through `/api/settings/:namespace` stored 4 rows (tenant 3, global 1) and 0 with `scope='runtime'`, with a positive control proving the query surfaces such a row when injected. Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw Co-authored-by: Claude <noreply@anthropic.com>
1 parent d06b3dc commit 61282f9

7 files changed

Lines changed: 102 additions & 9 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
---
4+
5+
fix(platform-objects): `sys_setting.scope` drops the never-implemented `runtime` option (#6036)
6+
7+
The `scope` select declared four cascade layers while the platform only ever had
8+
three. `SpecifierScopeSchema` (`packages/spec/src/system/settings-manifest.zod.ts`)
9+
is `z.enum(['global', 'tenant', 'user'])`, `SettingsService` never mentions the
10+
string `'runtime'` anywhere, and its `scopeRank()` switch handles only those same
11+
three — so no code path could write such a row and none could read one back. The
12+
sibling audit object `sys_setting_audit.scope` already declared only three. This
13+
was a declared-but-unenforced value domain of the ADR-0049 kind: nobody could hit
14+
it, but the next reader of the object definition would reasonably conclude the
15+
platform supports a fourth scope layer.
16+
17+
Removed rather than implemented — there is no runtime-scope product intent, and
18+
the spec enum stays the reference truth for what the cascade's layers are. A new
19+
pin (`sys-setting.scope-options.test.ts`) compares the object's option list
20+
against `SpecifierScopeSchema` directly, so a future divergence in either
21+
direction lands as a red test instead of a second silent one.
22+
23+
Removal was gated on a measurement, not on the zero-write-path prediction: a real
24+
engine booted over the platform objects, driven through the real
25+
`/api/settings/:namespace` write path, stored 4 rows (`tenant` 3, `global` 1) and
26+
**0** with `scope='runtime'` — with a positive control proving the query does
27+
surface such a row when one is injected directly.
28+
29+
No stored data is affected and no consumer read the option, so this is a
30+
definition-only correction.

packages/platform-objects/src/apps/translations/en.objects.generated.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,8 +2931,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
29312931
options: {
29322932
global: "Global",
29332933
tenant: "Tenant",
2934-
user: "User",
2935-
runtime: "Runtime"
2934+
user: "User"
29362935
}
29372936
},
29382937
user_id: {

packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,8 +2931,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
29312931
options: {
29322932
global: "Global",
29332933
tenant: "Inquilino",
2934-
user: "Usuario",
2935-
runtime: "Tiempo de ejecución"
2934+
user: "Usuario"
29362935
}
29372936
},
29382937
user_id: {

packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,8 +2931,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
29312931
options: {
29322932
global: "グローバル",
29332933
tenant: "テナント",
2934-
user: "ユーザー",
2935-
runtime: "ランタイム"
2934+
user: "ユーザー"
29362935
}
29372936
},
29382937
user_id: {

packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,8 +2931,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
29312931
options: {
29322932
global: "全局",
29332933
tenant: "租户",
2934-
user: "用户",
2935-
runtime: "运行时"
2934+
user: "用户"
29362935
}
29372936
},
29382937
user_id: {

packages/platform-objects/src/system/sys-setting.object.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,20 @@ export const SysSetting = ObjectSchema.create({
119119
description: 'Specifier key inside the namespace (snake_case).',
120120
}),
121121

122+
// The option list is the storage-side mirror of `SpecifierScopeSchema`
123+
// (`packages/spec/src/system/settings-manifest.zod.ts`), which is the
124+
// reference truth for the cascade's layers. Keep the two in step —
125+
// `sys-setting.scope-options.test.ts` pins the parity, and the sibling
126+
// audit object (`sys_setting_audit.scope`) mirrors the same three.
127+
// A fourth option lived here declaring `runtime` (#6036): the spec enum
128+
// never accepted it, `SettingsService` never mentioned it, and no write
129+
// path could produce such a row — a declared-but-unenforced value domain
130+
// of exactly the ADR-0049 kind. Removed rather than implemented.
122131
scope: Field.select(
123132
[
124133
{ label: 'Global', value: 'global' },
125134
{ label: 'Tenant', value: 'tenant' },
126135
{ label: 'User', value: 'user' },
127-
{ label: 'Runtime',value: 'runtime' },
128136
],
129137
{
130138
label: 'Scope',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// #6036 — `sys_setting.scope` once declared a fourth option, `runtime`, that
4+
// nothing in the platform could produce or consume: `SpecifierScopeSchema` is
5+
// three-valued, `SettingsService` never mentions the string, and every write
6+
// reaches the table through `set()`/`setMany()`, whose scope comes from the
7+
// manifest registry (`reg.scopes`) — i.e. from that same three-value enum.
8+
// A declared-but-unenforced value domain of exactly the ADR-0049 kind.
9+
//
10+
// These pins make the divergence loud instead of dormant. The load-bearing one
11+
// is the PARITY assertion: the storage column's option list and the spec enum
12+
// are two spellings of one truth, so they are compared to each other rather
13+
// than to a hand-copied literal that would need editing on both sides anyway.
14+
// A future fourth cascade layer therefore lands here as a red test, not as a
15+
// silent re-divergence.
16+
import { describe, expect, it } from 'vitest';
17+
import { SpecifierScopeSchema } from '@objectstack/spec/system';
18+
import { SysSetting } from './sys-setting.object.js';
19+
import { SysSettingAudit } from './sys-setting-audit.object.js';
20+
21+
/** Declared option values of a select field, in declaration order. */
22+
function optionValues(object: unknown, field: string): string[] {
23+
const f = (object as any).fields?.[field];
24+
expect(f, `${field} field exists`).toBeDefined();
25+
expect(f.type).toBe('select');
26+
return ((f.options ?? []) as Array<{ value: unknown }>).map((o) => String(o.value));
27+
}
28+
29+
describe('sys_setting.scope — value domain (#6036)', () => {
30+
it('declares exactly the cascade layers the resolver walks', () => {
31+
expect(optionValues(SysSetting, 'scope')).toEqual(['global', 'tenant', 'user']);
32+
});
33+
34+
it('does not declare a `runtime` layer', () => {
35+
// Spelled as its own case because THIS is the regression: the option was
36+
// inert, so re-adding it breaks nothing at runtime and would otherwise
37+
// sail through review a second time.
38+
expect(optionValues(SysSetting, 'scope')).not.toContain('runtime');
39+
});
40+
41+
it('matches SpecifierScopeSchema — the reference truth for the cascade', () => {
42+
// Set-compare: the spec enum is the authority on which layers exist, the
43+
// object definition is the storage mirror. Either side growing alone is
44+
// the #6036 defect, in whichever direction it happens next.
45+
expect([...optionValues(SysSetting, 'scope')].sort()).toEqual(
46+
[...SpecifierScopeSchema.options].sort(),
47+
);
48+
});
49+
50+
it('agrees with the audit trail object, which records the same layers', () => {
51+
expect(optionValues(SysSettingAudit, 'scope')).toEqual(optionValues(SysSetting, 'scope'));
52+
});
53+
54+
it('keeps `tenant` as the default, and the default is a declared option', () => {
55+
const f = (SysSetting as any).fields.scope;
56+
expect(f.defaultValue).toBe('tenant');
57+
expect(optionValues(SysSetting, 'scope')).toContain(f.defaultValue);
58+
});
59+
});

0 commit comments

Comments
 (0)