Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,10 @@ jobs:

- name: Type check example apps
run: pnpm --filter './examples/*' run typecheck

# Backward-compatibility gate: a frozen third-party-style consumer
# (#2035). Unlike the examples it must NOT be migrated to accommodate a
# spec change — a red typecheck here means the spec dropped/narrowed an
# export a published-spec third party already uses. See the package README.
- name: Type check downstream consumer contract
run: pnpm --filter @objectstack/downstream-contract run typecheck
45 changes: 45 additions & 0 deletions packages/downstream-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# @objectstack/downstream-contract

A **frozen, representative third-party consumer** of `@objectstack/spec`, used as a
backward-compatibility gate.

## Why this exists

Every other consumer the framework tests — the example apps, `@objectstack/dogfood` —
lives in this monorepo and **co-evolves with the spec in the same commit**. When a spec
change would break them, the same PR just fixes them. So they can never catch a change
that breaks a *real* third party (e.g. [hotcrm](https://github.com/objectstack-ai/hotcrm))
that is pinned to a **published** release and authors metadata independently.

This package closes that gap. It is authored exactly the way an external project does —
importing from the package entry points, mixing the builder (`ObjectSchema.create`),
the `defineX` factories, and **bare metadata literals** (the pattern third parties on
older releases used, see #2035) — and then validates that metadata against the spec.

## The contract (read before editing)

These fixtures are **frozen**. The gate is:

> A spec change that requires editing the fixtures to stay green is, by definition, a
> **breaking change** for third parties.

So if a change here turns red:

- **Do not** edit the fixtures to make it pass. That hides the very break this package
exists to surface.
- Treat it as a deliberate decision: either revert/adjust the spec change to stay
backward compatible, or — if the break is intended — bump `@objectstack/spec` to a new
**major** and document the migration, *then* update the fixtures in the same PR.

## What it checks

- `pnpm --filter @objectstack/downstream-contract typecheck` — the fixtures are typed
with real spec types (`ActionInput`, `ReportInput`, `PageInput`, …). A removed or
narrowed export fails here (the #2023 class of break).
- `pnpm --filter @objectstack/downstream-contract test` — runs each bare-literal fixture
through its schema's `.parse()` and assembles everything via `defineStack` (schema +
cross-reference validation — the heart of `objectstack validate`).

For the live counterpart, the release pipeline can additionally run hotcrm's own
`validate && typecheck && build` against the unreleased spec; this package is the fast,
deterministic, in-CI floor.
19 changes: 19 additions & 0 deletions packages/downstream-contract/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@objectstack/downstream-contract",
"version": "0.0.0",
"description": "Frozen third-party consumer fixture — a backward-compatibility gate for @objectstack/spec. Authored the way an external project on a published release authors metadata; if a spec change breaks it, that change is breaking (#2035).",
"license": "Apache-2.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@objectstack/spec": "workspace:*"
},
"devDependencies": {
"typescript": "^6.0.3",
"vitest": "^4.1.9"
}
}
23 changes: 23 additions & 0 deletions packages/downstream-contract/src/account.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

// Builder authoring path (object/field already have a good entry point).
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Account = ObjectSchema.create({
name: 'dc_account',
label: 'Account',
pluralLabel: 'Accounts',
icon: 'building',
description: 'Downstream-contract account object (builder authoring path).',
fields: {
name: Field.text({ label: 'Name', required: true, searchable: true, maxLength: 255 }),
stage: Field.select({
label: 'Stage',
options: [
{ label: 'Prospect', value: 'prospect', default: true },
{ label: 'Customer', value: 'customer' },
{ label: 'Churned', value: 'churned' },
],
}),
},
});
15 changes: 15 additions & 0 deletions packages/downstream-contract/src/account.view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

// Existing-factory authoring path.
import { defineView } from '@objectstack/spec/ui';

const data = { provider: 'object' as const, object: 'dc_account' };

export const AccountViews = defineView({
list: {
label: 'All Accounts',
type: 'grid',
data,
columns: [{ field: 'name' }, { field: 'stage' }],
},
});
17 changes: 17 additions & 0 deletions packages/downstream-contract/src/log-call.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

// FROZEN bare-literal fixture — authored WITHOUT the factory, the way a third
// party on an older spec did (#2035). No author-time `.parse()` runs here; only
// the contract test's schema parse validates it. DO NOT migrate this to the
// factory — that would hide the backward-compat break this file exists to catch.
import type { ActionInput } from '@objectstack/spec/ui';

export const LogCallAction: ActionInput = {
name: 'dc_log_call',
label: 'Log Call',
objectName: 'dc_account',
type: 'script',
target: 'logCall',
locations: ['record_header'],
successMessage: 'Call logged.',
};
13 changes: 13 additions & 0 deletions packages/downstream-contract/src/modern.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

// New-factory authoring path (#2035) — an updated consumer.
import { defineAction } from '@objectstack/spec/ui';

export const ArchiveAccountAction = defineAction({
name: 'dc_archive_account',
label: 'Archive',
objectName: 'dc_account',
type: 'script',
target: 'archiveAccount',
locations: ['record_header'],
});
14 changes: 14 additions & 0 deletions packages/downstream-contract/src/pipeline.report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

// FROZEN bare-literal fixture (see log-call.action.ts).
import type { ReportInput } from '@objectstack/spec/ui';

export const AccountsByStageReport: ReportInput = {
name: 'dc_accounts_by_stage',
label: 'Accounts by Stage',
description: 'Account count grouped by stage.',
type: 'summary',
dataset: 'dc_account_metrics',
rows: ['stage'],
values: ['account_count'],
};
24 changes: 24 additions & 0 deletions packages/downstream-contract/src/stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

// Assembles the consumer's metadata exactly as an objectstack.config.ts would,
// so the contract test exercises defineStack's schema parse + cross-reference
// validation (the metadata core of `objectstack validate`).
import { defineStack } from '@objectstack/spec';
import { Account } from './account.object.js';
import { AccountViews } from './account.view.js';
import { LogCallAction } from './log-call.action.js';
import { ArchiveAccountAction } from './modern.action.js';

export const ContractStack = defineStack({
manifest: {
id: 'com.objectstack.downstream_contract',
namespace: 'dc',
version: '1.0.0',
type: 'app',
name: 'Downstream Contract',
description: 'Frozen third-party consumer gating spec backward compatibility (#2035).',
},
objects: [Account],
views: [AccountViews],
actions: [LogCallAction, ArchiveAccountAction],
});
26 changes: 26 additions & 0 deletions packages/downstream-contract/src/welcome.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

// FROZEN bare-literal fixture (see log-call.action.ts). Pages were one of the
// 16 domains with no factory before #2035, so real third parties authored them
// exactly like this.
import type { PageInput } from '@objectstack/spec/ui';

export const WelcomePage: PageInput = {
name: 'dc_welcome',
label: 'Welcome',
type: 'home',
kind: 'full',
template: 'default',
regions: [
{
name: 'main',
width: 'full',
components: [
{
type: 'page:header',
properties: { title: 'Welcome', subtitle: 'Downstream contract page.' },
},
],
},
],
};
25 changes: 25 additions & 0 deletions packages/downstream-contract/test/contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect } from 'vitest';
import { ActionSchema, ReportSchema, PageSchema } from '@objectstack/spec/ui';
import { LogCallAction } from '../src/log-call.action.js';
import { AccountsByStageReport } from '../src/pipeline.report.js';
import { WelcomePage } from '../src/welcome.page.js';
import { ContractStack } from '../src/stack.js';

// FROZEN — see README. A failure means a spec change dropped or narrowed a
// property a published-spec third party already uses. That is breaking: adjust
// the spec or bump major; do NOT edit the fixtures to make this pass.
describe('downstream consumer contract (#2035)', () => {
it('bare-literal third-party metadata still parses against the current spec', () => {
expect(() => ActionSchema.parse(LogCallAction)).not.toThrow();
expect(() => ReportSchema.parse(AccountsByStageReport)).not.toThrow();
expect(() => PageSchema.parse(WelcomePage)).not.toThrow();
});

it('assembles into a stack — schema + cross-reference validation passes', () => {
expect(ContractStack).toBeDefined();
expect(ContractStack.objects).toHaveLength(1);
expect(ContractStack.actions).toHaveLength(2);
});
});
14 changes: 14 additions & 0 deletions packages/downstream-contract/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"noEmit": true,
"rootDir": "."
},
"include": ["src/**/*", "test/**/*"]
}
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading