Skip to content

Add metadata extension protocol for plugin extensibility#215

Closed
hotlong with Copilot wants to merge 4 commits into
mainfrom
copilot/design-metadata-definition-extensions
Closed

Add metadata extension protocol for plugin extensibility#215
hotlong with Copilot wants to merge 4 commits into
mainfrom
copilot/design-metadata-definition-extensions

Conversation

Copilot AI commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

Plugins need a standard way to extend core metadata (Objects, Fields) with custom properties without modifying base schemas.

Changes

Extension Protocol (src/system/extension.zod.ts)

  • ExtensionsMap: Namespaced key-value store (plugin_id.property_name)
  • ExtensionDefinition: Metadata registry for declaring extension schemas
  • Extension utilities: Type-safe helpers (get, set, has, remove)
  • Recursive type-safe schema supporting nested objects/arrays

Schema Integration

  • Added optional extensions field to FieldSchema and ObjectSchema
  • Fully backward compatible - existing schemas unchanged

AI Extensions (Reference Implementation)

  • Field extensions (9): Vector indexing, auto-summarization, sentiment analysis
  • Object extensions (15): RAG enablement, AI agents, predictive analytics, auto-classification

Usage

import { ObjectSchema, Extension } from '@objectstack/spec';

// Extend a field with AI capabilities
const field = {
  name: 'description',
  type: 'textarea',
  extensions: {
    'ai_assistant.vectorIndexed': true,
    'ai_assistant.embeddingModel': 'text-embedding-3-small',
    'ai_assistant.chunkSize': 512
  }
};

// Extend an object with RAG and agent support
const object = {
  name: 'customer_inquiry',
  fields: { /* ... */ },
  extensions: {
    'ai_assistant.enableRAG': true,
    'ai_assistant.contextFields': ['title', 'description'],
    'ai_assistant.agentEnabled': true,
    'ai_assistant.agentName': 'support_assistant'
  }
};

// Type-safe access
const ragEnabled = Extension.get(object.extensions, 'ai_assistant.enableRAG', false);

Documentation

  • User guide: content/docs/guides/metadata-extensions.mdx
  • Example: examples/ai-customer-service/ (complete AI-powered support system)
Original prompt

设计一套规范如何扩展标准的元数据定义,比如我需要开发一个AI插件,给对象或字段上扩展新的属性


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel

vercel Bot commented Jan 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Error Error Jan 26, 2026 5:27am

Request Review

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Design a specification for extending standard metadata definitions Add metadata extension protocol for plugin extensibility Jan 26, 2026
Copilot AI requested a review from hotlong January 26, 2026 05:29
@hotlong
hotlong marked this pull request as ready for review January 27, 2026 05:16
Copilot AI review requested due to automatic review settings January 27, 2026 05:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a standardized “extension” protocol so plugins can attach namespaced custom metadata to core definitions (e.g., Objects/Fields) without modifying base schemas.

Changes:

  • Introduces src/system/extension.zod.ts (extension value/map/definition/registry schemas + helper utilities) and accompanying tests.
  • Integrates optional extensions into FieldSchema and ObjectSchema, plus tests validating extended metadata.
  • Adds AI extension reference definitions (field/object) with schemas/tests, and updates generated JSON Schemas + docs + a new example.

Reviewed changes

Copilot reviewed 38 out of 38 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
packages/spec/src/system/index.ts Exposes the new extension protocol via the System barrel export.
packages/spec/src/system/extension.zod.ts Defines extension schemas and helper utilities for reading/writing extensions.
packages/spec/src/system/extension.test.ts Adds unit tests for extension schemas and helpers.
packages/spec/src/data/object.zod.ts Adds extensions support to Object metadata.
packages/spec/src/data/object.test.ts Tests Objects with/without extensions and complex extension values.
packages/spec/src/data/field.zod.ts Adds extensions support to Field metadata.
packages/spec/src/data/field.test.ts Tests Fields with/without extensions and complex extension values.
packages/spec/src/ai/object-extensions.zod.ts Provides AI-oriented Object extension definitions + schema.
packages/spec/src/ai/index.ts Exports AI extension definitions/schemas from the AI barrel.
packages/spec/src/ai/field-extensions.zod.ts Provides AI-oriented Field extension definitions + schema.
packages/spec/src/ai/extensions.test.ts Tests AI extension definitions and end-to-end schema integration.
packages/spec/json-schema/ui/FieldWidgetProps.json Updates generated UI schema to include extensions on fields.
packages/spec/json-schema/system/ExtensionsMap.json Adds generated JSON schema for ExtensionsMap.
packages/spec/json-schema/system/ExtensionValue.json Adds generated JSON schema for ExtensionValue.
packages/spec/json-schema/system/ExtensionRegistry.json Adds generated JSON schema for ExtensionRegistry.
packages/spec/json-schema/system/ExtensionDefinition.json Adds generated JSON schema for ExtensionDefinition.
packages/spec/json-schema/kernel/Manifest.json Updates generated Manifest schema to include extensions in embedded object/field definitions.
packages/spec/json-schema/hub/ComposerResponse.json Updates generated Hub schema to include extensions in embedded object/field definitions.
packages/spec/json-schema/data/Object.json Updates generated Object schema to include extensions.
packages/spec/json-schema/data/Field.json Updates generated Field schema to include extensions.
packages/spec/json-schema/ai/AIObjectExtension.json Adds generated schema for AI Object extension keys.
packages/spec/json-schema/ai/AIFieldExtension.json Adds generated schema for AI Field extension keys.
examples/ai-customer-service/index.ts Demonstrates adding extensions to an object/fields and validating with schemas.
examples/ai-customer-service/README.md Documents the AI customer service example usage.
content/docs/references/system/meta.json Registers the new System Extension reference section in docs navigation.
content/docs/references/system/extension/meta.json Adds docs metadata for the Extension reference section.
content/docs/references/system/extension/ExtensionsMap.mdx Adds reference doc stub for ExtensionsMap.
content/docs/references/system/extension/ExtensionValue.mdx Adds reference doc stub for ExtensionValue.
content/docs/references/system/extension/ExtensionRegistry.mdx Adds reference doc for ExtensionRegistry.
content/docs/references/system/extension/ExtensionDefinition.mdx Adds reference doc for ExtensionDefinition.
content/docs/references/data/object/Object.mdx Updates Object reference to mention the new extensions property.
content/docs/references/data/field/Field.mdx Updates Field reference to mention the new extensions property.
content/docs/references/ai/object-extensions/meta.json Adds docs metadata for AI Object extensions reference section.
content/docs/references/ai/object-extensions/AIObjectExtension.mdx Adds reference doc for AI Object extension keys.
content/docs/references/ai/meta.json Registers AI extension reference pages in AI docs navigation.
content/docs/references/ai/field-extensions/meta.json Adds docs metadata for AI Field extensions reference section.
content/docs/references/ai/field-extensions/AIFieldExtension.mdx Adds reference doc for AI Field extension keys.
content/docs/guides/metadata-extensions.mdx Adds a full guide describing how to use the extension protocol and helpers.

