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
42 changes: 42 additions & 0 deletions .changeset/vscode-snippets-parse-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
"objectstack-vscode": patch
---

fix(vscode): every contributed snippet expands to metadata the spec accepts — and a gate that keeps it that way (#4917)

The extension's snippets are a metadata **producer**: whatever `os-view-grid`
expands to is the first `.view.ts` an author (human or AI) ever writes. Nothing
in this repo has ever parsed that output, so the snippets drifted out of the
spec in silence. An audit of all eight found **five** broken against
`@objectstack/spec` 17:

| snippet | what was rejected | canonical form now |
|---|---|---|
| `os-view-grid` | `list.defaultSort`, `list.pageSize` (never declared on `ListViewSchema`); plus `type` / `objectName` on the **container**, which is the flat-view-where-a-container-goes mistake `ViewSchema`'s own guidance names | `defineView({ object, list: { …, sort: [{ field, order }], pagination: { pageSize } } })` |
| `os-flow` | node `name` / `next` (the keys are `label` + an `edges` array), and a top-level `trigger` block | `defineFlow` with the object binding on the START node's `config: { objectName, triggerType }` and an explicit `edges: []` |
| `os-agent` | `tools` — removed in protocol 17 (#3894) | `skills: []` |
| `os-stack` | `manifest` missing the required `id` and `type` | `{ id, namespace, version, type, name, engines }` |
| `os-field-lookup` | `reference: { object, labelField }` — `reference` is a plain object name | `reference: 'target_object'` + `displayField` |

Separately, **all five** module snippets imported `{ Data }` / `{ UI }` /
`{ Automation }` / `{ AI }` from the package root. Those namespace re-exports
were removed for being untree-shakeable (see `packages/spec/src/index.ts`), so
the very first line of each scaffold did not resolve. They now import from the
subpath and author through the domain's validating factory — `ObjectSchema.create`,
`defineView`, `defineFlow`, `defineAgent`, `defineStack` — which parses at
authoring time and, being a *value* import, fails loudly instead of degrading
to `any` (issue #2035's rationale, applied to the scaffolds themselves).

**The recurrence is what actually got fixed.** `os-view-grid` broke because
#4001 closed `ListViewSchema` for unknown keys and no gate anywhere could see a
snippet body; the next strictness batch would have broken another one the same
way. The package now has a `test` script that expands every snippet, evaluates
it against the real spec, and `safeParse`s the authored literal with the schema
the runtime uses. Three independent failure modes are covered — the expansion
does not evaluate, the literal does not parse, or an import names a binding the
spec no longer exports — with a negative control asserting the pre-fix shape is
still rejected, a plan table that fails when a snippet arrives ungated, and a
lockstep check on the `engines.protocol` major so that stamp cannot rot either.

No authoring change is required of anyone: this only replaces snippet output
that never validated.
37 changes: 27 additions & 10 deletions packages/vscode-objectstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,30 @@

## Snippets

| Prefix | Description |
|--------|-------------|
| `os-object` | Define a new business object |
| `os-field-text` | Add a text field |
| `os-field-select` | Add a select (picklist) field |
| `os-field-lookup` | Add a lookup (reference) field |
| `os-view-grid` | Define a grid list view |
| `os-flow` | Define an automation flow |
| `os-stack` | Full `defineStack` boilerplate |
| `os-agent` | Define an AI agent |
Every snippet scaffolds through the spec's own authoring factory
(`ObjectSchema.create`, `defineView`, `defineFlow`, `defineAgent`,
`defineStack`), so what you tab out of the IDE validates against
`@objectstack/spec` the moment it runs — never a bare `: Type` literal that
type-checks over a shape nothing ever parses.

| Prefix | Scaffolds | Validated by |
|--------|-----------|--------------|
| `os-object` | A new business object | `ObjectSchema.create` |
| `os-field-text` | A text field (paste inside `fields: { … }`) | `FieldSchema` |
| `os-field-select` | A select (picklist) field | `FieldSchema` |
| `os-field-lookup` | A lookup (reference) field | `FieldSchema` |
| `os-view-grid` | A grid list view container | `defineView` |
| `os-flow` | A record-change automation flow | `defineFlow` |
| `os-stack` | Full `defineStack` boilerplate | `defineStack` |
| `os-agent` | An AI agent | `defineAgent` |

That claim is enforced, not advertised: `pnpm test` in this package expands
every snippet, evaluates it against the real `@objectstack/spec`, and
`safeParse`s the authored literal with the same schema the runtime uses — plus
a check that each import binding still exists on the spec's export surface. A
snippet that goes stale (as `os-view-grid` did when `ListViewSchema` closed
`defaultSort` / `pageSize`) fails CI instead of shipping. See
`test/snippets.test.ts`.

## Installation

Expand Down Expand Up @@ -67,6 +81,9 @@ npm run build
# Watch for changes
npm run watch

# Verify every contributed snippet still parses against @objectstack/spec
npm test

# Package as .vsix
npm run package
```
Expand Down
8 changes: 6 additions & 2 deletions packages/vscode-objectstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,16 @@
"build": "tsc -p ./tsconfig.json",
"watch": "tsc -watch -p ./tsconfig.json",
"package": "vsce package",
"typecheck": "tsc --noEmit"
"test": "vitest run",
"typecheck": "tsc --noEmit && tsc -p ./tsconfig.test.json"
},
"devDependencies": {
"@objectstack/spec": "workspace:*",
"@types/node": "^26.1.2",
"@types/vscode": "^1.125.0",
"@vscode/vsce": "^3.9.2",
"typescript": "^6.0.3"
"typescript": "^6.0.3",
"vitest": "^4.1.10"
},
"keywords": [
"objectstack",
Expand Down
84 changes: 41 additions & 43 deletions packages/vscode-objectstack/snippets/objectstack.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"prefix": "os-object",
"description": "Define a new ObjectStack business object",
"body": [
"import { Data } from '@objectstack/spec';",
"import { ObjectSchema } from '@objectstack/spec/data';",
"",
"const ${1:myObject}: Data.Object = {",
"export const ${1:MyObject} = ObjectSchema.create({",
" name: '${2:my_object}',",
" label: '${3:My Object}',",
" pluralLabel: '${3:My Object}s',",
Expand All @@ -18,9 +18,7 @@
" },",
" $0",
" },",
"};",
"",
"export default ${1:myObject};"
"});"
]
},
"ObjectStack: Text Field": {
Expand Down Expand Up @@ -59,66 +57,65 @@
" type: 'lookup',",
" label: '${2:Related Object}',",
" required: ${3|false,true|},",
" reference: {",
" object: '${4:target_object}',",
" labelField: '${5:name}',",
" },",
" reference: '${4:target_object}',",
" displayField: '${5:name}',",
"},"
]
},
"ObjectStack: Grid List View": {
"prefix": "os-view-grid",
"description": "Define a grid list view for an object",
"body": [
"import { UI } from '@objectstack/spec';",
"import { defineView } from '@objectstack/spec';",
"",
"const ${1:myObject}ListView: UI.View = {",
" name: '${2:my_object}_list',",
" label: '${3:My Object} List',",
" type: 'list',",
" objectName: '${2:my_object}',",
"export const ${1:MyObject}Views = defineView({",
" object: '${2:my_object}',",
" list: {",
" label: '${3:My Object} List',",
" type: 'grid',",
" data: { provider: 'object', object: '${2:my_object}' },",
" columns: [",
" { field: 'name', width: 200 },",
" $0",
" ],",
" defaultSort: { field: 'name', direction: 'asc' },",
" pageSize: 25,",
" sort: [{ field: 'name', order: 'asc' }],",
" pagination: { pageSize: 25 },",
" },",
"};",
"",
"export default ${1:myObject}ListView;"
"});"
]
},
"ObjectStack: Automation Flow": {
"prefix": "os-flow",
"description": "Define an automation flow",
"description": "Define a record-change automation flow",
"body": [
"import { Automation } from '@objectstack/spec';",
"import { defineFlow } from '@objectstack/spec';",
"",
"const ${1:myFlow}: Automation.Flow = {",
"export const ${1:MyFlow} = defineFlow({",
" name: '${2:my_flow}',",
" label: '${3:My Flow}',",
" type: '${4|autolaunched,screen,schedule|}',",
" type: 'record_change',",
" status: 'draft',",
" trigger: {",
" type: 'record_change',",
" object: '${5:my_object}',",
" events: ['after_insert', 'after_update'],",
" },",
" nodes: [",
" {",
" id: 'start',",
" type: 'start',",
" name: 'Start',",
" next: '${6:end}',",
" label: 'Start',",
" config: {",
" objectName: '${4:my_object}',",
" triggerType: '${5|record-after-write,record-after-insert,record-after-update,record-after-delete|}',",
" },",
" },",
" $0",
" {",
" id: 'end',",
" type: 'end',",
" label: 'End',",
" },",
" ],",
"};",
"",
"export default ${1:myFlow};"
" edges: [",
" { id: 'e1', source: 'start', target: 'end' },",
" ],",
"});"
]
},
"ObjectStack: defineStack Boilerplate": {
Expand All @@ -129,9 +126,12 @@
"",
"export default defineStack({",
" manifest: {",
" name: '${1:my_app}',",
" version: '${2:0.1.0}',",
" label: '${3:My Application}',",
" id: '${1:com.example.my_app}',",
" namespace: '${2:my_app}',",
" version: '${3:0.1.0}',",
" type: 'app',",
" name: '${4:My Application}',",
" engines: { protocol: '^17' },",
" },",
" objects: [",
" $0",
Expand All @@ -145,9 +145,9 @@
"prefix": "os-agent",
"description": "Define an AI agent",
"body": [
"import { AI } from '@objectstack/spec';",
"import { defineAgent } from '@objectstack/spec';",
"",
"const ${1:myAgent}: AI.Agent = {",
"export const ${1:MyAgent} = defineAgent({",
" name: '${2:my_agent}',",
" label: '${3:My Agent}',",
" role: '${4:Assistant}',",
Expand All @@ -156,12 +156,10 @@
" provider: '${6|openai,anthropic,google|}',",
" model: '${7:gpt-4o}',",
" },",
" tools: [",
" skills: [",
" $0",
" ],",
"};",
"",
"export default ${1:myAgent};"
"});"
]
}
}
Loading
Loading