diff --git a/.changeset/environment-artifact-dual-source-c10.md b/.changeset/environment-artifact-dual-source-c10.md new file mode 100644 index 0000000000..8cf2eb25de --- /dev/null +++ b/.changeset/environment-artifact-dual-source-c10.md @@ -0,0 +1,62 @@ +--- +"@objectstack/spec": major +--- + +BREAKING(spec): `EnvironmentArtifact` 信封收敛为单一声明 —— `@objectstack/spec/system` 持有活的 wire 形状,`@objectstack/spec/cloud` re-export;从未实现的 v0 家族(`functions` / `manifest` / `payloadRef` 及其 8 个子 schema)退役 (#4740, #4535 C10) + +`EnvironmentArtifact` / `EnvironmentArtifactInput` / `EnvironmentArtifactSchema` 过去被两个入口导出,但**不是同一个声明**,拿到哪个形状只取决于 import 路径 —— #4411 陷阱: + +| 入口 | 声明位置(旧) | 形状 | 状态 | +|:--|:--|:--|:--| +| `@objectstack/spec/cloud` | `cloud/environment-artifact.zod.ts` | **活的 wire 形**:`checksum` 为 64 位 hex **字符串**,`metadata` = `ObjectStackDefinitionSchema` | **活**:全仓唯一 runtime Zod parse(`packages/metadata/src/plugin.ts` `_parseAndRegisterArtifact`)与 cloud 仓全部 type import 都用这侧 | +| `@objectstack/spec/system` | `system/environment-artifact.zod.ts` | 文档化「v0」:`checksum` 为 `{ algorithm, value }` **对象**、分类袋 `metadata`、内联 `functions[]`、必填 `manifest`、保留位 `payloadRef` | **declared-only**:三仓(objectstack / cloud / objectui)零代码消费者,从未有任何 producer 产出过该形状 | + +两侧互相解析不过(checksum 类型硬冲突)。按维护者裁决(#4740,路线 A′):**单一声明落 `./system`、取活的 wire 形状,`./cloud` 改为 re-export 同一声明** —— 两个入口现在解析到同一批符号,对活消费者零迁移。 + +## FROM → TO + +```ts +// 不变 —— cloud 入口的名字与形状都没变,只是声明搬家(re-export) +import { EnvironmentArtifactSchema, type EnvironmentArtifact } from '@objectstack/spec/cloud'; + +// 不变(形状变了!)—— system 入口同名导出仍在,但现在是活的 wire 形: +// checksum: string(64 hex) 而非 { algorithm, value } 对象 +// metadata: ObjectStackDefinition 而非分类袋 +import { EnvironmentArtifactSchema, type EnvironmentArtifact } from '@objectstack/spec/system'; +``` + +```ts +// FROM —— 编译期起将以 TS2305 失败(实测三仓零命中,预期无人受影响) +import { + EnvironmentArtifactChecksumSchema, EnvironmentArtifactFunctionSchema, + EnvironmentArtifactManifestSchema, EnvironmentArtifactMetadataSchema, + EnvironmentArtifactPayloadRefSchema, EnvironmentArtifactRequirementSchema, + EnvironmentArtifactHashAlgorithmEnum, EnvironmentArtifactFunctionLanguageEnum, +} from '@objectstack/spec/system'; +// TO —— 无替代物:v0 家族从未被任何 producer/consumer 实现。 +// 校验 checksum 用 Sha256DigestSchema(现同时从 ./system 与 ./cloud 导出); +// 校验 metadata 用 ObjectStackDefinitionSchema(根入口)。 +``` + +## ⚠️ wire 形状警示(#4666 盲区:key 级门禁不可见的类型变更) + +- `EnvironmentArtifact['checksum']`:对 `./system` 侧 import 者是 **`{ algorithm, value }` 对象 → 64 位 hex 字符串** 的类型变更(`./cloud` 侧一直是字符串,不变)。线上 wire 从来只有字符串形;pin 测试钉住「旧对象形 → 拒;hex 字符串 → 过」。 +- `EnvironmentArtifact['metadata']`:对 `./system` 侧 import 者从宽松分类袋(passthrough)变为受 `ObjectStackDefinitionSchema` 校验的编译产物。 +- 退役键 `functions` / `manifest` / `payloadRef` 走 `retiredKey()` tombstone:作者写入即得升级指引(tsc 处 `never`,parse 处 prescription),不是静默剥离。 + +## 退役论证(#4734 先例逐条评估,不照抄) + +**无 ADR-0087 D2/D3 conversion**:信封是**传输形状**,不是 authorable 元数据 —— 不作为 `sys_metadata` 行落库、不在 conversion walker 走的 stack 树上;且 `objectstack compile` 与控制面从未产出过这三个键(cloud 仓 `cloud-artifact-helpers.ts` 一直写 hex 字符串 checksum;函数代码走独立 runtimeModule,从不内联)。存量行**不可能携带**,conversion 写不出能跑到的 fixture(`converge-activation-event-schema` 同款论证)。tombstone prescription 即迁移文档。 + +## 定级理由(逐条自证) + +定 **major**:`./system` 的 16 个已发布导出名(8 schema const + 8 type)消失,外部 import 将以 TS2305 失败 —— 与 C14 / C16 同形,但**实测三仓 import 级零命中**。同时: + +- **零元数据迁移**:被删 9 个 def(`system/EnvironmentArtifact` 全家)均不从 `BUILTIN_METADATA_TYPE_SCHEMAS` 元数据根可达,#4650 门禁对 `authorable-surface.json` 被删 46 行的实跑判定是 7 组「def no longer emitted by this build」自证路径(输出见 PR);无 conversion / migration,`spec-changes.json` / upgrade-guide 零变化。 +- **runtime 零行为变化**:`packages/metadata/src/plugin.ts` 的 parse 目标形状就是收敛后的形状,一字未动。 +- cloud 仓 3 处 type import(`artifact-api-client.ts` / `file-artifact-api-client.ts` / `registry-reader.ts`,均 `spec/cloud`)名字与形状均不变。 +- JSON Schema 产物:`system/EnvironmentArtifact*` 9 个 def 停止发布(`json-schema.manifest.json` 同步删键,#2978 蓄意移除通道);新增 `system/Sha256Digest`(声明随家搬迁,`./cloud` 仍导出)。 + +## 基线 6 → 3 + +`dual-source-exports.baseline.json` 删掉 `EnvironmentArtifact` / `EnvironmentArtifactInput` / `EnvironmentArtifactSchema` 三行,其余行一字未动。 diff --git a/content/docs/concepts/north-star.mdx b/content/docs/concepts/north-star.mdx index 16569a9d0f..17c76b83cd 100644 --- a/content/docs/concepts/north-star.mdx +++ b/content/docs/concepts/north-star.mdx @@ -63,18 +63,18 @@ development live outside this repo. validated against `ObjectStackDefinitionSchema`). When published to Cloud it is wrapped in the immutable environment artifact envelope defined by `EnvironmentArtifactSchema` in -`packages/spec/src/system/environment-artifact.zod.ts`. +`packages/spec/src/system/environment-artifact.zod.ts` (the single +declaration; `@objectstack/spec/cloud` re-exports it). The artifact contains: - `schemaVersion` - `environmentId` - `commitId` -- `checksum` -- `metadata` -- `functions` -- `manifest` -- optional provenance and payload-reference fields +- `checksum` — a SHA-256 hex digest (string) of the canonical `metadata` + serialization +- `metadata` — the compiled `ObjectStackDefinition` itself +- optional `builtAt` / `builtWith` provenance The artifact is enough to describe what the runtime should load. It is not enough to deploy by itself; the host still supplies deployment config. diff --git a/content/docs/references/cloud/environment-artifact.mdx b/content/docs/references/cloud/environment-artifact.mdx index 0b84cfadba..1980ed85bd 100644 --- a/content/docs/references/cloud/environment-artifact.mdx +++ b/content/docs/references/cloud/environment-artifact.mdx @@ -5,19 +5,25 @@ description: Environment Artifact protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Environment Artifact Envelope (M1) +# Environment Artifact Envelope — re-export (#4740, #4535 C10) -Describes the response shape of `GET /api/v1/cloud/environments/:environmentId/artifact` +The envelope has exactly ONE declaration: -— the assembled artifact ObjectOS pulls from the control plane. +`../system/environment-artifact.zod` (maintainer route A′ on #4740 — -Distinct from the marketplace `PackageArtifactSchema` (a .tgz file listing). +`./system` holds the live wire shape, `./cloud` re-exports it). Importing -This envelope wraps the compiled `ObjectStackDefinitionSchema` produced by +from `@objectstack/spec/cloud` and `@objectstack/spec/system` yields the -`objectstack compile` together with control-plane assigned identity +SAME symbols, so the import path can never change the shape a consumer -(`commitId`, `checksum`). +gets (the #4411 dual-source trap, closed for this name). + +Do NOT re-declare the envelope here. A second declaration under this name + +is exactly what `check:dual-source-exports` and the symbol-identity pin in + +`../system/environment-artifact.test.ts` exist to reject. **Source:** `packages/spec/src/cloud/environment-artifact.zod.ts` diff --git a/content/docs/references/system/environment-artifact.mdx b/content/docs/references/system/environment-artifact.mdx index c93a17e18e..3866d1de13 100644 --- a/content/docs/references/system/environment-artifact.mdx +++ b/content/docs/references/system/environment-artifact.mdx @@ -5,210 +5,90 @@ description: Environment Artifact protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Environment Artifact Format Protocol (v0) +# Environment Artifact Envelope -Defines the immutable envelope produced by `objectstack compile` and consumed +THE single declaration of the environment artifact envelope (#4740, -by the ObjectStack runtime at boot. The artifact carries everything a runtime instance +#4535 C10 — maintainer route A′). `@objectstack/spec/cloud` re-exports -needs to hydrate an environment kernel without reading control-plane DB rows +this file; both entry points resolve to these exact symbols, so the -directly. +chosen entry point can never change the shape a consumer gets (the -## Boundary - -- **Artifact (this schema):** environment metadata + inlined function code + - -plugin/driver requirements. Immutable, content-addressable via `commitId` - -and `checksum`. - -- **Deployment Config (NOT in this schema):** business DB coordinates, - -credentials, environment identity, secrets. Injected at runtime. +#4411 dual-source trap, closed for this name). -See `content/docs/concepts/north-star.mdx` §6.3 for the runtime-inputs +Describes the response shape of -boundary, and `ROADMAP.md` M1 for the milestone definition. +`GET /api/v1/cloud/environments/:environmentId/artifact` — the assembled -## Storage / Distribution - -v0 stores the full payload inline. Future revisions may swap `metadata` / - -`functions` for a `payloadRef` that points at out-of-band storage (S3, - -signed URL). The envelope shape preserves room for that indirection without - -a breaking schema bump. - - -**Source:** `packages/spec/src/system/environment-artifact.zod.ts` - +artifact ObjectOS pulls from the control plane, and the shape the runtime -## TypeScript Usage - -```typescript -import { EnvironmentArtifactSchema, EnvironmentArtifactChecksumSchema, EnvironmentArtifactFunctionSchema, EnvironmentArtifactFunctionLanguageEnum, EnvironmentArtifactHashAlgorithmEnum, EnvironmentArtifactManifestSchema, EnvironmentArtifactMetadataSchema, EnvironmentArtifactPayloadRefSchema, EnvironmentArtifactRequirementSchema } from '@objectstack/spec/system'; -import type { EnvironmentArtifact, EnvironmentArtifactChecksum, EnvironmentArtifactFunction, EnvironmentArtifactManifest, EnvironmentArtifactMetadata, EnvironmentArtifactPayloadRef, EnvironmentArtifactRequirement } from '@objectstack/spec/system'; - -// Validate data -const result = EnvironmentArtifactSchema.parse(data); -``` - ---- - -## EnvironmentArtifact - -ObjectStack Environment Artifact envelope (v0) - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **schemaVersion** | `'0.1'` | ✅ | Environment artifact envelope schema version | -| **environmentId** | `string` | ✅ | Environment identifier (control-plane scoped) | -| **commitId** | `string` | ✅ | Content-addressable revision id | -| **checksum** | `{ algorithm: Enum<'sha256' \| 'sha384' \| 'sha512'>; value: string }` | ✅ | Artifact integrity checksum | -| **builtAt** | `string` | optional | ISO-8601 timestamp of when the artifact was built | -| **builtWith** | `string` | optional | Build tool identifier | -| **metadata** | `Record` | ✅ | Compiled environment metadata grouped by category | -| **functions** | `{ name: string; language: Enum<'javascript' \| 'typescript'>; code: string; source?: object; … }[]` | ✅ | Inlined function code packaged with the artifact | -| **manifest** | `{ plugins?: { id: string; version?: string }[]; drivers?: { id: string; version?: string }[]; engine?: object }` | ✅ | Plugin/driver requirements baked into the artifact | -| **payloadRef** | `{ url: string; expiresAt?: string; checksum: object }` | optional | Out-of-band payload reference (reserved for future use) | - - ---- +metadata loader parses at boot (`packages/metadata/src/plugin.ts`, -## EnvironmentArtifactChecksum +`_parseAndRegisterArtifact` — the one runtime Zod parse of this envelope). -Artifact integrity checksum +Distinct from the marketplace `PackageArtifactSchema` (a .tgz file -### Properties +listing). This envelope wraps the compiled `ObjectStackDefinitionSchema` -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **algorithm** | `Enum<'sha256' \| 'sha384' \| 'sha512'>` | ✅ | Hash algorithm used for the artifact checksum | -| **value** | `string` | ✅ | Hex-encoded digest of the artifact body | +produced by `objectstack compile` together with control-plane assigned +identity (`commitId`, `checksum`). ---- - -## EnvironmentArtifactFunction - -A single inlined function - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Function machine name (snake_case) | -| **language** | `Enum<'javascript' \| 'typescript'>` | ✅ | Source language of the function code | -| **code** | `string` | ✅ | Inlined function source | -| **source** | `{ path?: string; exportName?: string }` | optional | Source-map metadata for the function | -| **hash** | `string` | optional | Hex SHA-256 of the inlined code | - - ---- - -## EnvironmentArtifactFunctionLanguageEnum - -Source language of the function code - -### Allowed Values - -* `javascript` -* `typescript` +## Boundary +- **Artifact (this schema):** compiled environment metadata plus ---- +provenance. Immutable, content-addressable via `commitId` and -## EnvironmentArtifactHashAlgorithmEnum +`checksum`. -Hash algorithm used for the artifact checksum +- **Deployment Config (NOT in this schema):** business DB coordinates, -### Allowed Values +credentials, environment identity, secrets. Injected at runtime. -* `sha256` -* `sha384` -* `sha512` +See `content/docs/concepts/north-star.mdx` §6.3 for the +runtime-inputs boundary. ---- +## History (#4740) -## EnvironmentArtifactManifest +This file previously documented a richer "v0" envelope — a -Plugin/driver requirements baked into the artifact +`\{ algorithm, value \}` checksum object, a category-bag `metadata`, inlined -### Properties +`functions[]`, a required plugin/driver `manifest`, and a reserved -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **plugins** | `{ id: string; version?: string }[]` | optional | | -| **drivers** | `{ id: string; version?: string }[]` | optional | | -| **engine** | `{ objectstack: string }` | optional | | +`payloadRef` indirection — that NO producer or consumer ever implemented: +`objectstack compile` ships function code as standalone runtime modules ---- +referenced from the compiled definition, and the control plane has always -## EnvironmentArtifactMetadata - -Compiled environment metadata grouped by category - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **objects** | `any[]` | optional | | -| **fields** | `any[]` | optional | | -| **views** | `any[]` | optional | | -| **apps** | `any[]` | optional | | -| **pages** | `any[]` | optional | | -| **dashboards** | `any[]` | optional | | -| **reports** | `any[]` | optional | | -| **flows** | `any[]` | optional | | -| **workflows** | `any[]` | optional | | -| **triggers** | `any[]` | optional | | -| **agents** | `any[]` | optional | | -| **tools** | `any[]` | optional | | -| **skills** | `any[]` | optional | | -| **permissions** | `any[]` | optional | | -| **permissionSets** | `any[]` | optional | | -| **positions** | `any[]` | optional | | -| **translations** | `any[]` | optional | | -| **datasources** | `any[]` | optional | | -| **datasets** | `any[]` | optional | | -| **actions** | `any[]` | optional | | -| **apis** | `any[]` | optional | | +served the wire shape below (string SHA-256 checksum, `metadata` = the +compiled definition). The declaration converged to the live wire shape; ---- +the never-implemented keys are tombstoned below (ADR-0049 -## EnvironmentArtifactPayloadRef +enforce-or-remove: declared = enforced, or absent). -Out-of-band payload reference (reserved for future use) + +**Source:** `packages/spec/src/system/environment-artifact.zod.ts` + -### Properties +## TypeScript Usage -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **url** | `string` | ✅ | Signed URL pointing at the artifact payload | -| **expiresAt** | `string` | optional | ISO-8601 expiry timestamp | -| **checksum** | `{ algorithm: Enum<'sha256' \| 'sha384' \| 'sha512'>; value: string }` | ✅ | Checksum of the referenced payload | +```typescript +import { Sha256DigestSchema } from '@objectstack/spec/system'; +import type { Sha256Digest } from '@objectstack/spec/system'; +// Validate data +const result = Sha256DigestSchema.parse(data); +``` --- -## EnvironmentArtifactRequirement - -A plugin or driver dependency declaration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Plugin/driver package id | -| **version** | `string` | optional | SemVer range required by the environment | - --- diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 56fc9af45f..d8afcfc61c 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -913,23 +913,7 @@ "EncryptionConfigInput (type)", "EncryptionConfigSchema (const)", "EnvironmentArtifact (type)", - "EnvironmentArtifactChecksum (type)", - "EnvironmentArtifactChecksumSchema (const)", - "EnvironmentArtifactFunction (type)", - "EnvironmentArtifactFunctionLanguage (type)", - "EnvironmentArtifactFunctionLanguageEnum (const)", - "EnvironmentArtifactFunctionSchema (const)", - "EnvironmentArtifactHashAlgorithm (type)", - "EnvironmentArtifactHashAlgorithmEnum (const)", "EnvironmentArtifactInput (type)", - "EnvironmentArtifactManifest (type)", - "EnvironmentArtifactManifestSchema (const)", - "EnvironmentArtifactMetadata (type)", - "EnvironmentArtifactMetadataSchema (const)", - "EnvironmentArtifactPayloadRef (type)", - "EnvironmentArtifactPayloadRefSchema (const)", - "EnvironmentArtifactRequirement (type)", - "EnvironmentArtifactRequirementSchema (const)", "EnvironmentArtifactSchema (const)", "ExecuteSqlOperation (const)", "ExtendedLogLevel (type)", @@ -1253,6 +1237,8 @@ "SettingsNamespacePayload (type)", "SettingsNamespacePayloadSchema (const)", "SettingsUnsubscribe (type)", + "Sha256Digest (type)", + "Sha256DigestSchema (const)", "SocialProviderConfig (type)", "SocialProviderConfigSchema (const)", "Span (type)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 11a3986f87..663c355f37 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -5944,52 +5944,6 @@ "system/EncryptionConfig:keyManagement", "system/EncryptionConfig:scope", "system/EncryptionConfig:searchableEncryption", - "system/EnvironmentArtifact:builtAt", - "system/EnvironmentArtifact:builtWith", - "system/EnvironmentArtifact:checksum", - "system/EnvironmentArtifact:commitId", - "system/EnvironmentArtifact:environmentId", - "system/EnvironmentArtifact:functions", - "system/EnvironmentArtifact:manifest", - "system/EnvironmentArtifact:metadata", - "system/EnvironmentArtifact:payloadRef", - "system/EnvironmentArtifact:schemaVersion", - "system/EnvironmentArtifactChecksum:algorithm", - "system/EnvironmentArtifactChecksum:value", - "system/EnvironmentArtifactFunction:code", - "system/EnvironmentArtifactFunction:hash", - "system/EnvironmentArtifactFunction:language", - "system/EnvironmentArtifactFunction:name", - "system/EnvironmentArtifactFunction:source", - "system/EnvironmentArtifactManifest:drivers", - "system/EnvironmentArtifactManifest:engine", - "system/EnvironmentArtifactManifest:plugins", - "system/EnvironmentArtifactMetadata:actions", - "system/EnvironmentArtifactMetadata:agents", - "system/EnvironmentArtifactMetadata:apis", - "system/EnvironmentArtifactMetadata:apps", - "system/EnvironmentArtifactMetadata:dashboards", - "system/EnvironmentArtifactMetadata:datasets", - "system/EnvironmentArtifactMetadata:datasources", - "system/EnvironmentArtifactMetadata:fields", - "system/EnvironmentArtifactMetadata:flows", - "system/EnvironmentArtifactMetadata:objects", - "system/EnvironmentArtifactMetadata:pages", - "system/EnvironmentArtifactMetadata:permissionSets", - "system/EnvironmentArtifactMetadata:permissions", - "system/EnvironmentArtifactMetadata:positions", - "system/EnvironmentArtifactMetadata:reports", - "system/EnvironmentArtifactMetadata:skills", - "system/EnvironmentArtifactMetadata:tools", - "system/EnvironmentArtifactMetadata:translations", - "system/EnvironmentArtifactMetadata:triggers", - "system/EnvironmentArtifactMetadata:views", - "system/EnvironmentArtifactMetadata:workflows", - "system/EnvironmentArtifactPayloadRef:checksum", - "system/EnvironmentArtifactPayloadRef:expiresAt", - "system/EnvironmentArtifactPayloadRef:url", - "system/EnvironmentArtifactRequirement:id", - "system/EnvironmentArtifactRequirement:version", "system/ExecuteSqlOperation:description", "system/ExecuteSqlOperation:sql", "system/ExecuteSqlOperation:type", diff --git a/packages/spec/docs-import-surface.baseline.json b/packages/spec/docs-import-surface.baseline.json index ce1ed1a8a7..92c096dc84 100644 --- a/packages/spec/docs-import-surface.baseline.json +++ b/packages/spec/docs-import-surface.baseline.json @@ -1,145 +1,143 @@ { - "_comment": "Accepted gaps between the reference docs' import examples and the real export surface of @objectstack/spec (#4570): a documented JSON Schema whose entry point exports no matching type alias (\"no type export\") or no matching schema const (\"no schema const export\"). The name is omitted from the generated import line — the docs never advertise an import that cannot compile — and listed here so the omission is countable instead of silent. Shrink-only ratchet: a NEW gap fails check:docs (fix it by adding `export type X = z.infer;` next to the schema and regenerating api-surface.json, or by retiring the schema together with its alias), and a stale entry fails until its line is deleted. Growing this list is a maintainer decision that shows up as this file in the diff. Regenerate with: tsx scripts/build-docs.ts --update-import-baseline (after gen:schema).", + "_comment": "Accepted gaps between the reference docs' import examples and the real export surface of @objectstack/spec (#4570): a documented JSON Schema whose entry point exports no matching type alias (\"no type export\") or no matching schema const (\"no schema const export\"). The name is omitted from the generated import line \u2014 the docs never advertise an import that cannot compile \u2014 and listed here so the omission is countable instead of silent. Shrink-only ratchet: a NEW gap fails check:docs (fix it by adding `export type X = z.infer;` next to the schema and regenerating api-surface.json, or by retiring the schema together with its alias), and a stale entry fails until its line is deleted. Growing this list is a maintainer decision that shows up as this file in the diff. Regenerate with: tsx scripts/build-docs.ts --update-import-baseline (after gen:schema).", "entries": [ - "ai/AIModelConfig — no type export", - "ai/CodeContent — no type export", - "ai/FileContent — no type export", - "ai/ImageContent — no type export", - "ai/TextContent — no type export", - "api/AnalyticsResultResponse — no type export", - "api/ApiMapping — no type export", - "api/Discovery — no type export", - "api/GetAnalyticsMetaRequest — no type export", - "api/HttpFindQueryParams — no type export", - "api/MetadataBulkUnregisterRequest — no type export", - "api/MetadataExportRequest — no type export", - "api/MetadataImportRequest — no type export", - "api/MetadataOverlaySaveRequest — no type export", - "api/MetadataQueryRequest — no type export", - "api/MetadataValidateRequest — no type export", - "api/SubscriptionEvent — no type export", - "automation/ApproverType — no type export", - "automation/FlowNodeAction — no type export", - "automation/FlowVariable — no type export", - "automation/GuardRef — no type export", - "automation/StateMachine — no type export", - "automation/StateNode — no type export", - "data/AddressValue — no type export", - "data/AggregationFunction — no type export", - "data/AggregationMetricType — no type export", - "data/CalendarDateValue — no type export", - "data/ClockTimeValue — no type export", - "data/ContextTokenPlaceholder — no type export", - "data/DataEngineAggregateRequest — no type export", - "data/DataEngineCountRequest — no type export", - "data/DataEngineDeleteRequest — no type export", - "data/DataEngineExecuteRequest — no type export", - "data/DataEngineFindOneRequest — no type export", - "data/DataEngineFindRequest — no type export", - "data/DataEngineInsertRequest — no type export", - "data/DataEngineUpdateRequest — no type export", - "data/DataEngineVectorFindRequest — no type export", - "data/DateGranularity — no type export", - "data/DateMacroPlaceholder — no type export", - "data/DimensionType — no type export", - "data/DriverSslToggle — no type export", - "data/DriverType — no type export", - "data/EqualityOperator — no type export", - "data/FileLikeValue — no type export", - "data/FileReferenceIdValue — no type export", - "data/FileValue — no type export", - "data/HookEvent — no type export", - "data/Index — no type export", - "data/InstantValue — no type export", - "data/LocationValue — no type export", - "data/Object — no type export", - "data/ObjectOwnershipEnum — no type export", - "data/Query — no type export", - "data/ReferenceIdValue — no type export", - "data/SeedMode — no type export", - "data/SetOperator — no type export", - "data/SpecialOperator — no type export", - "data/StringOperator — no type export", - "data/TimeUpdateInterval — no type export", - "data/TransformType — no type export", - "integration/ConnectorAction — no type export", - "integration/ConnectorInstanceAPIKeyAuth — no type export", - "integration/ConnectorInstanceBasicAuth — no type export", - "integration/ConnectorInstanceBearerAuth — no type export", - "integration/ConnectorInstanceNoAuth — no type export", - "integration/ConnectorTrigger — no type export", - "kernel/DependencyStatusEnum — no type export", - "kernel/EventMetadata — no type export", - "kernel/Manifest — no type export", - "kernel/MetadataCategoryEnum — no type export", - "kernel/PackageStatusEnum — no type export", - "kernel/Plugin — no type export", - "qa/TestActionType — no type export", - "qa/TestAssertionType — no type export", - "qa/TestContext — no type export", - "security/OWDModel — no type export", - "security/ShareRecipientType — no type export", - "security/SharingLevel — no type export", - "security/SharingRuleType — no type export", - "shared/IsolationLevelEnum — no type export", - "shared/MutationEventEnum — no type export", - "shared/SortDirectionEnum — no type export", - "studio/ActionLocation — no type export", - "studio/PanelLocation — no type export", - "system/AddFieldOperation — no type export", - "system/CreateObjectOperation — no type export", - "system/DeleteObjectOperation — no type export", - "system/DeployStatusEnum — no type export", - "system/EnvironmentArtifactFunctionLanguageEnum — no type export", - "system/EnvironmentArtifactHashAlgorithmEnum — no type export", - "system/ExecuteSqlOperation — no type export", - "system/KernelServiceMap — no type export", - "system/Locale — no type export", - "system/MetadataState — no type export", - "system/MigrationDependency — no type export", - "system/ModifyFieldOperation — no type export", - "system/RemoveFieldOperation — no type export", - "system/RenameObjectOperation — no type export", - "system/ServiceConfig — no type export", - "system/ServiceCriticality — no type export", - "system/ServiceStatus — no type export", - "ui/AIChatWindowProps — no type export", - "ui/ActionType — no type export", - "ui/BreakpointColumnMap — no type export", - "ui/BreakpointOrderMap — no type export", - "ui/CalendarConfig — no type export", - "ui/DerivedMeasureOp — no type export", - "ui/ElementButtonProps — no type export", - "ui/ElementFilterProps — no type export", - "ui/ElementFormProps — no type export", - "ui/ElementImageProps — no type export", - "ui/ElementMetadataViewerProps — no type export", - "ui/ElementNumberProps — no type export", - "ui/ElementRecordPickerProps — no type export", - "ui/ElementTextInputProps — no type export", - "ui/ElementTextProps — no type export", - "ui/GanttConfig — no type export", - "ui/GanttQuickFilter — no type export", - "ui/GroupingField — no type export", - "ui/HttpMethod — no type export", - "ui/KanbanConfig — no type export", - "ui/NavigationMode — no type export", - "ui/ObjectListView — no type export", - "ui/ObjectUserFilters — no type export", - "ui/PageAccordionProps — no type export", - "ui/PageCardProps — no type export", - "ui/PageComponentType — no type export", - "ui/PageHeaderProps — no type export", - "ui/PageTabsProps — no type export", - "ui/RecordActivityProps — no type export", - "ui/RecordChatterProps — no type export", - "ui/RecordDetailsProps — no type export", - "ui/RecordHighlightsField — no type export", - "ui/RecordHighlightsProps — no type export", - "ui/RecordPathProps — no type export", - "ui/RecordRelatedListProps — no type export", - "ui/ReportType — no type export", - "ui/TreeConfig — no type export", - "ui/ViewItemName — no type export" + "ai/AIModelConfig \u2014 no type export", + "ai/CodeContent \u2014 no type export", + "ai/FileContent \u2014 no type export", + "ai/ImageContent \u2014 no type export", + "ai/TextContent \u2014 no type export", + "api/AnalyticsResultResponse \u2014 no type export", + "api/ApiMapping \u2014 no type export", + "api/Discovery \u2014 no type export", + "api/GetAnalyticsMetaRequest \u2014 no type export", + "api/HttpFindQueryParams \u2014 no type export", + "api/MetadataBulkUnregisterRequest \u2014 no type export", + "api/MetadataExportRequest \u2014 no type export", + "api/MetadataImportRequest \u2014 no type export", + "api/MetadataOverlaySaveRequest \u2014 no type export", + "api/MetadataQueryRequest \u2014 no type export", + "api/MetadataValidateRequest \u2014 no type export", + "api/SubscriptionEvent \u2014 no type export", + "automation/ApproverType \u2014 no type export", + "automation/FlowNodeAction \u2014 no type export", + "automation/FlowVariable \u2014 no type export", + "automation/GuardRef \u2014 no type export", + "automation/StateMachine \u2014 no type export", + "automation/StateNode \u2014 no type export", + "data/AddressValue \u2014 no type export", + "data/AggregationFunction \u2014 no type export", + "data/AggregationMetricType \u2014 no type export", + "data/CalendarDateValue \u2014 no type export", + "data/ClockTimeValue \u2014 no type export", + "data/ContextTokenPlaceholder \u2014 no type export", + "data/DataEngineAggregateRequest \u2014 no type export", + "data/DataEngineCountRequest \u2014 no type export", + "data/DataEngineDeleteRequest \u2014 no type export", + "data/DataEngineExecuteRequest \u2014 no type export", + "data/DataEngineFindOneRequest \u2014 no type export", + "data/DataEngineFindRequest \u2014 no type export", + "data/DataEngineInsertRequest \u2014 no type export", + "data/DataEngineUpdateRequest \u2014 no type export", + "data/DataEngineVectorFindRequest \u2014 no type export", + "data/DateGranularity \u2014 no type export", + "data/DateMacroPlaceholder \u2014 no type export", + "data/DimensionType \u2014 no type export", + "data/DriverSslToggle \u2014 no type export", + "data/DriverType \u2014 no type export", + "data/EqualityOperator \u2014 no type export", + "data/FileLikeValue \u2014 no type export", + "data/FileReferenceIdValue \u2014 no type export", + "data/FileValue \u2014 no type export", + "data/HookEvent \u2014 no type export", + "data/Index \u2014 no type export", + "data/InstantValue \u2014 no type export", + "data/LocationValue \u2014 no type export", + "data/Object \u2014 no type export", + "data/ObjectOwnershipEnum \u2014 no type export", + "data/Query \u2014 no type export", + "data/ReferenceIdValue \u2014 no type export", + "data/SeedMode \u2014 no type export", + "data/SetOperator \u2014 no type export", + "data/SpecialOperator \u2014 no type export", + "data/StringOperator \u2014 no type export", + "data/TimeUpdateInterval \u2014 no type export", + "data/TransformType \u2014 no type export", + "integration/ConnectorAction \u2014 no type export", + "integration/ConnectorInstanceAPIKeyAuth \u2014 no type export", + "integration/ConnectorInstanceBasicAuth \u2014 no type export", + "integration/ConnectorInstanceBearerAuth \u2014 no type export", + "integration/ConnectorInstanceNoAuth \u2014 no type export", + "integration/ConnectorTrigger \u2014 no type export", + "kernel/DependencyStatusEnum \u2014 no type export", + "kernel/EventMetadata \u2014 no type export", + "kernel/Manifest \u2014 no type export", + "kernel/MetadataCategoryEnum \u2014 no type export", + "kernel/PackageStatusEnum \u2014 no type export", + "kernel/Plugin \u2014 no type export", + "qa/TestActionType \u2014 no type export", + "qa/TestAssertionType \u2014 no type export", + "qa/TestContext \u2014 no type export", + "security/OWDModel \u2014 no type export", + "security/ShareRecipientType \u2014 no type export", + "security/SharingLevel \u2014 no type export", + "security/SharingRuleType \u2014 no type export", + "shared/IsolationLevelEnum \u2014 no type export", + "shared/MutationEventEnum \u2014 no type export", + "shared/SortDirectionEnum \u2014 no type export", + "studio/ActionLocation \u2014 no type export", + "studio/PanelLocation \u2014 no type export", + "system/AddFieldOperation \u2014 no type export", + "system/CreateObjectOperation \u2014 no type export", + "system/DeleteObjectOperation \u2014 no type export", + "system/DeployStatusEnum \u2014 no type export", + "system/ExecuteSqlOperation \u2014 no type export", + "system/KernelServiceMap \u2014 no type export", + "system/Locale \u2014 no type export", + "system/MetadataState \u2014 no type export", + "system/MigrationDependency \u2014 no type export", + "system/ModifyFieldOperation \u2014 no type export", + "system/RemoveFieldOperation \u2014 no type export", + "system/RenameObjectOperation \u2014 no type export", + "system/ServiceConfig \u2014 no type export", + "system/ServiceCriticality \u2014 no type export", + "system/ServiceStatus \u2014 no type export", + "ui/AIChatWindowProps \u2014 no type export", + "ui/ActionType \u2014 no type export", + "ui/BreakpointColumnMap \u2014 no type export", + "ui/BreakpointOrderMap \u2014 no type export", + "ui/CalendarConfig \u2014 no type export", + "ui/DerivedMeasureOp \u2014 no type export", + "ui/ElementButtonProps \u2014 no type export", + "ui/ElementFilterProps \u2014 no type export", + "ui/ElementFormProps \u2014 no type export", + "ui/ElementImageProps \u2014 no type export", + "ui/ElementMetadataViewerProps \u2014 no type export", + "ui/ElementNumberProps \u2014 no type export", + "ui/ElementRecordPickerProps \u2014 no type export", + "ui/ElementTextInputProps \u2014 no type export", + "ui/ElementTextProps \u2014 no type export", + "ui/GanttConfig \u2014 no type export", + "ui/GanttQuickFilter \u2014 no type export", + "ui/GroupingField \u2014 no type export", + "ui/HttpMethod \u2014 no type export", + "ui/KanbanConfig \u2014 no type export", + "ui/NavigationMode \u2014 no type export", + "ui/ObjectListView \u2014 no type export", + "ui/ObjectUserFilters \u2014 no type export", + "ui/PageAccordionProps \u2014 no type export", + "ui/PageCardProps \u2014 no type export", + "ui/PageComponentType \u2014 no type export", + "ui/PageHeaderProps \u2014 no type export", + "ui/PageTabsProps \u2014 no type export", + "ui/RecordActivityProps \u2014 no type export", + "ui/RecordChatterProps \u2014 no type export", + "ui/RecordDetailsProps \u2014 no type export", + "ui/RecordHighlightsField \u2014 no type export", + "ui/RecordHighlightsProps \u2014 no type export", + "ui/RecordPathProps \u2014 no type export", + "ui/RecordRelatedListProps \u2014 no type export", + "ui/ReportType \u2014 no type export", + "ui/TreeConfig \u2014 no type export", + "ui/ViewItemName \u2014 no type export" ] } diff --git a/packages/spec/dual-source-exports.baseline.json b/packages/spec/dual-source-exports.baseline.json index 9d5f137d71..f1586f91f4 100644 --- a/packages/spec/dual-source-exports.baseline.json +++ b/packages/spec/dual-source-exports.baseline.json @@ -1,11 +1,8 @@ { - "_comment": "Accepted cross-entry DUAL-SOURCE exports of @objectstack/spec (#4446): names that two or more public entry points export for DIFFERENT declarations, so which type a consumer gets depends on the import path — the #4411 trap. Shrink-only ratchet, judged by symbol identity (a re-export of one declaration from many entries is fine and not listed). A NEW name here fails check:dual-source-exports: converge on one declaration and re-export it, or rename one side — growing this list needs maintainer sign-off and shows up as this file in the diff. An entry that stops being dual-source fails until its line is deleted. Regenerate with: tsx scripts/check-dual-source-exports.ts --update (after pnpm build).", + "_comment": "Accepted cross-entry DUAL-SOURCE exports of @objectstack/spec (#4446): names that two or more public entry points export for DIFFERENT declarations, so which type a consumer gets depends on the import path \u2014 the #4411 trap. Shrink-only ratchet, judged by symbol identity (a re-export of one declaration from many entries is fine and not listed). A NEW name here fails check:dual-source-exports: converge on one declaration and re-export it, or rename one side \u2014 growing this list needs maintainer sign-off and shows up as this file in the diff. An entry that stops being dual-source fails until its line is deleted. Regenerate with: tsx scripts/check-dual-source-exports.ts --update (after pnpm build).", "entries": [ - "ActionLocationSchema — [./studio (const)] ≠ [./ui (const)]", - "EnvironmentArtifact — [./cloud (type)] ≠ [./system (type)]", - "EnvironmentArtifactInput — [./cloud (type)] ≠ [./system (type)]", - "EnvironmentArtifactSchema — [./cloud (const)] ≠ [./system (const)]", - "PackageDependency — [./cloud (type)] ≠ [./kernel (type)]", - "PackageDependencySchema — [./cloud (const)] ≠ [./kernel (const)]" + "ActionLocationSchema \u2014 [./studio (const)] \u2260 [./ui (const)]", + "PackageDependency \u2014 [./cloud (type)] \u2260 [./kernel (type)]", + "PackageDependencySchema \u2014 [./cloud (const)] \u2260 [./kernel (const)]" ] } diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 39de0cac2b..fa3fc74f2d 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1265,15 +1265,6 @@ "system/EmailVerificationConfig", "system/EncryptionAlgorithm", "system/EncryptionConfig", - "system/EnvironmentArtifact", - "system/EnvironmentArtifactChecksum", - "system/EnvironmentArtifactFunction", - "system/EnvironmentArtifactFunctionLanguageEnum", - "system/EnvironmentArtifactHashAlgorithmEnum", - "system/EnvironmentArtifactManifest", - "system/EnvironmentArtifactMetadata", - "system/EnvironmentArtifactPayloadRef", - "system/EnvironmentArtifactRequirement", "system/ExecuteSqlOperation", "system/ExtendedLogLevel", "system/ExternalServiceDestinationConfig", @@ -1420,6 +1411,7 @@ "system/SettingsChangeEvent", "system/SettingsManifest", "system/SettingsNamespacePayload", + "system/Sha256Digest", "system/SocialProviderConfig", "system/Span", "system/SpanAttributeValue", diff --git a/packages/spec/src/cloud/environment-artifact.zod.ts b/packages/spec/src/cloud/environment-artifact.zod.ts index 7affbda4af..97ff001f80 100644 --- a/packages/spec/src/cloud/environment-artifact.zod.ts +++ b/packages/spec/src/cloud/environment-artifact.zod.ts @@ -1,60 +1,25 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { z } from 'zod'; -import { lazySchema } from '../shared/lazy-schema'; -import { ObjectStackDefinitionSchema } from '../stack.zod'; - /** - * # Environment Artifact Envelope (M1) + * # Environment Artifact Envelope — re-export (#4740, #4535 C10) * - * Describes the response shape of `GET /api/v1/cloud/environments/:environmentId/artifact` - * — the assembled artifact ObjectOS pulls from the control plane. + * The envelope has exactly ONE declaration: + * `../system/environment-artifact.zod` (maintainer route A′ on #4740 — + * `./system` holds the live wire shape, `./cloud` re-exports it). Importing + * from `@objectstack/spec/cloud` and `@objectstack/spec/system` yields the + * SAME symbols, so the import path can never change the shape a consumer + * gets (the #4411 dual-source trap, closed for this name). * - * Distinct from the marketplace `PackageArtifactSchema` (a .tgz file listing). - * This envelope wraps the compiled `ObjectStackDefinitionSchema` produced by - * `objectstack compile` together with control-plane assigned identity - * (`commitId`, `checksum`). + * Do NOT re-declare the envelope here. A second declaration under this name + * is exactly what `check:dual-source-exports` and the symbol-identity pin in + * `../system/environment-artifact.test.ts` exist to reject. */ - -// --- SHA-256 digest of a single artifact payload --- -export const Sha256DigestSchema = z - .string() - .regex(/^[a-f0-9]{64}$/, 'Must be a 64-character lowercase hex SHA-256 digest') - .describe('SHA-256 digest (64 hex chars)'); - -export type Sha256Digest = z.infer; - -// --- Artifact envelope --- -export const EnvironmentArtifactSchema = lazySchema(() => z.object({ - /** Envelope format version. Increment on breaking changes. */ - schemaVersion: z.literal('0.1').default('0.1'), - - /** Control-plane environment ID this artifact belongs to. */ - environmentId: z.string(), - - /** Metadata revision assigned by the control plane on publish. */ - commitId: z.string(), - - /** - * SHA-256 digest of the canonical JSON serialization of the `metadata` - * block (stable key ordering). Computed by the control plane when - * assembling the GET response. - */ - checksum: Sha256DigestSchema, - - /** Build timestamp (ISO 8601). */ - builtAt: z.string().datetime().optional(), - - /** CLI version that produced this artifact (e.g. "objectstack-cli@0.4.0"). */ - builtWith: z.string().optional(), - - /** - * Full compiled metadata definition. - * Includes objects, views, flows, hooks, functions, agents, etc. - * This is the direct output of `objectstack compile`. - */ - metadata: ObjectStackDefinitionSchema, -})); - -export type EnvironmentArtifact = z.infer; -export type EnvironmentArtifactInput = z.input; +export { + Sha256DigestSchema, + EnvironmentArtifactSchema, +} from '../system/environment-artifact.zod'; +export type { + Sha256Digest, + EnvironmentArtifact, + EnvironmentArtifactInput, +} from '../system/environment-artifact.zod'; diff --git a/packages/spec/src/system/environment-artifact.test.ts b/packages/spec/src/system/environment-artifact.test.ts index 99508da736..663b1889c5 100644 --- a/packages/spec/src/system/environment-artifact.test.ts +++ b/packages/spec/src/system/environment-artifact.test.ts @@ -4,118 +4,288 @@ import { describe, it, expect } from 'vitest'; import { ENVIRONMENT_ARTIFACT_SCHEMA_VERSION, EnvironmentArtifactSchema, - EnvironmentArtifactChecksumSchema, - EnvironmentArtifactFunctionSchema, - EnvironmentArtifactManifestSchema, + Sha256DigestSchema, } from './environment-artifact.zod'; -describe('EnvironmentArtifactSchema', () => { - const minimal = { - schemaVersion: ENVIRONMENT_ARTIFACT_SCHEMA_VERSION, - environmentId: 'proj_01HABCDE', - commitId: 'commit_01HABCDE', - checksum: { algorithm: 'sha256' as const, value: 'a1b2c3d4' }, - metadata: {}, - functions: [], - manifest: {}, - }; - - it('accepts a minimal valid artifact', () => { - const parsed = EnvironmentArtifactSchema.parse(minimal); - expect(parsed.schemaVersion).toBe('0.1'); - expect(parsed.functions).toEqual([]); - }); +// ─── [#4740] `EnvironmentArtifact(Schema)` has ONE declaration — ./system ─── +// +// `./cloud` and `./system` both exported `EnvironmentArtifact` / +// `EnvironmentArtifactInput` / `EnvironmentArtifactSchema` for two DIFFERENT +// declarations (the #4411 trap — which shape a consumer got depended only on +// the import path): +// +// cloud/environment-artifact.zod.ts (pre-#4740) → the LIVE wire shape: +// string SHA-256 `checksum`, `metadata` = ObjectStackDefinitionSchema. +// The one runtime Zod parse of the envelope +// (packages/metadata/src/plugin.ts `_parseAndRegisterArtifact`) and all +// cloud-repo type imports used this side. +// system/environment-artifact.zod.ts (pre-#4740) → a documented richer +// "v0": `{ algorithm, value }` checksum object, category-bag `metadata`, +// inlined `functions[]`, required `manifest`, reserved `payloadRef` — +// NEVER implemented by any producer or consumer in any repo +// (objectstack / cloud / objectui). The two sides could not parse each +// other's artifacts. +// +// Maintainer ruling on #4740 (ledger #4535 C10): route A′ — converge on the +// live wire shape as the SINGLE declaration in ./system, with ./cloud +// re-exporting it (zero migration for the live consumers). Unlike C16 +// (#4739), the re-export is the SANCTIONED route here: both entries must +// keep the name, but must resolve it to the ONE ./system declaration. A +// fresh declaration on ./cloud is the forbidden route (S2 sabotage below). +// +// #4642 established that a compile-time conditional-type pin in this package +// is a no-op (tsconfig excludes `**/*.test.ts`; vitest never enables +// `typecheck`), so the load-bearing pin is the compiler-API test below, with +// anti-vacuity guards; sabotage-verified in the PR. + +/** Every export name of the retired, never-implemented v0 artifact family. */ +const RETIRED_V0_FAMILY = [ + 'EnvironmentArtifactChecksum', 'EnvironmentArtifactChecksumSchema', + 'EnvironmentArtifactFunction', 'EnvironmentArtifactFunctionSchema', + 'EnvironmentArtifactFunctionLanguage', 'EnvironmentArtifactFunctionLanguageEnum', + 'EnvironmentArtifactHashAlgorithm', 'EnvironmentArtifactHashAlgorithmEnum', + 'EnvironmentArtifactManifest', 'EnvironmentArtifactManifestSchema', + 'EnvironmentArtifactMetadata', 'EnvironmentArtifactMetadataSchema', + 'EnvironmentArtifactPayloadRef', 'EnvironmentArtifactPayloadRefSchema', + 'EnvironmentArtifactRequirement', 'EnvironmentArtifactRequirementSchema', +] as const; + +describe('[#4740] `EnvironmentArtifact(Schema)` resolves to the ./system declaration everywhere', () => { + it('resolves the export surface: one declaration in ./system, ./cloud re-exports it, the v0 family is gone', async () => { + const ts = (await import('typescript')).default; + const { resolve, relative, dirname } = await import('node:path'); + const { fileURLToPath } = await import('node:url'); + const { readFileSync } = await import('node:fs'); + + const specDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + // Every public entry point, read from package.json's exports map so a + // future entry cannot silently escape the uniqueness pin below. + const pkg = JSON.parse(readFileSync(resolve(specDir, 'package.json'), 'utf8')) as { + exports: Record; + }; + const entries: Record = {}; + for (const sub of Object.keys(pkg.exports)) { + if (sub === '.') entries[sub] = resolve(specDir, 'src/index.ts'); + else if (/^\.\/[a-z-]+$/.test(sub)) entries[sub] = resolve(specDir, `src/${sub.slice(2)}/index.ts`); + // './openapi.json' / './package.json' are not TypeScript entry points. + } + // Anti-vacuity: the enumeration must have found the real surface. + expect(Object.keys(entries)).toContain('./cloud'); + expect(Object.keys(entries)).toContain('./system'); + expect(Object.keys(entries).length).toBeGreaterThan(10); + + const program = ts.createProgram(Object.values(entries), { + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + noEmit: true, + }); + const checker = program.getTypeChecker(); + const unalias = (s: import('typescript').Symbol) => + s.getFlags() & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s; + + const exportsOf = (sub: string) => { + const sf = program.getSourceFile(entries[sub]); + const moduleSym = sf && checker.getSymbolAtLocation(sf); + // Without this guard a resolution failure would make every assertion + // below pass vacuously — the exact way a gate goes dormant (#4642). + expect(moduleSym, `${sub} module symbol must resolve`).toBeTruthy(); + return checker.getExportsOfModule(moduleSym!); + }; + + const originOf = (sym: import('typescript').Symbol, label: string) => { + const decl = unalias(sym).declarations?.[0]; + expect(decl, `${label} must have a declaration`).toBeTruthy(); + const declFile = decl!.getSourceFile(); + return `${relative(specDir, declFile.fileName)}:${ + declFile.getLineAndCharacterOfPosition(decl!.getStart()).line + 1 + }`; + }; + + // 1. The surviving declaration: `./system` exports the envelope, declared + // in system/environment-artifact.zod.ts — and still exports a + // non-trivial surface, so the `not.toContain` checks below cannot pass + // by resolving nothing. + const systemExports = exportsOf('./system'); + expect(systemExports.length, './system must export a non-trivial surface').toBeGreaterThan(100); + const names = ['EnvironmentArtifact', 'EnvironmentArtifactInput', 'EnvironmentArtifactSchema'] as const; + const canonical: Record = {}; + for (const name of names) { + const sym = systemExports.find((e) => e.getName() === name); + expect(sym, `./system must export \`${name}\``).toBeTruthy(); + const origin = originOf(sym!, `./system ${name}`); + expect(origin).toMatch(/^src\/system\/environment-artifact\.zod\.ts:\d+$/); + canonical[name] = origin; + } + + // 2. The re-exporting side: `./cloud` keeps every pre-#4740 name, but + // each resolves to the SAME ./system declaration — the sanctioned + // route A′. A fresh cloud-side declaration flips this red (S2). + const cloudExports = exportsOf('./cloud'); + for (const name of names) { + const sym = cloudExports.find((e) => e.getName() === name); + expect(sym, `./cloud must keep exporting \`${name}\``).toBeTruthy(); + expect( + originOf(sym!, `./cloud ${name}`), + `./cloud must resolve \`${name}\` to the ./system declaration`, + ).toBe(canonical[name]); + } + // `Sha256Digest(Schema)` moved with the declaration; ./cloud keeps it by + // re-export too. + for (const name of ['Sha256Digest', 'Sha256DigestSchema']) { + const sys = systemExports.find((e) => e.getName() === name); + const cld = cloudExports.find((e) => e.getName() === name); + expect(sys, `./system must export \`${name}\``).toBeTruthy(); + expect(cld, `./cloud must keep exporting \`${name}\``).toBeTruthy(); + expect(originOf(cld!, `./cloud ${name}`)).toBe(originOf(sys!, `./system ${name}`)); + expect(originOf(sys!, `./system ${name}`)).toMatch( + /^src\/system\/environment-artifact\.zod\.ts:\d+$/, + ); + } - it('defaults functions to an empty array when omitted', () => { - const { functions: _omit, ...rest } = minimal; - const parsed = EnvironmentArtifactSchema.parse(rest); - expect(parsed.functions).toEqual([]); + // 3. Uniqueness — the dual-source pin proper: across EVERY public entry, + // an export named `EnvironmentArtifact(Input|Schema)` must resolve to + // the ONE ./system declaration. Re-adding a second declaration under + // any entry (S1/S2 sabotage) turns this red. + for (const sub of Object.keys(entries)) { + for (const name of names) { + for (const sym of exportsOf(sub).filter((e) => e.getName() === name)) { + expect( + originOf(sym, `${sub} ${name}`), + `${sub} must resolve \`${name}\` to the ./system declaration`, + ).toBe(canonical[name]); + } + } + } + + // 4. The retired v0 family exists in NO entry any more. + for (const sub of Object.keys(entries)) { + const subNames = exportsOf(sub).map((e) => e.getName()); + for (const gone of RETIRED_V0_FAMILY) { + expect(subNames, `${sub} must not name ${gone}`).not.toContain(gone); + } + } }); - it('rejects unknown schemaVersion values', () => { - const result = EnvironmentArtifactSchema.safeParse({ ...minimal, schemaVersion: '9.9' }); - expect(result.success).toBe(false); + it('keeps the runtime namespaces consistent with the compiler view', async () => { + const cloud = await import('../cloud/index'); + const system = await import('./index'); + // Re-export means the very same binding, not a lookalike. + expect(cloud.EnvironmentArtifactSchema).toBe(system.EnvironmentArtifactSchema); + expect(cloud.Sha256DigestSchema).toBe(system.Sha256DigestSchema); + for (const gone of RETIRED_V0_FAMILY) { + expect(gone in cloud, `cloud must not export ${gone}`).toBe(false); + expect(gone in system, `system must not export ${gone}`).toBe(false); + } }); +}); + +// ─── Wire-shape behavior ──────────────────────────────────────────────────── + +const WIRE_CHECKSUM = '0123456789abcdef'.repeat(4); // 64 lowercase hex chars + +const wireMinimal = { + schemaVersion: ENVIRONMENT_ARTIFACT_SCHEMA_VERSION, + environmentId: 'proj_01HABCDE', + commitId: 'commit_01HABCDE', + checksum: WIRE_CHECKSUM, + metadata: { + objects: [{ name: 'account', label: 'Account', fields: {} }], + }, +}; - it('requires environmentId and commitId', () => { - expect(EnvironmentArtifactSchema.safeParse({ ...minimal, environmentId: '' }).success).toBe(false); - expect(EnvironmentArtifactSchema.safeParse({ ...minimal, commitId: '' }).success).toBe(false); +describe('EnvironmentArtifactSchema (wire shape)', () => { + it('accepts a minimal control-plane artifact', () => { + const parsed = EnvironmentArtifactSchema.parse(wireMinimal); + expect(parsed.schemaVersion).toBe('0.1'); + expect(parsed.checksum).toBe(WIRE_CHECKSUM); + expect(parsed.metadata.objects?.[0]?.name).toBe('account'); }); - it('passes through unknown metadata categories without dropping them', () => { - const parsed = EnvironmentArtifactSchema.parse({ - ...minimal, - metadata: { objects: [{ name: 'account' }], futureCategory: [{ id: 'x' }] }, - }); - expect((parsed.metadata as Record).futureCategory).toBeDefined(); + it('defaults schemaVersion and rejects unknown versions', () => { + const { schemaVersion: _omit, ...rest } = wireMinimal; + expect(EnvironmentArtifactSchema.parse(rest).schemaVersion).toBe('0.1'); + expect(EnvironmentArtifactSchema.safeParse({ ...wireMinimal, schemaVersion: '9.9' }).success).toBe(false); }); it('accepts optional builtAt / builtWith provenance', () => { const parsed = EnvironmentArtifactSchema.parse({ - ...minimal, + ...wireMinimal, builtAt: '2026-04-26T00:00:00Z', - builtWith: 'objectstack-cli@3.4.0', + builtWith: 'objectstack-cli@0.4.0', }); expect(parsed.builtAt).toBe('2026-04-26T00:00:00Z'); }); - it('accepts optional payloadRef for future S3 indirection', () => { - const parsed = EnvironmentArtifactSchema.parse({ - ...minimal, - payloadRef: { - url: 'https://artifacts.objectstack.io/proj_x/commit_y.json', - checksum: { algorithm: 'sha256' as const, value: 'deadbeef' }, - }, - }); - expect(parsed.payloadRef?.url).toContain('artifacts.objectstack.io'); + it('validates metadata as an ObjectStackDefinition, not an opaque bag', () => { + expect( + EnvironmentArtifactSchema.safeParse({ ...wireMinimal, metadata: { objects: 'not-an-array' } }).success, + ).toBe(false); + expect(EnvironmentArtifactSchema.safeParse({ ...wireMinimal, metadata: [] }).success).toBe(false); }); -}); -describe('EnvironmentArtifactChecksumSchema', () => { - it('accepts lowercase hex values', () => { - expect(EnvironmentArtifactChecksumSchema.parse({ value: 'abc123' }).algorithm).toBe('sha256'); - }); + // ⚠ #4666 pin — the checksum object→string convergence is a TYPE change + // invisible to the key-level authorable-surface gates (`checksum` exists in + // both shapes). These parses are the gate for it. + describe('checksum is a 64-char hex STRING (#4666 pin)', () => { + it('accepts a 64-char lowercase hex digest', () => { + expect(Sha256DigestSchema.parse(WIRE_CHECKSUM)).toBe(WIRE_CHECKSUM); + }); - it('rejects uppercase / non-hex values', () => { - expect(EnvironmentArtifactChecksumSchema.safeParse({ value: 'ABC123' }).success).toBe(false); - expect(EnvironmentArtifactChecksumSchema.safeParse({ value: 'not-hex' }).success).toBe(false); - }); -}); + it('rejects the retired `{ algorithm, value }` checksum object', () => { + const result = EnvironmentArtifactSchema.safeParse({ + ...wireMinimal, + checksum: { algorithm: 'sha256', value: WIRE_CHECKSUM }, + }); + expect(result.success).toBe(false); + }); -describe('EnvironmentArtifactFunctionSchema', () => { - it('accepts a typical inlined function', () => { - const parsed = EnvironmentArtifactFunctionSchema.parse({ - name: 'on_account_create', - code: 'export default async (ctx) => {}', + it('rejects non-64-char, uppercase and non-hex values', () => { + for (const bad of ['abc123', WIRE_CHECKSUM.slice(0, 63), WIRE_CHECKSUM.toUpperCase(), 'not-hex']) { + expect(Sha256DigestSchema.safeParse(bad).success, `must reject ${JSON.stringify(bad)}`).toBe(false); + } }); - expect(parsed.language).toBe('javascript'); }); - it('rejects function names that are not snake_case', () => { - expect( - EnvironmentArtifactFunctionSchema.safeParse({ name: 'OnAccountCreate', code: '' }).success, - ).toBe(false); - }); -}); + // The v0 keys are tombstoned, not silently stripped: authoring one raises + // the prescription itself (retiredKey, #3855), and `tsc` types the key + // `never` at the authoring site. + describe('retired v0 keys are tombstoned (#4740, ADR-0049)', () => { + it('rejects `functions` with the prescription', () => { + expect(() => EnvironmentArtifactSchema.parse({ ...wireMinimal, functions: [] })) + .toThrow(/`environmentArtifact\.functions` was removed in @objectstack\/spec 17\.0\.0.*Delete the key/s); + }); -describe('EnvironmentArtifactManifestSchema', () => { - it('accepts an empty manifest', () => { - expect(EnvironmentArtifactManifestSchema.parse({})).toEqual({}); - }); + it('rejects `manifest` with the prescription', () => { + expect(() => EnvironmentArtifactSchema.parse({ ...wireMinimal, manifest: {} })) + .toThrow(/`environmentArtifact\.manifest` was removed in @objectstack\/spec 17\.0\.0.*metadata\.manifest/s); + }); - it('accepts plugins, drivers and engine constraints', () => { - const parsed = EnvironmentArtifactManifestSchema.parse({ - plugins: [{ id: '@objectstack/plugin-auth', version: '^3.0.0' }], - drivers: [{ id: '@objectstack/driver-sql' }], - engine: { objectstack: '>=3.0.0' }, + it('rejects `payloadRef` with the prescription', () => { + expect(() => EnvironmentArtifactSchema.parse({ ...wireMinimal, payloadRef: { url: 'https://x' } })) + .toThrow(/`environmentArtifact\.payloadRef` was removed in @objectstack\/spec 17\.0\.0.*Delete the key/s); }); - expect(parsed.plugins?.[0].id).toBe('@objectstack/plugin-auth'); - expect(parsed.engine?.objectstack).toBe('>=3.0.0'); - }); - it('rejects malformed engine version ranges', () => { - expect( - EnvironmentArtifactManifestSchema.safeParse({ engine: { objectstack: 'not-a-range' } }).success, - ).toBe(false); + it('a full pre-#4740 v0 artifact is rejected loudly, not half-parsed', () => { + const v0 = { + schemaVersion: '0.1', + environmentId: 'proj_01HABCDE', + commitId: 'commit_01HABCDE', + checksum: { algorithm: 'sha256', value: 'a1b2c3d4' }, + metadata: { objects: [{ name: 'account', label: 'Account' }] }, + functions: [{ name: 'on_account_create', code: 'export default async () => {}' }], + manifest: { plugins: [{ id: '@objectstack/plugin-auth' }] }, + }; + expect(EnvironmentArtifactSchema.safeParse(v0).success).toBe(false); + }); + + it('absent tombstoned keys stay absent from the parse result', () => { + const parsed = EnvironmentArtifactSchema.parse(wireMinimal); + expect(parsed).not.toHaveProperty('functions'); + expect(parsed).not.toHaveProperty('payloadRef'); + // NB `manifest` may legitimately appear INSIDE metadata (the stack + // manifest) — the tombstone governs the envelope level only. + expect(Object.keys(parsed)).not.toContain('manifest'); + }); }); }); diff --git a/packages/spec/src/system/environment-artifact.zod.ts b/packages/spec/src/system/environment-artifact.zod.ts index 05ef94abb7..8e1c64c9c3 100644 --- a/packages/spec/src/system/environment-artifact.zod.ts +++ b/packages/spec/src/system/environment-artifact.zod.ts @@ -1,32 +1,53 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { lazySchema } from '../shared/lazy-schema'; +import { retiredKey } from '../shared/retired-key'; +import { ObjectStackDefinitionSchema } from '../stack.zod'; /** - * # Environment Artifact Format Protocol (v0) + * # Environment Artifact Envelope * - * Defines the immutable envelope produced by `objectstack compile` and consumed - * by the ObjectStack runtime at boot. The artifact carries everything a runtime instance - * needs to hydrate an environment kernel without reading control-plane DB rows - * directly. + * THE single declaration of the environment artifact envelope (#4740, + * #4535 C10 — maintainer route A′). `@objectstack/spec/cloud` re-exports + * this file; both entry points resolve to these exact symbols, so the + * chosen entry point can never change the shape a consumer gets (the + * #4411 dual-source trap, closed for this name). + * + * Describes the response shape of + * `GET /api/v1/cloud/environments/:environmentId/artifact` — the assembled + * artifact ObjectOS pulls from the control plane, and the shape the runtime + * metadata loader parses at boot (`packages/metadata/src/plugin.ts`, + * `_parseAndRegisterArtifact` — the one runtime Zod parse of this envelope). + * + * Distinct from the marketplace `PackageArtifactSchema` (a .tgz file + * listing). This envelope wraps the compiled `ObjectStackDefinitionSchema` + * produced by `objectstack compile` together with control-plane assigned + * identity (`commitId`, `checksum`). * * ## Boundary * - * - **Artifact (this schema):** environment metadata + inlined function code + - * plugin/driver requirements. Immutable, content-addressable via `commitId` - * and `checksum`. + * - **Artifact (this schema):** compiled environment metadata plus + * provenance. Immutable, content-addressable via `commitId` and + * `checksum`. * - **Deployment Config (NOT in this schema):** business DB coordinates, * credentials, environment identity, secrets. Injected at runtime. * - * See {@link content/docs/concepts/north-star.mdx} §6.3 for the runtime-inputs - * boundary, and {@link ROADMAP.md} M1 for the milestone definition. + * See {@link content/docs/concepts/north-star.mdx} §6.3 for the + * runtime-inputs boundary. * - * ## Storage / Distribution + * ## History (#4740) * - * v0 stores the full payload inline. Future revisions may swap `metadata` / - * `functions` for a `payloadRef` that points at out-of-band storage (S3, - * signed URL). The envelope shape preserves room for that indirection without - * a breaking schema bump. + * This file previously documented a richer "v0" envelope — a + * `{ algorithm, value }` checksum object, a category-bag `metadata`, inlined + * `functions[]`, a required plugin/driver `manifest`, and a reserved + * `payloadRef` indirection — that NO producer or consumer ever implemented: + * `objectstack compile` ships function code as standalone runtime modules + * referenced from the compiled definition, and the control plane has always + * served the wire shape below (string SHA-256 checksum, `metadata` = the + * compiled definition). The declaration converged to the live wire shape; + * the never-implemented keys are tombstoned below (ADR-0049 + * enforce-or-remove: declared = enforced, or absent). */ // ========================================== @@ -36,207 +57,17 @@ import { z } from 'zod'; /** Current artifact schema version. Bump on every breaking envelope change. */ export const ENVIRONMENT_ARTIFACT_SCHEMA_VERSION = '0.1' as const; -/** Hash algorithms permitted for artifact checksums. */ -export const EnvironmentArtifactHashAlgorithmEnum = z - .enum(['sha256', 'sha384', 'sha512']) - .describe('Hash algorithm used for the artifact checksum'); - -export type EnvironmentArtifactHashAlgorithm = z.infer; - // ========================================== // Checksum // ========================================== -/** - * Content-addressable checksum of the canonical JSON-serialized artifact body - * (everything except the `checksum` field itself). Used by the runtime to verify - * that the artifact bytes were not tampered with in transit and to key the - * local artifact cache. - */ -export const EnvironmentArtifactChecksumSchema = z - .object({ - algorithm: EnvironmentArtifactHashAlgorithmEnum.default('sha256'), - value: z - .string() - .regex(/^[a-f0-9]+$/, 'Checksum value must be lowercase hexadecimal') - .describe('Hex-encoded digest of the artifact body'), - }) - .describe('Artifact integrity checksum'); - -export type EnvironmentArtifactChecksum = z.infer; - -// ========================================== -// Function code packaging -// ========================================== - -/** - * Languages supported for inlined function code. The runtime decides how to - * load each language; v0 only commits to JavaScript bytes shipping unmodified. - */ -export const EnvironmentArtifactFunctionLanguageEnum = z - .enum(['javascript', 'typescript']) - .describe('Source language of the function code'); - -export type EnvironmentArtifactFunctionLanguage = z.infer; - -/** - * A single function (object trigger, computed field, action, etc.) packaged - * into the artifact. Function code is inlined as a UTF-8 string; binary or - * out-of-band storage is reserved for a future revision via `payloadRef`. - */ -export const EnvironmentArtifactFunctionSchema = z - .object({ - /** Globally unique function name (snake_case). */ - name: z - .string() - .regex(/^[a-z_][a-z0-9_]*$/) - .describe('Function machine name (snake_case)'), - - /** Source language of the inlined `code` field. */ - language: EnvironmentArtifactFunctionLanguageEnum.default('javascript'), - - /** UTF-8 encoded function source. Must be self-contained. */ - code: z.string().describe('Inlined function source'), - - /** - * Optional provenance pointer: where the code came from in the original - * TypeScript workspace. Useful for debug overlays in Studio. - */ - source: z - .object({ - path: z.string().optional().describe('Source file path (relative to project root)'), - exportName: z.string().optional().describe('Exported symbol name'), - }) - .optional() - .describe('Source-map metadata for the function'), +/** SHA-256 digest of a single artifact payload. */ +export const Sha256DigestSchema = z + .string() + .regex(/^[a-f0-9]{64}$/, 'Must be a 64-character lowercase hex SHA-256 digest') + .describe('SHA-256 digest (64 hex chars)'); - /** Hex SHA-256 of `code` for cache invalidation. */ - hash: z - .string() - .regex(/^[a-f0-9]+$/) - .optional() - .describe('Hex SHA-256 of the inlined code'), - }) - .describe('A single inlined function'); - -export type EnvironmentArtifactFunction = z.infer; - -// ========================================== -// Plugin / Driver requirements -// ========================================== - -/** - * Plugin/driver requirement entry. The runtime uses these to verify that the - * runtime has every plugin the environment depends on before hydrating the kernel. - * Configuration values live in **Deployment Config**, not in the artifact. - */ -export const EnvironmentArtifactRequirementSchema = z - .object({ - /** Package id (reverse-domain or short id). */ - id: z.string().describe('Plugin/driver package id'), - - /** SemVer range required by the environment. */ - version: z.string().optional().describe('SemVer range required by the environment'), - }) - .describe('A plugin or driver dependency declaration'); - -export type EnvironmentArtifactRequirement = z.infer; - -/** - * Environment-level manifest captured inside the artifact. Mirrors the parts of - * the package manifest the runtime needs to bootstrap; user-facing manifest - * fields (description, icon, marketplace metadata) are excluded. - */ -export const EnvironmentArtifactManifestSchema = z - .object({ - /** Plugins required to run this environment's metadata. */ - plugins: z.array(EnvironmentArtifactRequirementSchema).optional(), - - /** Drivers required to run this environment's metadata. */ - drivers: z.array(EnvironmentArtifactRequirementSchema).optional(), - - /** Minimum platform version (mirrors `Manifest.engine`). */ - engine: z - .object({ - objectstack: z - .string() - .regex(/^[><=~^]*\d+\.\d+\.\d+/) - .describe('ObjectStack platform version requirement (SemVer range)'), - }) - .optional(), - }) - .describe('Plugin/driver requirements baked into the artifact'); - -export type EnvironmentArtifactManifest = z.infer; - -// ========================================== -// Metadata payload -// ========================================== - -/** - * Compiled project metadata. v0 is intentionally permissive: the inner shape - * is validated by the protocol-level `ObjectStackDefinitionSchema` (and per- - * domain Zod schemas) rather than re-validated here, to avoid coupling the - * artifact envelope to every domain schema bump. - * - * Treat this as a typed bag of arrays keyed by metadata category. Unknown - * categories are passed through (`passthrough()`) so older runtime builds can - * boot newer artifacts safely if no breaking changes were made. - */ -export const EnvironmentArtifactMetadataSchema = z - .object({ - objects: z.array(z.unknown()).optional(), - fields: z.array(z.unknown()).optional(), - views: z.array(z.unknown()).optional(), - apps: z.array(z.unknown()).optional(), - pages: z.array(z.unknown()).optional(), - dashboards: z.array(z.unknown()).optional(), - reports: z.array(z.unknown()).optional(), - flows: z.array(z.unknown()).optional(), - workflows: z.array(z.unknown()).optional(), - triggers: z.array(z.unknown()).optional(), - agents: z.array(z.unknown()).optional(), - tools: z.array(z.unknown()).optional(), - skills: z.array(z.unknown()).optional(), - permissions: z.array(z.unknown()).optional(), - permissionSets: z.array(z.unknown()).optional(), - // ADR-0090 D3/D2: `roles`/`profiles` categories retired with the concepts - // (stacks declare `positions`; the profile concept is gone). Old artifacts - // carrying them still parse via `passthrough()` — the keys are simply no - // longer part of the declared envelope. - positions: z.array(z.unknown()).optional(), - translations: z.array(z.unknown()).optional(), - datasources: z.array(z.unknown()).optional(), - datasets: z.array(z.unknown()).optional(), - actions: z.array(z.unknown()).optional(), - apis: z.array(z.unknown()).optional(), - }) - .passthrough() - .describe('Compiled environment metadata grouped by category'); - -export type EnvironmentArtifactMetadata = z.infer; - -// ========================================== -// Out-of-band payload reference (reserved) -// ========================================== - -/** - * Reserved indirection for moving large payloads out of the inline JSON. v0 - * artifacts inline `metadata` and `functions` directly; future revisions can - * set `payloadRef` to a signed URL and omit (or truncate) the inline copies. - * - * Defined now so the envelope shape is stable across the inline-only ↔ S3 - * transition. - */ -export const EnvironmentArtifactPayloadRefSchema = z - .object({ - url: z.string().url().describe('Signed URL pointing at the artifact payload'), - expiresAt: z.string().datetime().optional().describe('ISO-8601 expiry timestamp'), - checksum: EnvironmentArtifactChecksumSchema.describe('Checksum of the referenced payload'), - }) - .describe('Out-of-band payload reference (reserved for future use)'); - -export type EnvironmentArtifactPayloadRef = z.infer; +export type Sha256Digest = z.infer; // ========================================== // Envelope @@ -245,69 +76,74 @@ export type EnvironmentArtifactPayloadRef = z.infer; +export const EnvironmentArtifactSchema = lazySchema(() => z.object({ + /** Envelope format version. Increment on breaking changes. */ + schemaVersion: z + .literal(ENVIRONMENT_ARTIFACT_SCHEMA_VERSION) + .default(ENVIRONMENT_ARTIFACT_SCHEMA_VERSION), + + /** Control-plane environment ID this artifact belongs to. */ + environmentId: z.string(), + + /** Metadata revision assigned by the control plane on publish. */ + commitId: z.string(), + + /** + * SHA-256 digest of the canonical JSON serialization of the `metadata` + * block (stable key ordering). Computed by the control plane when + * assembling the GET response. + * + * ⚠ A STRING — not the retired `{ algorithm, value }` object (#4740, + * a type change invisible to key-level gates, #4666; pinned by the + * parse tests next to this file). + */ + checksum: Sha256DigestSchema, + + /** Build timestamp (ISO 8601). */ + builtAt: z.string().datetime().optional(), + + /** CLI version that produced this artifact (e.g. "objectstack-cli@0.4.0"). */ + builtWith: z.string().optional(), + + /** + * Full compiled metadata definition. + * Includes objects, views, flows, hooks, functions, agents, etc. + * This is the direct output of `objectstack compile`. + */ + metadata: ObjectStackDefinitionSchema, + + // ── Retired v0 keys (#4740, ADR-0049) ────────────────────────────── + // Declared-but-never-implemented in the pre-convergence ./system shape. + // Tombstoned (not silently stripped) so a producer that authors one gets + // the prescription at parse time and `tsc` rejects it at the authoring + // site. No ADR-0087 conversion is registered: the envelope is a transport + // shape — never stored as a sys_metadata row, never walked by the + // conversion chain — and no producer ever emitted these keys. + + functions: retiredKey( + '`environmentArtifact.functions` was removed in @objectstack/spec 17.0.0 (#4740, ADR-0049) — ' + + 'the v0 inline-functions block never had a producer or reader: `objectstack compile` has always ' + + 'shipped function code as standalone runtime modules referenced from the compiled definition. ' + + 'Delete the key; function code travels inside `metadata` (the compiled ObjectStackDefinition).', + ), + + manifest: retiredKey( + '`environmentArtifact.manifest` was removed in @objectstack/spec 17.0.0 (#4740, ADR-0049) — ' + + 'the v0 plugin/driver requirements block never had a producer or reader. Delete the key; ' + + 'package/plugin requirements live in the stack manifest inside `metadata` ' + + '(`metadata.manifest`, ManifestSchema).', + ), + + payloadRef: retiredKey( + '`environmentArtifact.payloadRef` was removed in @objectstack/spec 17.0.0 (#4740, ADR-0049) — ' + + 'the reserved out-of-band payload indirection was never implemented; every artifact inlines ' + + '`metadata`. Delete the key.', + ), +})); + +export type EnvironmentArtifact = z.infer; export type EnvironmentArtifactInput = z.input;