Comment on lines +254 to +256
has: (extensions: ExtensionsMap | undefined, key: string): boolean => {
return extensions ? key in extensions : false;
},

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extension.has uses the in operator, which returns true for inherited keys like toString even when the extension was never set. Use an own-property check instead (and consider using a null-prototype object for extension maps) to avoid false positives.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +76
export const ExtensionsMapSchema = z.record(
z.string().describe('Namespaced extension key (e.g., "plugin_id.property_name")'),
ExtensionValueSchema.describe('Extension value (string, number, boolean, object, or array)')
).optional().describe('Custom extension properties from plugins and modules');

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtensionsMapSchema is defined as .optional(), which makes the standalone ExtensionsMap type include undefined and produces an awkward top-level JSON Schema (e.g. anyOf with not: {}). Consider making ExtensionsMapSchema the non-optional record, and apply .optional() only at usage sites (Field/Object). Also consider validating keys with a namespaced-key regex (matching ExtensionDefinitionSchema) rather than plain z.string() to enforce the protocol.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +133
/**
* Default value if not specified
*/
default: z.any().optional().describe('Default value for this extension'),

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default is z.any(), which allows non-JSON values even though extension values are documented as JSON-compatible. Consider validating default with ExtensionValueSchema (and similarly restricting schema to an object shape if it’s meant to hold JSON Schema).

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +29
```typescript
import { ExtensionsMap } from '@objectstack/spec';

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples in this guide import ExtensionsMap (and later FieldSchema, ObjectSchema, Extension, etc.) directly from @objectstack/spec, but the package root currently only exports namespaces (see packages/spec/src/index.ts). Update the snippets to use the documented namespace import (import { System, Data, AI } from '@objectstack/spec') or direct subpath imports like @objectstack/spec/system / @objectstack/spec/data so they match the actual public API.

Copilot uses AI. Check for mistakes.

### Key Concepts

1. **Namespaced Keys**: All extensions use dot notation: `plugin_id.property_name`

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guide states extension keys use plugin_id.property_name, but elsewhere (and in the schemas) the convention is namespace.property with the property segment in camelCase. Align this description with the intended convention (e.g. plugin_id.propertyName) to avoid encouraging snake_case property segments.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +224 to +227
get: <T = any>(extensions: ExtensionsMap | undefined, key: string, defaultValue?: T): T | undefined => {
if (!extensions) return defaultValue;
return (extensions[key] as T) ?? defaultValue;
},

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extension.get reads extensions[key] directly, which can return inherited prototype properties (e.g. toString) instead of the provided default. Guard with an own-property check (e.g. Object.hasOwn / hasOwnProperty.call) before returning the stored value, otherwise fall back to defaultValue.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +105
key: z.string()
.regex(/^[a-z_][a-z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*$/)
.describe('Fully qualified extension key (e.g., "ai_assistant.vectorIndexed")'),

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key regex and nearby docs allow property_name and a property segment starting with uppercase, but the file also states the property should be camelCase. Tighten the regex/examples so the namespace is snake_case and the property segment is lowerCamelCase (and keep docs consistent with that convention).

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +137 to +139
appliesTo: z.array(z.enum([
'object', 'field', 'view', 'app', 'dashboard', 'report', 'action', 'workflow'
])).describe('Metadata types this extension can be applied to'),

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appliesTo is required but can currently be an empty array, which makes an ExtensionDefinition effectively meaningless. Consider using a non-empty array constraint (e.g. .nonempty()) so every definition declares at least one applicable target.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +192
extensions: z.record(
z.string(),
ExtensionDefinitionSchema
).describe('Registry of available extension definitions'),

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtensionRegistrySchema.extensions allows any string as the record key, so the map key can diverge from ExtensionDefinition.key. Consider constraining the record key with the same key regex and adding a refinement that enforces recordKey === definition.key (and optionally that definition.pluginId matches the namespace prefix).

Copilot uses AI. Check for mistakes.
* Demonstrates ObjectStack metadata extensions for AI capabilities
*/

import { ObjectSchema, Extension } from '@objectstack/spec';

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example imports ObjectSchema and Extension from @objectstack/spec, but the package root entrypoint currently only exports namespaces (e.g. Data, System). Use namespace imports (import { Data, System } from '@objectstack/spec') or subpath imports (@objectstack/spec/data, @objectstack/spec/system) so the example can actually run.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants