@open-domain-specification/skill 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ # Patterns from the Petstore example
2
+
3
+ Excerpts from the ODS example workspace (`models/petstore/src/workspace.ts`
4
+ in the ODS repository). Each shows one pattern worth copying.
5
+
6
+ ## A context serving two subdomains, and a legacy one
7
+
8
+ ```ts
9
+ const inventoryBC = workspace.addBoundedContext("Inventory BC", {
10
+ description: "Projection for /store/inventory (status→count)",
11
+ subdomains: [inventorySD, catalogSD],
12
+ team: petShopTeam,
13
+ });
14
+ const identityBC = usersSD.addBoundedcontext("Identity BC", {
15
+ description: "Owns User aggregate & user endpoints. Legacy: user status is an untyped int",
16
+ bigBallOfMud: true,
17
+ team: platformTeam,
18
+ });
19
+ ```
20
+
21
+ ## Attributes backed by value objects, relations with cardinality, invariants on attributes
22
+
23
+ ```ts
24
+ petRoot.addAttribute("id", { type: "int64", identity: true });
25
+ petRoot.addAttribute("status", { type: "PetStatus", valueobject: petStatusVO });
26
+ petRoot.uses(categoryVO, "categorized-as", "0..1");
27
+ petRoot.uses(photoUrlVO, "has-photo", "1..*");
28
+ petAgg
29
+ .addInvariant("NameRequired", { description: "Pet.name must be non-empty" })
30
+ .constrains(petRoot.attributes.get("name")!);
31
+ ```
32
+
33
+ ## A cross-aggregate reference by identity to the other root
34
+
35
+ ```ts
36
+ orderRoot.references(petRoot, "for-pet", "1");
37
+ ```
38
+
39
+ ## Published events with a payload schema, and an internal operation that raises one
40
+
41
+ ```ts
42
+ const petStatusChangedSchema = catalogBC.addSchema("PetStatusChanged");
43
+ petStatusChangedSchema.addAttribute("petId", { type: "int64", identity: true });
44
+
45
+ const petStatusChanged = petAgg.provides("PetStatusChanged", {
46
+ description: "Pet status changed (available|pending|sold)",
47
+ type: "event",
48
+ pattern: "published-language",
49
+ schema: petStatusChangedSchema,
50
+ });
51
+ const _changePetStatus = petAgg
52
+ .provides("ChangePetStatus", {
53
+ description: "Move a pet between available, pending and sold",
54
+ type: "operation",
55
+ internal: true,
56
+ schema: petStatusChangedSchema,
57
+ })
58
+ .raises(petStatusChanged);
59
+ ```
60
+
61
+ ## An open-host application service whose operations raise the aggregate's events
62
+
63
+ ```ts
64
+ const petApp = catalogBC.addService("PetApp", {
65
+ description: "Open-host service for /pet endpoints",
66
+ type: "application",
67
+ });
68
+ const _addPetOp = petApp
69
+ .provides("AddPet", {
70
+ description: "POST /pet",
71
+ type: "operation",
72
+ pattern: "open-host-service",
73
+ schema: registerPetSchema,
74
+ })
75
+ .raises(petRegistered);
76
+ ```
77
+
78
+ ## A consumption through an anti-corruption layer, and the relationship that explains it
79
+
80
+ ```ts
81
+ orderApp.consumes(getPetSummaryOp, { pattern: "anti-corruption-layer" });
82
+
83
+ salesBC.downstreamOf(catalogBC, {
84
+ type: "customer-supplier",
85
+ upstreamRoles: ["open-host-service"],
86
+ downstreamRoles: ["anti-corruption-layer"],
87
+ description: "Sales needs pet availability; Catalog commits to the summary contract",
88
+ });
89
+ ```
90
+
91
+ ## Separate ways, on purpose
92
+
93
+ ```ts
94
+ identityBC.separateWaysFrom(
95
+ salesBC,
96
+ "Orders are anonymous in Petstore v3; no integration by design",
97
+ );
98
+ ```
99
+
100
+ ## A policy reacting to events from two contexts
101
+
102
+ ```ts
103
+ salesBC
104
+ .addPolicy("Approve when pet available", {
105
+ description: "When a pet becomes available and an order for it is placed, approve the order",
106
+ })
107
+ .on(petStatusChanged, orderPlaced)
108
+ .then(approveOrder);
109
+ ```
110
+
111
+ ## Conformist consumptions feeding a projection
112
+
113
+ ```ts
114
+ inventoryAgg.consumes(petStatusChanged, { pattern: "conformist" });
115
+ inventoryAgg.consumes(orderApproved, { pattern: "conformist" });
116
+ ```
117
+
118
+ ## Glossary terms embodied by model elements
119
+
120
+ ```ts
121
+ catalogBC.addTerm("Category", {
122
+ definition: "The kind of animal a pet is, such as Dogs or Cats",
123
+ aliases: ["Species"],
124
+ embodiedBy: categoryVO,
125
+ });
126
+ ```
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ // Validates one or more ODS workspace JSON files.
3
+ // Usage: node validate.mjs .ods/petstore.json [.ods/other.json ...]
4
+ // Exits 1 when a file fails to load or has an error-level diagnostic.
5
+ import { readFileSync } from "node:fs";
6
+ import { createRequire } from "node:module";
7
+
8
+ const require = createRequire(`${process.cwd()}/`);
9
+ let Workspace;
10
+ try {
11
+ ({ Workspace } = require("@open-domain-specification/core"));
12
+ } catch {
13
+ console.error(
14
+ "@open-domain-specification/core is not installed here. Run from the project root, install it (npm i -D @open-domain-specification/core), or use: npx -p @open-domain-specification/core node validate.mjs <file>",
15
+ );
16
+ process.exit(2);
17
+ }
18
+
19
+ let failed = false;
20
+ for (const file of process.argv.slice(2)) {
21
+ let workspace;
22
+ try {
23
+ workspace = Workspace.fromSchema(JSON.parse(readFileSync(file, "utf8")));
24
+ } catch (error) {
25
+ console.log(
26
+ `[load-error] ${file}: ${error instanceof Error ? error.message : error}`,
27
+ );
28
+ failed = true;
29
+ continue;
30
+ }
31
+ const diagnostics = workspace.validate();
32
+ console.log(`${file}: ${diagnostics.length} diagnostic(s)`);
33
+ for (const d of diagnostics) {
34
+ console.log(` [${d.severity}] ${d.rule}: ${d.message} (${d.ref})`);
35
+ if (d.severity === "error") failed = true;
36
+ }
37
+ }
38
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,56 @@
1
+ # DDD terms in one sentence each
2
+
3
+ Use these the first time a term comes up, filling the example with the user's own words.
4
+ Never repeat an explanation, and never explain a term the user already used correctly.
5
+
6
+ - **Domain** — the whole area of business the system exists for, e.g. "running the pet store".
7
+ - **Subdomain** — one slice of that problem, e.g. "the catalogue" or "taking orders"; calling it
8
+ *core* only marks where your competitive effort goes, *supporting* means needed but ordinary,
9
+ *generic* means you would buy it.
10
+ - **Bounded context** — a boundary inside which every word has one exact meaning; your billing
11
+ "Customer" and your support "Customer" being different things is why they get separate
12
+ contexts.
13
+ - **Ubiquitous language / glossary** — the words a context uses, written down once so code,
14
+ conversations and documents all mean the same thing by "Order".
15
+ - **Team ownership** — the people who decide what a context means and how it changes.
16
+ - **Big ball of mud** — a context whose model nobody fully controls, flagged so that anything
17
+ talking to it translates rather than trusts.
18
+ - **Entity** — something that matters because of *which one* it is, like this particular order,
19
+ so it carries an identity.
20
+ - **Value object** — something that matters only by its values, like an address; two with the
21
+ same values are interchangeable.
22
+ - **Attribute** — one piece of information an entity, value object or message carries; the
23
+ identity attribute is the one that tells two entities apart.
24
+ - **Aggregate** — the cluster of things you change together and check rules across, named after
25
+ its *root*, the one thing you go through to change any of it; the order and its lines.
26
+ - **Invariant** — a rule that must always hold inside an aggregate, such as "quantity is never
27
+ zero".
28
+ - **Relation** — how one thing points at another: *includes* for parts that cannot exist alone,
29
+ *uses* for values it carries, *references* for another aggregate's root by identity.
30
+ - **Cardinality** — how many of the other thing: exactly one, at most one, any number, at least
31
+ one.
32
+ - **Operation** — something you can ask a part of the system to do, like "place an order"; in
33
+ conversation people often say *command*.
34
+ - **Event** — a fact that already happened, stated in the past tense, like "order placed", that
35
+ other parts can react to.
36
+ - **Consumable** — an operation or event that a part offers, and **consumption** is another part
37
+ using it.
38
+ - **Schema** — the shape of the information that travels with an operation or event.
39
+ - **Policy** — a rule of the form "when this event happens, do that operation", possibly across
40
+ contexts.
41
+ - **Application service** — the part that fronts an API or a screen and turns requests into
42
+ operations on aggregates.
43
+ - **Domain service** — business logic that does not belong to any single thing, like pricing
44
+ across several orders.
45
+ - **Upstream / downstream** — the side that is depended on, and the side that depends on it.
46
+ - **Customer-supplier** — a dependency where the downstream side gets a say before the upstream
47
+ side changes things.
48
+ - **Partnership** — two contexts whose teams plan and release together.
49
+ - **Shared kernel** — code or data two contexts both own and change.
50
+ - **Separate ways** — a deliberate decision that two contexts will not integrate.
51
+ - **Open host service** — the upstream side offers a documented API for anyone to use.
52
+ - **Published language** — the upstream side offers a documented message format everyone
53
+ agrees on.
54
+ - **Conformist** — the downstream side takes the upstream model as it comes.
55
+ - **Anti-corruption layer** — the downstream side copies and reshapes what it receives into its
56
+ own terms, so the upstream model cannot leak in.
@@ -0,0 +1,45 @@
1
+ # DSL reference (`@open-domain-specification/core`)
2
+
3
+ Every class is created through its parent and registers itself there, so `parent.addX(...)`
4
+ is the only call needed. Every attributes object accepts an optional `id` to fix the id
5
+ independently of the name.
6
+
7
+ | Receiver | Method | Creates / does |
8
+ |---|---|---|
9
+ | — | `new Workspace(name, { odsVersion, description, version, homepage?, logoUrl?, primaryColor?, id? })` | the workspace |
10
+ | `Workspace` | `addDomain(name, { description })` | a domain |
11
+ | `Workspace` | `addTeam(name, { description?, homepage? })` | a team |
12
+ | `Workspace` | `addBoundedContext(name, { description, subdomains?, bigBallOfMud?, team? })` | a context serving zero or more subdomains |
13
+ | `Workspace` | `addRelationship({...})` | a relationship; prefer the context helpers below |
14
+ | `Workspace` | `validate()` | the diagnostics list |
15
+ | `Workspace` | `toSchema()` / `Workspace.fromSchema(json)` | serialise / load |
16
+ | `Domain` | `addSubdomain(name, { type, description })` | a subdomain; `type` is `"core" \| "supporting" \| "generic"` |
17
+ | `Subdomain` | `addBoundedcontext(name, { description, bigBallOfMud?, team? })` | a context serving this subdomain |
18
+ | `BoundedContext` | `serves(subdomain)` | adds a served subdomain |
19
+ | `BoundedContext` | `ownedBy(team)` | sets the owning team |
20
+ | `BoundedContext` | `upstreamOf(other, { type?, upstreamRoles?, downstreamRoles?, description? })` | directed relationship, this side upstream; `type` defaults to `"upstream-downstream"`, or `"customer-supplier"` |
21
+ | `BoundedContext` | `downstreamOf(other, options)` | the same, this side downstream |
22
+ | `BoundedContext` | `partnerOf(other, description?)` | partnership |
23
+ | `BoundedContext` | `sharesKernelWith(other, description?)` | shared kernel |
24
+ | `BoundedContext` | `separateWaysFrom(other, description?)` | separate ways |
25
+ | `BoundedContext` | `addAggregate(name, { description })` | an aggregate |
26
+ | `BoundedContext` | `addService(name, { type, description })` | a service; `type` is `"application" \| "domain"` |
27
+ | `BoundedContext` | `addPolicy(name, { description })` | a policy; chain `.on(...events).then(...operations)` |
28
+ | `BoundedContext` | `addTerm(name, { definition, aliases?, embodiedBy? })` | a glossary term; or chain `.embody(element)` |
29
+ | `BoundedContext` | `addSchema(name, { description? })` | a payload schema; add fields with `addAttribute` |
30
+ | `Aggregate` | `addRootEntity(name, { description })` | the root entity |
31
+ | `Aggregate` | `addEntity(name, { description, root? })` | an entity |
32
+ | `Aggregate` | `addValueObject(name, { description })` | a value object |
33
+ | `Aggregate` | `addInvariant(name, { description })` | an invariant; chain `.constrains(...entities, valueObjects or attributes)` |
34
+ | `Aggregate`, `Service` | `provides(name, { type, description, pattern?, internal?, schema? })` | a consumable; `type` is `"event" \| "operation"`, `pattern` is `"open-host-service" \| "published-language"` |
35
+ | `Aggregate`, `Service` | `consumes(consumable, { pattern? })` | a consumption; `pattern` is `"conformist" \| "anti-corruption-layer"` |
36
+ | `Consumable` | `raises(...events)` | the events an operation raises |
37
+ | `Entity`, `ValueObject`, `DataSchema` | `addAttribute(name, { type, description?, identity?, valueobject? })` | an attribute; `type` is free text |
38
+ | `Entity`, `ValueObject` | `uses(target, label, cardinality?)` | a `uses` relation |
39
+ | `Entity`, `ValueObject` | `includes(target, label, cardinality?)` | an `includes` relation |
40
+ | `Entity`, `ValueObject` | `references(target, label, cardinality?)` | a `references` relation; across aggregates target the root |
41
+ | `Entity`, `ValueObject` | `addRelation(target, { relation, label?, cardinality? })` | any relation explicitly |
42
+ | `Entity` | `.attributes.get("name")` | look an attribute up, e.g. to constrain it |
43
+
44
+ `cardinality` is `"1" | "0..1" | "*" | "1..*"`. Chainable methods (`raises`, `on`, `then`,
45
+ `constrains`, `embody`, `serves`, `ownedBy`) return their receiver.
@@ -0,0 +1,63 @@
1
+ # DSL mode
2
+
3
+ The TypeScript source is the artefact; the JSON under `.ods/` is generated from it. Edit the
4
+ source, run the generator, read the diagnostics it prints.
5
+
6
+ ## Find the generator
7
+
8
+ Look for a file that imports `Workspace` from `@open-domain-specification/core`, builds the
9
+ model, and writes `workspace.toSchema()` to disk. The canonical shape (from the ODS example
10
+ package) is:
11
+
12
+ ```ts
13
+ import fs from "node:fs";
14
+ import { workspace } from "./petstore/workspace.ts";
15
+
16
+ for (const d of workspace.validate()) {
17
+ console.log(`[${d.severity}] ${d.rule}: ${d.message} (${d.ref})`);
18
+ }
19
+
20
+ fs.mkdirSync(".ods", { recursive: true });
21
+ fs.writeFileSync(
22
+ ".ods/petstore.json",
23
+ JSON.stringify({ $schema: "./schema.json", ...workspace.toSchema() }, null, 2),
24
+ );
25
+ ```
26
+
27
+ `package.json` usually has a script for it (`build`, `ods`, `generate`, `model`). Node 24 runs
28
+ `.ts` files directly; on older Node use `npx tsx <file>`.
29
+
30
+ ## Loop
31
+
32
+ 1. Edit the model source. Keep the file's existing sections and ordering (domains, teams,
33
+ contexts, then one section per context).
34
+ 2. Run the generator. It validates and rewrites the JSON.
35
+ 3. Read every `[error]` and `[warning]` line and explain it to the user with
36
+ `validation-rules.md`.
37
+ 4. Never hand-edit the emitted JSON; the next run overwrites it. If the user edits it, tell
38
+ them and offer to port the change into the source.
39
+
40
+ If the generator does not print diagnostics, add the four-line loop above before the write.
41
+ `toSchema()` does not emit `$schema`; spread it back in exactly as shown, so editors and the
42
+ VS Code extension keep the file associated with `schema.json`.
43
+
44
+ ## Ids and renames
45
+
46
+ Ids are derived from names with `snake_case` unless `id` is passed. Because ids are the JSON
47
+ keys and the ref segments, renaming an element by changing its name silently changes its id
48
+ and breaks anything outside the source that points at it (documentation links, bookmarks,
49
+ other files). When renaming, pass the old id explicitly:
50
+
51
+ ```ts
52
+ // was: catalogBC.addAggregate("Pet", {...})
53
+ catalogBC.addAggregate("Listed Pet", { id: "pet", description: "..." });
54
+ ```
55
+
56
+ ## Conventions from the example
57
+
58
+ - Name the variables after the element and its kind (`petAgg`, `petRoot`, `categoryVO`,
59
+ `petApp`), so refs read naturally in the code.
60
+ - Prefix a variable with `_` when the element is kept only for its side effect on the model
61
+ (an operation nobody references again).
62
+ - Create all consumables before the policies and consumptions that point at them.
63
+ - Full DSL surface: `dsl-api.md`. Patterns worth copying: `examples/petstore.md`.
@@ -0,0 +1,108 @@
1
+ # Interview playbook
2
+
3
+ You are the facilitator. The user knows their system and their business; you know DDD. Your
4
+ job is to get the model out of their head without making them learn the vocabulary first.
5
+
6
+ ## Ground rules
7
+
8
+ - One question per turn. Wait for the answer.
9
+ - No DDD word before its one-sentence explanation (see `ddd-glossary.md`), and each term is
10
+ explained once.
11
+ - After every answer, paraphrase it as the element you would record: "So I'd note ... right?"
12
+ - Write the increment as soon as a context or an aggregate is stable. Do not wait until the
13
+ whole interview is done; a model in the file beats a model in the chat.
14
+ - Skip any phase the existing workspace already covers. Read first, ask second.
15
+ - Keep the user's words. Descriptions and glossary definitions are written in their language,
16
+ not in DDD language.
17
+
18
+ ## Phase A: orientation (produces the Workspace)
19
+
20
+ - "In one or two sentences, what does this system do, and for whom?" → `name`, `description`.
21
+ - "Is there a homepage or logo I should link?" → `homepage`, `logoUrl` (skip if none).
22
+
23
+ ## Phase B: the problem space (produces Domains and Subdomains with a type)
24
+
25
+ - "What are the big areas of the business this covers? Think of the headings you would put
26
+ on a whiteboard." → domains.
27
+ - Per area: "What distinct jobs sit inside that area?" → subdomains.
28
+ - Per subdomain: "Is this something that makes you different from competitors, something you
29
+ need but any sensible way of doing it is fine, or something you would happily buy off the
30
+ shelf?" → `core` / `supporting` / `generic`.
31
+ - Explain once: a subdomain is one slice of the problem; calling it core only marks where your
32
+ competitive effort goes.
33
+
34
+ ## Phase C: ownership (produces Teams, Bounded Contexts, `subdomains`, `bigBallOfMud`)
35
+
36
+ - "Which teams or people work on this, and which parts does each look after?" → teams, and a
37
+ candidate context per part.
38
+ - "If two teams both say <word>, do they mean exactly the same thing?" If not, that is two
39
+ contexts. Explain once: a bounded context is a boundary inside which every word has one
40
+ exact meaning; your billing "Customer" and your support "Customer" being different is why
41
+ they get separate contexts.
42
+ - "Which of the jobs from before does each part serve?" → `subdomains` refs. One context may
43
+ serve several.
44
+ - "Is any of these an old system that nobody fully understands, where the data model is a
45
+ mess?" → `bigBallOfMud: true`. Explain: we flag it so anything talking to it knows to
46
+ translate rather than trust.
47
+
48
+ ## Phase D: the integration map (produces Relationships and seeds consumptions)
49
+
50
+ - "Which parts talk to each other? For each pair, who depends on whom?" → `upstream-downstream`
51
+ with the depended-on side upstream.
52
+ - "When the upstream team changes something, does the downstream team get a say beforehand?"
53
+ Yes → `customer-supplier`.
54
+ - "Do those two teams plan and release together, as one?" → `partnership`.
55
+ - "Do they share actual code or tables that both change?" → `shared-kernel`.
56
+ - "Are there two parts that you have decided, on purpose, should never integrate?" →
57
+ `separate-ways`.
58
+ - "How does the downstream side take the data: as it comes, or does it copy and reshape it
59
+ into its own terms?" → `conformist` / `anti-corruption-layer`. Goes on `downstreamRoles`
60
+ and on each consumption's `pattern`.
61
+ - "Does the upstream side publish a documented API, or a documented message format?" →
62
+ `open-host-service` / `published-language`. Goes on `upstreamRoles` and on each exposed
63
+ consumable's `pattern`.
64
+
65
+ ## Phase E: inside one context (produces Aggregates, Entities, Value Objects, Invariants, Glossary)
66
+
67
+ Repeat for each context the user wants detailed. Ask which one to start with.
68
+
69
+ - "Inside <context>, what are the things people talk about? Just list the nouns." → candidate
70
+ entities and value objects; every noun becomes a glossary term with the user's definition.
71
+ - Per noun: "If two of these had identical details, would they still be two different things?"
72
+ Yes → entity; no → value object. Explain once: an entity matters because of which one it is
73
+ (this order, not that one); a value object matters only by its values (an address).
74
+ - "What identifies it: an order number, an email?" → an attribute with `identity: true`.
75
+ - "What details does it carry?" → attributes, with `type` in the user's words.
76
+ - "Which of these do you always change or check together? What must be true across all of
77
+ them at once?" → the aggregate boundary. The thing they state the rule about is the root.
78
+ Explain once: an aggregate is the cluster you change together and check rules across; the
79
+ root is the one you name it after.
80
+ - "What must never be allowed to happen to a <root>?" → invariants, each constraining the
81
+ entity, value object or attribute it is about.
82
+ - "Does a <root> point at things in another cluster, for example an order pointing at a
83
+ product?" → `references` to that cluster's root; ask "one or many?" for cardinality.
84
+ - "Does it contain things that cannot exist without it?" → `includes`.
85
+ - "Does it use a value like an address, money or a status?" → `uses`.
86
+
87
+ ## Phase F: behaviour (produces Consumables, `raises`, Policies, Schemas)
88
+
89
+ - "What can someone ask this part to do?" → `operation` consumables. Put an API entry point on
90
+ an application service, and a state change of one aggregate on that aggregate.
91
+ - "When that happens, what fact would you announce to the rest of the business?" → `event`
92
+ consumable, linked from the operation with `raises`. Events are past tense.
93
+ - "Is that something only this part uses, or would other parts care?" → `internal: true`, or
94
+ an upstream `pattern`.
95
+ - "What information travels with that announcement or request?" → a schema on the context,
96
+ attached with `schema`.
97
+ - "When <event> happens, what do you then do automatically?" → a policy with `on` the event
98
+ and `then` the operation. Either side may live in another context.
99
+ - "Who outside this part listens for <event>?" → a consumption on their aggregate or service,
100
+ with a downstream `pattern`.
101
+ - Close: "Which of the words we used should I define, and does each map to one of the things
102
+ we modelled?" → glossary terms with `embodiedBy`.
103
+
104
+ ## Phase G: validate and reflect
105
+
106
+ Run validation. Explain each diagnostic in one plain sentence, propose the fix, and ask before
107
+ applying fixes for warnings. Then summarise what changed, in the user's words, and ask what to
108
+ model next.
@@ -0,0 +1,52 @@
1
+ # JSON mode
2
+
3
+ The workspace files are the artefact. Each `.ods/*.json` file is one complete workspace, and
4
+ the VS Code extension, the docs generator and anyone else load it with `Workspace.fromSchema`.
5
+
6
+ ## Files
7
+
8
+ - `.ods/` (or the folder named by the VS Code setting `ods.folder`) at the project root.
9
+ - `.ods/schema.json`: the JSON Schema, written by the extension (`ODS: Write schema.json`).
10
+ Never edit it. If it is missing, copy it from
11
+ `node_modules/@open-domain-specification/core/dist/workspace.schema.json`.
12
+ - `.ods/<workspace-id>.json`: one workspace per file. The first key is
13
+ `"$schema": "./schema.json"`; the loader ignores it, editors use it for completion.
14
+ - Keep the file's `id` equal to its basename, and `odsVersion` equal to the other files' (use
15
+ `"1.0.0"` for a first file).
16
+
17
+ The smallest valid file is `examples/minimal.ods.json`. Copy it when creating a workspace, then
18
+ grow it.
19
+
20
+ ## Editing rules
21
+
22
+ - The schema is strict: every required field is present even when empty, and unknown fields
23
+ are rejected. `references/model-reference.md` lists them.
24
+ - Ids are the object keys. Create them as `snake_case` of the name, then never change them.
25
+ Renaming is changing `name`.
26
+ - Every `$ref` follows the grammar at the end of `model-reference.md` and points at something
27
+ that exists. A dangling ref makes the whole file fail to load; the extension then shows
28
+ "Workspace file could not be loaded" instead of diagnostics.
29
+ - Preserve the key order and two-space indentation of the file so diffs stay readable.
30
+ - Prefer several small edits, each followed by validation, over one large rewrite.
31
+
32
+ ## Validation
33
+
34
+ There is no CLI. Run `examples/validate.mjs` from the project root:
35
+
36
+ ```sh
37
+ node .claude/skills/ods-authoring/examples/validate.mjs .ods/petstore.json
38
+ ```
39
+
40
+ Or inline:
41
+
42
+ ```sh
43
+ node -e 'const {Workspace}=require("@open-domain-specification/core");const f=process.argv[1];const ws=Workspace.fromSchema(JSON.parse(require("fs").readFileSync(f,"utf8")));for(const d of ws.validate())console.log(`[${d.severity}] ${d.rule}: ${d.message} (${d.ref})`)' .ods/petstore.json
44
+ ```
45
+
46
+ If `@open-domain-specification/core` is not installed, prefix with
47
+ `npx -p @open-domain-specification/core` or install it as a devDependency. The VS Code Problems
48
+ panel shows the same diagnostics (source `ods`, code = rule id) and updates on save.
49
+
50
+ ## Several workspace files
51
+
52
+ A `.ods` folder may hold several files. Treat each as its own workspace; refs never cross files.