Skip to content

Commit 92647c1

Browse files
xuyushun441-sysos-zhuangclaude
authored
test(spec): downstream-consumer contract as a backward-compat gate (#2035) (#2089)
Adds `@objectstack/downstream-contract` — a frozen, representative third-party consumer of `@objectstack/spec`, and the CI gate that runs it. Why: every other spec consumer the framework tests (example apps, `@objectstack/dogfood`) lives in this repo and co-evolves with the spec in the same commit, so none can catch a change that breaks a *real* third party (e.g. hotcrm) pinned to a published release. This fixture is authored the way an external project does — builder + `defineX` factories + bare metadata literals (the pre-#2035 pattern) — and is FROZEN: a spec change that requires editing it to stay green is, by definition, breaking. Two signals, both wired into existing CI jobs (no new workflow): • typecheck — fixtures are typed with real spec types (`ActionInput`, `ReportInput`, `PageInput`, …); a removed/narrowed export fails (the #2023 class). Added to lint.yml's TypeScript Type Check job. • test — each bare-literal fixture is run through its schema's `.parse()`, and everything is assembled via `defineStack` (schema + cross-reference validation, the metadata core of `objectstack validate`). Rides `turbo run test` (Test Core). Verified locally: typecheck + test green; confirmed the schema parse has teeth (rejects a broken action `type`). README documents the freeze contract. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5f875fe commit 92647c1

13 files changed

Lines changed: 255 additions & 0 deletions

File tree

.github/workflows/lint.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,10 @@ jobs:
100100

101101
- name: Type check example apps
102102
run: pnpm --filter './examples/*' run typecheck
103+
104+
# Backward-compatibility gate: a frozen third-party-style consumer
105+
# (#2035). Unlike the examples it must NOT be migrated to accommodate a
106+
# spec change — a red typecheck here means the spec dropped/narrowed an
107+
# export a published-spec third party already uses. See the package README.
108+
- name: Type check downstream consumer contract
109+
run: pnpm --filter @objectstack/downstream-contract run typecheck
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# @objectstack/downstream-contract
2+
3+
A **frozen, representative third-party consumer** of `@objectstack/spec`, used as a
4+
backward-compatibility gate.
5+
6+
## Why this exists
7+
8+
Every other consumer the framework tests — the example apps, `@objectstack/dogfood`
9+
lives in this monorepo and **co-evolves with the spec in the same commit**. When a spec
10+
change would break them, the same PR just fixes them. So they can never catch a change
11+
that breaks a *real* third party (e.g. [hotcrm](https://github.com/objectstack-ai/hotcrm))
12+
that is pinned to a **published** release and authors metadata independently.
13+
14+
This package closes that gap. It is authored exactly the way an external project does —
15+
importing from the package entry points, mixing the builder (`ObjectSchema.create`),
16+
the `defineX` factories, and **bare metadata literals** (the pattern third parties on
17+
older releases used, see #2035) — and then validates that metadata against the spec.
18+
19+
## The contract (read before editing)
20+
21+
These fixtures are **frozen**. The gate is:
22+
23+
> A spec change that requires editing the fixtures to stay green is, by definition, a
24+
> **breaking change** for third parties.
25+
26+
So if a change here turns red:
27+
28+
- **Do not** edit the fixtures to make it pass. That hides the very break this package
29+
exists to surface.
30+
- Treat it as a deliberate decision: either revert/adjust the spec change to stay
31+
backward compatible, or — if the break is intended — bump `@objectstack/spec` to a new
32+
**major** and document the migration, *then* update the fixtures in the same PR.
33+
34+
## What it checks
35+
36+
- `pnpm --filter @objectstack/downstream-contract typecheck` — the fixtures are typed
37+
with real spec types (`ActionInput`, `ReportInput`, `PageInput`, …). A removed or
38+
narrowed export fails here (the #2023 class of break).
39+
- `pnpm --filter @objectstack/downstream-contract test` — runs each bare-literal fixture
40+
through its schema's `.parse()` and assembles everything via `defineStack` (schema +
41+
cross-reference validation — the heart of `objectstack validate`).
42+
43+
For the live counterpart, the release pipeline can additionally run hotcrm's own
44+
`validate && typecheck && build` against the unreleased spec; this package is the fast,
45+
deterministic, in-CI floor.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@objectstack/downstream-contract",
3+
"version": "0.0.0",
4+
"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).",
5+
"license": "Apache-2.0",
6+
"private": true,
7+
"type": "module",
8+
"scripts": {
9+
"typecheck": "tsc --noEmit",
10+
"test": "vitest run"
11+
},
12+
"dependencies": {
13+
"@objectstack/spec": "workspace:*"
14+
},
15+
"devDependencies": {
16+
"typescript": "^6.0.3",
17+
"vitest": "^4.1.9"
18+
}
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// Builder authoring path (object/field already have a good entry point).
4+
import { ObjectSchema, Field } from '@objectstack/spec/data';
5+
6+
export const Account = ObjectSchema.create({
7+
name: 'dc_account',
8+
label: 'Account',
9+
pluralLabel: 'Accounts',
10+
icon: 'building',
11+
description: 'Downstream-contract account object (builder authoring path).',
12+
fields: {
13+
name: Field.text({ label: 'Name', required: true, searchable: true, maxLength: 255 }),
14+
stage: Field.select({
15+
label: 'Stage',
16+
options: [
17+
{ label: 'Prospect', value: 'prospect', default: true },
18+
{ label: 'Customer', value: 'customer' },
19+
{ label: 'Churned', value: 'churned' },
20+
],
21+
}),
22+
},
23+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// Existing-factory authoring path.
4+
import { defineView } from '@objectstack/spec/ui';
5+
6+
const data = { provider: 'object' as const, object: 'dc_account' };
7+
8+
export const AccountViews = defineView({
9+
list: {
10+
label: 'All Accounts',
11+
type: 'grid',
12+
data,
13+
columns: [{ field: 'name' }, { field: 'stage' }],
14+
},
15+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// FROZEN bare-literal fixture — authored WITHOUT the factory, the way a third
4+
// party on an older spec did (#2035). No author-time `.parse()` runs here; only
5+
// the contract test's schema parse validates it. DO NOT migrate this to the
6+
// factory — that would hide the backward-compat break this file exists to catch.
7+
import type { ActionInput } from '@objectstack/spec/ui';
8+
9+
export const LogCallAction: ActionInput = {
10+
name: 'dc_log_call',
11+
label: 'Log Call',
12+
objectName: 'dc_account',
13+
type: 'script',
14+
target: 'logCall',
15+
locations: ['record_header'],
16+
successMessage: 'Call logged.',
17+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// New-factory authoring path (#2035) — an updated consumer.
4+
import { defineAction } from '@objectstack/spec/ui';
5+
6+
export const ArchiveAccountAction = defineAction({
7+
name: 'dc_archive_account',
8+
label: 'Archive',
9+
objectName: 'dc_account',
10+
type: 'script',
11+
target: 'archiveAccount',
12+
locations: ['record_header'],
13+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// FROZEN bare-literal fixture (see log-call.action.ts).
4+
import type { ReportInput } from '@objectstack/spec/ui';
5+
6+
export const AccountsByStageReport: ReportInput = {
7+
name: 'dc_accounts_by_stage',
8+
label: 'Accounts by Stage',
9+
description: 'Account count grouped by stage.',
10+
type: 'summary',
11+
dataset: 'dc_account_metrics',
12+
rows: ['stage'],
13+
values: ['account_count'],
14+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// Assembles the consumer's metadata exactly as an objectstack.config.ts would,
4+
// so the contract test exercises defineStack's schema parse + cross-reference
5+
// validation (the metadata core of `objectstack validate`).
6+
import { defineStack } from '@objectstack/spec';
7+
import { Account } from './account.object.js';
8+
import { AccountViews } from './account.view.js';
9+
import { LogCallAction } from './log-call.action.js';
10+
import { ArchiveAccountAction } from './modern.action.js';
11+
12+
export const ContractStack = defineStack({
13+
manifest: {
14+
id: 'com.objectstack.downstream_contract',
15+
namespace: 'dc',
16+
version: '1.0.0',
17+
type: 'app',
18+
name: 'Downstream Contract',
19+
description: 'Frozen third-party consumer gating spec backward compatibility (#2035).',
20+
},
21+
objects: [Account],
22+
views: [AccountViews],
23+
actions: [LogCallAction, ArchiveAccountAction],
24+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// FROZEN bare-literal fixture (see log-call.action.ts). Pages were one of the
4+
// 16 domains with no factory before #2035, so real third parties authored them
5+
// exactly like this.
6+
import type { PageInput } from '@objectstack/spec/ui';
7+
8+
export const WelcomePage: PageInput = {
9+
name: 'dc_welcome',
10+
label: 'Welcome',
11+
type: 'home',
12+
kind: 'full',
13+
template: 'default',
14+
regions: [
15+
{
16+
name: 'main',
17+
width: 'full',
18+
components: [
19+
{
20+
type: 'page:header',
21+
properties: { title: 'Welcome', subtitle: 'Downstream contract page.' },
22+
},
23+
],
24+
},
25+
],
26+
};

0 commit comments

Comments
 (0)