diff --git a/.changeset/retire-feed-discovery-surface.md b/.changeset/retire-feed-discovery-surface.md new file mode 100644 index 0000000000..bdad54d6fe --- /dev/null +++ b/.changeset/retire-feed-discovery-surface.md @@ -0,0 +1,25 @@ +--- +"@objectstack/spec": minor +"@objectstack/metadata-protocol": patch +"@objectstack/client": patch +--- + +**Breaking (discovery response shape): retire the residual feed capability surface (#3180, follow-up to #1959 / ADR-0052 §5).** + +The feed backend was retired long ago; #1959 removed the feed contracts + SDK. This +removes the last discovery/dispatcher references to it, and fixes a real bug where the +`comments` capability was permanently `false`. + +- `@objectstack/spec` — `WellKnownCapabilitiesSchema.feed` and `ApiRoutesSchema.feed` + (`routes.feed`) are **removed**, and the `/api/v1/feed` entry is dropped from + `DEFAULT_DISPATCHER_ROUTES`. FROM → TO: clients reading `discovery.capabilities.feed` + or `discovery.routes.feed` → use `discovery.capabilities.comments`; comments/activity + are served by the generic data API on `sys_comment` / `sys_activity` + (`/api/v1/data/sys_comment/…`). +- `@objectstack/metadata-protocol` — `getDiscovery()` no longer emits the always-`false` + `feed` service/capability. **Bug fix:** the `comments` capability previously keyed off + the deleted `'feed'` service (so it was permanently `false` after #1955); it now tracks + the presence of the `sys_comment` object (provided by the always-on audit slate), so + `declared === enforced`. +- `@objectstack/client` — the internal `feed: '/api/v1/feed'` route constant is removed + (it only existed to satisfy the now-removed `ApiRoutes.feed` type; no client code used it). diff --git a/content/docs/references/api/discovery.mdx b/content/docs/references/api/discovery.mdx index 90b7625edc..10035b7513 100644 --- a/content/docs/references/api/discovery.mdx +++ b/content/docs/references/api/discovery.mdx @@ -59,7 +59,6 @@ const result = ApiRoutes.parse(data); | **notifications** | `string` | optional | e.g. /api/v1/notifications | | **ai** | `string` | optional | e.g. /api/v1/ai | | **i18n** | `string` | optional | e.g. /api/v1/i18n | -| **feed** | `string` | optional | e.g. /api/v1/feed | --- @@ -155,8 +154,7 @@ Well-known capability flags for frontend intelligent adaptation | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **feed** | `boolean` | ✅ | Whether the backend supports Feed / Chatter API | -| **comments** | `boolean` | ✅ | Whether the backend supports comments (a subset of Feed) | +| **comments** | `boolean` | ✅ | Whether the backend supports record comments / chatter (the `sys_comment` object served via the data API) | | **automation** | `boolean` | ✅ | Whether the backend supports Automation CRUD (flows, triggers) | | **cron** | `boolean` | ✅ | Whether the backend supports cron scheduling | | **search** | `boolean` | ✅ | Whether the backend supports full-text search | diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts index 3c5579d425..dfbd9c95b4 100644 --- a/packages/client/src/client.test.ts +++ b/packages/client/src/client.test.ts @@ -863,7 +863,6 @@ describe('ObjectStackClient.automation', () => { it('should expose capabilities after connect', async () => { const caps = { - feed: true, comments: true, automation: false, cron: false, @@ -887,7 +886,7 @@ describe('ObjectStackClient.automation', () => { await client.connect(); expect(client.capabilities).toBeDefined(); - expect(client.capabilities!.feed).toBe(true); + expect(client.capabilities!.comments).toBe(true); expect(client.capabilities!.automation).toBe(false); expect(client.capabilities!.search).toBe(true); }); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 5af5fd223b..ddeaaa909c 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -3202,11 +3202,6 @@ export class ObjectStackClient { notifications: '/api/v1/notifications', ai: '/api/v1/ai', i18n: '/api/v1/i18n', - // NOTE: the `feed` route constant is retained only to satisfy the - // discovery `ApiRoutes` type (`routes.feed`), which is a separate - // follow-up cleanup. The feed SDK accessor + backend were retired - // (ADR-0052 §5); no client code consumes this route. - feed: '/api/v1/feed', graphql: '/graphql', }; diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index db2e190d86..51627d557d 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -1307,22 +1307,6 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { } } - // Add feed service status - if (registeredServices.has('feed')) { - services['feed'] = { - enabled: true, - status: 'available' as const, - route: '/api/v1/data', - provider: 'service-feed', - }; - } else { - services['feed'] = { - enabled: false, - status: 'unavailable' as const, - message: 'Install service-feed to enable', - }; - } - const routes: ApiRoutes = { data: '/api/v1/data', metadata: '/api/v1/meta', @@ -1333,8 +1317,11 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { // DiscoverySchema defines capabilities as Record // (hierarchical format). We also keep a flat WellKnownCapabilities for backward compat. const wellKnown: WellKnownCapabilities = { - feed: registeredServices.has('feed'), - comments: registeredServices.has('feed'), + // Comments/chatter are served by the `sys_comment` object via the generic + // data API (ADR-0052 §5) — not a dedicated service. The capability is true + // iff that object is loaded (the always-on audit slate provides it); this + // keeps declared === enforced (Prime Directive #10). #3180 + comments: !!this.engine.registry?.getObject?.('sys_comment'), automation: registeredServices.has('automation'), cron: registeredServices.has('job'), search: registeredServices.has('search'), diff --git a/packages/objectql/src/protocol-discovery.test.ts b/packages/objectql/src/protocol-discovery.test.ts index d32130e0f8..5d00ce6916 100644 --- a/packages/objectql/src/protocol-discovery.test.ts +++ b/packages/objectql/src/protocol-discovery.test.ts @@ -198,7 +198,7 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => // workflow is registered but doesn't map to a well-known capability directly expect(discovery.services.workflow.enabled).toBe(true); // All well-known capabilities should be disabled since workflow doesn't map to any - expect(discovery.capabilities!.feed).toEqual({ enabled: false }); + // (comments derives from the sys_comment object, which is not registered here). expect(discovery.capabilities!.comments).toEqual({ enabled: false }); expect(discovery.capabilities!.automation).toEqual({ enabled: false }); expect(discovery.capabilities!.cron).toEqual({ enabled: false }); @@ -212,7 +212,6 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => const discovery = await protocol.getDiscovery(); expect(discovery.capabilities).toBeDefined(); - expect(discovery.capabilities!.feed).toEqual({ enabled: false }); expect(discovery.capabilities!.comments).toEqual({ enabled: false }); expect(discovery.capabilities!.automation).toEqual({ enabled: false }); expect(discovery.capabilities!.cron).toEqual({ enabled: false }); @@ -223,7 +222,6 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => it('should dynamically set capabilities based on registered services', async () => { const mockServices = new Map(); - mockServices.set('feed', {}); mockServices.set('automation', {}); mockServices.set('search', {}); mockServices.set('file-storage', {}); @@ -231,13 +229,27 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); const discovery = await protocol.getDiscovery(); - expect(discovery.capabilities!.feed).toEqual({ enabled: true }); - expect(discovery.capabilities!.comments).toEqual({ enabled: true }); expect(discovery.capabilities!.automation).toEqual({ enabled: true }); expect(discovery.capabilities!.cron).toEqual({ enabled: false }); expect(discovery.capabilities!.search).toEqual({ enabled: true }); expect(discovery.capabilities!.export).toEqual({ enabled: true }); expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: true }); + // comments is independent of services — it tracks the sys_comment object (#3180). + expect(discovery.capabilities!.comments).toEqual({ enabled: false }); + }); + + it('should enable comments capability when the sys_comment object is registered (#3180)', async () => { + // comments/chatter are served by the sys_comment object via the data API + // (ADR-0052 §5), not a dedicated service — so the capability tracks that + // object's presence, keeping declared === enforced. + engine.registerObject( + { name: 'sys_comment', label: 'Comment', fields: { body: { type: 'text' } } } as any, + 'plugin-audit', + ); + protocol = new ObjectStackProtocolImplementation(engine, () => new Map()); + const discovery = await protocol.getDiscovery(); + + expect(discovery.capabilities!.comments).toEqual({ enabled: true }); }); it('should enable cron capability when job service is registered', async () => { diff --git a/packages/spec/src/api/discovery.test.ts b/packages/spec/src/api/discovery.test.ts index 6e18f92bea..9000fea503 100644 --- a/packages/spec/src/api/discovery.test.ts +++ b/packages/spec/src/api/discovery.test.ts @@ -37,13 +37,11 @@ describe('ApiRoutesSchema', () => { actions: '/api/v1/p', storage: '/api/v1/storage', graphql: '/api/v1/graphql', - feed: '/api/v1/feed', discovery: '/api/v1/discovery', }); expect(routes.data).toBe('/api/v1/data'); expect(routes.graphql).toBe('/api/v1/graphql'); - expect(routes.feed).toBe('/api/v1/feed'); expect(routes.discovery).toBe('/api/v1/discovery'); }); @@ -705,7 +703,6 @@ describe('DiscoverySchema (schemaDiscovery field)', () => { describe('WellKnownCapabilitiesSchema', () => { it('should accept all capabilities enabled', () => { const caps: WellKnownCapabilities = { - feed: true, comments: true, automation: true, cron: true, @@ -718,7 +715,6 @@ describe('WellKnownCapabilitiesSchema', () => { it('should accept all capabilities disabled', () => { const caps = WellKnownCapabilitiesSchema.parse({ - feed: false, comments: false, automation: false, cron: false, @@ -726,19 +722,18 @@ describe('WellKnownCapabilitiesSchema', () => { export: false, chunkedUpload: false, }); - expect(caps.feed).toBe(false); + expect(caps.comments).toBe(false); expect(caps.chunkedUpload).toBe(false); }); it('should reject missing required fields', () => { - expect(() => WellKnownCapabilitiesSchema.parse({ feed: true })).toThrow(); + expect(() => WellKnownCapabilitiesSchema.parse({ comments: true })).toThrow(); expect(() => WellKnownCapabilitiesSchema.parse({})).toThrow(); }); it('should reject non-boolean values', () => { expect(() => WellKnownCapabilitiesSchema.parse({ - feed: 'yes', - comments: true, + comments: 'yes', automation: true, cron: true, search: true, @@ -749,7 +744,6 @@ describe('WellKnownCapabilitiesSchema', () => { it('should have .describe() annotations on all fields', () => { const shape = WellKnownCapabilitiesSchema.shape; - expect(shape.feed.description).toBeDefined(); expect(shape.comments.description).toBeDefined(); expect(shape.automation.description).toBeDefined(); expect(shape.cron.description).toBeDefined(); diff --git a/packages/spec/src/api/discovery.zod.ts b/packages/spec/src/api/discovery.zod.ts index d102f4afb1..0a1889e4b8 100644 --- a/packages/spec/src/api/discovery.zod.ts +++ b/packages/spec/src/api/discovery.zod.ts @@ -201,9 +201,6 @@ export const ApiRoutesSchema = lazySchema(() => z.object({ /** Base URL for Internationalization */ i18n: z.string().optional().describe('e.g. /api/v1/i18n'), - - /** Base URL for Feed / Chatter API */ - feed: z.string().optional().describe('e.g. /api/v1/feed'), })); /** @@ -280,10 +277,8 @@ export const DiscoverySchema = lazySchema(() => z.object({ * Clients can use these to show/hide UI elements without probing individual endpoints. */ export const WellKnownCapabilitiesSchema = lazySchema(() => z.object({ - /** Whether the backend supports Feed / Chatter API */ - feed: z.boolean().describe('Whether the backend supports Feed / Chatter API'), - /** Whether the backend supports comments (a subset of Feed) */ - comments: z.boolean().describe('Whether the backend supports comments (a subset of Feed)'), + /** Whether the backend supports record comments / chatter (served by `sys_comment` via the data API) */ + comments: z.boolean().describe('Whether the backend supports record comments / chatter (the `sys_comment` object served via the data API)'), /** Whether the backend supports Automation CRUD (flows, triggers) */ automation: z.boolean().describe('Whether the backend supports Automation CRUD (flows, triggers)'), /** Whether the backend supports cron scheduling */ diff --git a/packages/spec/src/api/dispatcher.test.ts b/packages/spec/src/api/dispatcher.test.ts index 0987b4b70c..693f2cbefd 100644 --- a/packages/spec/src/api/dispatcher.test.ts +++ b/packages/spec/src/api/dispatcher.test.ts @@ -132,13 +132,12 @@ describe('DEFAULT_DISPATCHER_ROUTES', () => { expect(services).toContain('auth'); }); - it('should include storage and feed services', () => { + it('should include the storage service', () => { const services = DEFAULT_DISPATCHER_ROUTES.map(r => r.service); expect(services).toContain('file-storage'); - // feed route maps to the data service - const feedRoute = DEFAULT_DISPATCHER_ROUTES.find(r => r.prefix.includes('feed')); - expect(feedRoute).toBeDefined(); - expect(feedRoute!.service).toBe('data'); + // The feed route was retired (ADR-0052 §5 / #3180) — comments/activity are + // served by the generic data API on sys_comment / sys_activity. + expect(DEFAULT_DISPATCHER_ROUTES.find(r => r.prefix.includes('feed'))).toBeUndefined(); }); it('should include optional services', () => { diff --git a/packages/spec/src/api/dispatcher.zod.ts b/packages/spec/src/api/dispatcher.zod.ts index 5f77fd8d3f..e89072d159 100644 --- a/packages/spec/src/api/dispatcher.zod.ts +++ b/packages/spec/src/api/dispatcher.zod.ts @@ -157,7 +157,6 @@ export const DEFAULT_DISPATCHER_ROUTES: DispatcherRouteInput[] = [ { prefix: '/api/v1/analytics', service: 'analytics' }, { prefix: '/api/v1/automation', service: 'automation' }, { prefix: '/api/v1/storage', service: 'file-storage' }, - { prefix: '/api/v1/feed', service: 'data' }, { prefix: '/api/v1/i18n', service: 'i18n' }, { prefix: '/api/v1/notifications', service: 'notification' }, { prefix: '/api/v1/realtime', service: 'realtime' },