kalup 0.3.0 → 0.5.0

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.
Files changed (41) hide show
  1. package/README.md +4 -4
  2. package/dist/{commands-T4j_GVBD.mjs → commands-D_Q8-jc5.mjs} +7123 -2131
  3. package/dist/commands.d.mts +1 -1
  4. package/dist/commands.mjs +1 -1
  5. package/dist/{context-C1tH5K0x.d.mts → context-0mrva_6H.d.mts} +84 -0
  6. package/dist/{host-CDDeLV5T.mjs → host-BkBYlh_7.mjs} +1 -1
  7. package/dist/host.d.mts +1 -1
  8. package/dist/host.mjs +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/schemas/ir-1.schema.json +681 -116
  11. package/dist/schemas/plan-1.schema.json +18 -0
  12. package/dist/schemas/state-1.schema.json +80 -18
  13. package/docs/apply.md +10 -5
  14. package/docs/config.md +64 -9
  15. package/docs/dictionary.md +3 -3
  16. package/docs/errors/E_ASSOCIATION_FIELD.md +21 -0
  17. package/docs/errors/E_ASSOCIATION_NAME.md +17 -0
  18. package/docs/errors/E_BLUEPRINT_REQUIRES.md +3 -3
  19. package/docs/errors/E_DUPLICATE_LABEL.md +24 -0
  20. package/docs/errors/E_MISSING_EXPORT.md +2 -2
  21. package/docs/errors/E_PIPELINE_FIELD.md +23 -0
  22. package/docs/errors/E_PIPELINE_ID.md +17 -0
  23. package/docs/errors/E_PIPELINE_STAGES.md +17 -0
  24. package/docs/errors/E_PLAN_DELETE.md +1 -1
  25. package/docs/errors/E_PLAN_INVALID.md +1 -1
  26. package/docs/errors/E_PLAN_RISK.md +1 -1
  27. package/docs/errors/E_PREVENT_DESTROY.md +1 -1
  28. package/docs/errors/E_TOMBSTONE_ADDRESS.md +3 -3
  29. package/docs/errors/E_TOMBSTONE_CONFLICT.md +1 -1
  30. package/docs/errors/E_UNKNOWN_OBJECT.md +1 -1
  31. package/docs/errors/E_UNSUPPORTED_FILE.md +3 -3
  32. package/docs/errors/E_WRITE_IN_READ_MODE.md +1 -1
  33. package/docs/errors/E_WRITE_NOT_ALLOWED.md +1 -1
  34. package/docs/errors/W_OBJECT_FIELD.md +25 -0
  35. package/docs/errors/W_OBJECT_PROPERTY.md +17 -0
  36. package/docs/plan.md +40 -8
  37. package/docs/pull.md +14 -7
  38. package/docs/rm.md +8 -5
  39. package/docs/snapshot.md +3 -3
  40. package/docs/targets.md +2 -2
  41. package/package.json +2 -2
@@ -469,6 +469,24 @@
469
469
  "description": "Logical form, $ref included, resolved at apply. A create holds the full definition.",
470
470
  "type": "object"
471
471
  },
472
+ "stages": {
473
+ "description": "A pipeline create only: the stages it carries, in display order, each with its full definition. HubSpot refuses a pipeline without a stage.",
474
+ "type": "array",
475
+ "items": {
476
+ "type": "object",
477
+ "required": ["address", "desired"],
478
+ "properties": {
479
+ "address": { "$ref": "#/$defs/address" },
480
+ "desired": { "type": "object" }
481
+ },
482
+ "additionalProperties": false
483
+ }
484
+ },
485
+ "stageLabels": {
486
+ "description": "Display only, never approved: the label of each stage a pipeline step's stage order names, config's else the portal's, so the plan text shows labels and not IDs.",
487
+ "type": "object",
488
+ "additionalProperties": { "type": "string" }
489
+ },
472
490
  "ignoreChanges": {
473
491
  "description": "Create only: set on create, released after.",
474
492
  "type": "array",
@@ -6,7 +6,9 @@
6
6
  "type": "object",
7
7
  "required": ["format", "lineage", "serial", "portalId", "resources"],
8
8
  "properties": {
9
- "format": { "const": "kalup.state/1" },
9
+ "format": {
10
+ "const": "kalup.state/1"
11
+ },
10
12
  "lineage": {
11
13
  "description": "16 lowercase hex characters, new on rebuild and rebind. A plan made against another lineage is refused.",
12
14
  "type": "string",
@@ -26,17 +28,33 @@
26
28
  "type": "object",
27
29
  "required": ["planId", "writesHash", "actor", "at", "outcome"],
28
30
  "properties": {
29
- "planId": { "type": "string", "pattern": "^pl_[0-9a-f]{12}$" },
30
- "writesHash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
31
- "actor": { "type": "string" },
32
- "at": { "type": "string" },
33
- "outcome": { "enum": ["running", "done", "partial", "uncertain"] }
31
+ "planId": {
32
+ "type": "string",
33
+ "pattern": "^pl_[0-9a-f]{12}$"
34
+ },
35
+ "writesHash": {
36
+ "type": "string",
37
+ "pattern": "^sha256:[0-9a-f]{64}$"
38
+ },
39
+ "actor": {
40
+ "type": "string"
41
+ },
42
+ "at": {
43
+ "type": "string"
44
+ },
45
+ "outcome": {
46
+ "enum": ["running", "done", "partial", "uncertain"]
47
+ }
34
48
  },
35
49
  "additionalProperties": false
36
50
  },
37
51
  "resources": {
38
52
  "type": "object",
39
- "patternProperties": { "^[a-z]+:\\S+$": { "$ref": "#/$defs/resourceState" } },
53
+ "patternProperties": {
54
+ "^[a-z]+:\\S+$": {
55
+ "$ref": "#/$defs/resourceState"
56
+ }
57
+ },
40
58
  "additionalProperties": false
41
59
  }
42
60
  },
@@ -54,14 +72,32 @@
54
72
  "description": "The portal name the entry owns. It owns the resource only while this equals the name the address resolves to. null for runbook-only types.",
55
73
  "type": ["string", "null"]
56
74
  },
57
- "via": { "description": "Transport of the last write.", "type": "string" },
58
- "normVersion": { "description": "Normalizer version of the type that wrote base.", "type": "integer" },
59
- "base": { "$ref": "#/$defs/base" },
60
- "baseHash": { "description": "Replaces base for opaque payloads and runbook types.", "type": "string" },
75
+ "via": {
76
+ "description": "Transport of the last write.",
77
+ "type": "string"
78
+ },
79
+ "normVersion": {
80
+ "description": "Normalizer version of the type that wrote base.",
81
+ "type": "integer"
82
+ },
83
+ "base": {
84
+ "$ref": "#/$defs/base"
85
+ },
86
+ "baseHash": {
87
+ "description": "Replaces base for opaque payloads and runbook types.",
88
+ "type": "string"
89
+ },
61
90
  "attested": {
62
91
  "type": "object",
63
92
  "required": ["by", "at"],
64
- "properties": { "by": { "type": "string" }, "at": { "type": "string" } },
93
+ "properties": {
94
+ "by": {
95
+ "type": "string"
96
+ },
97
+ "at": {
98
+ "type": "string"
99
+ }
100
+ },
65
101
  "additionalProperties": false
66
102
  },
67
103
  "rewrites": {
@@ -70,9 +106,19 @@
70
106
  "additionalProperties": {
71
107
  "type": "object",
72
108
  "required": ["sent", "stored"],
73
- "properties": { "sent": {}, "stored": {} },
109
+ "properties": {
110
+ "sent": {},
111
+ "stored": {}
112
+ },
74
113
  "additionalProperties": false
75
114
  }
115
+ },
116
+ "typeIds": {
117
+ "description": "An association's HubSpot type IDs in this portal, its direction's first: they name it while HubSpot's schema read does not list its name yet.",
118
+ "type": "array",
119
+ "items": {
120
+ "type": "integer"
121
+ }
76
122
  }
77
123
  },
78
124
  "additionalProperties": false
@@ -81,16 +127,32 @@
81
127
  "description": "Per owned unit, the value config and portal last agreed on; possibly partial. Scalar units by field name. options: a map keyed by option value whose members hold the agreed label, hidden and description; a member with no fields means only its membership is agreed. optionsOrder: the agreed order of the members both sides held.",
82
128
  "type": "object",
83
129
  "properties": {
84
- "options": { "type": "object", "additionalProperties": { "$ref": "#/$defs/baseOption" } },
85
- "optionsOrder": { "type": "array", "items": { "type": "string" } }
130
+ "options": {
131
+ "type": "object",
132
+ "additionalProperties": {
133
+ "$ref": "#/$defs/baseOption"
134
+ }
135
+ },
136
+ "optionsOrder": {
137
+ "type": "array",
138
+ "items": {
139
+ "type": "string"
140
+ }
141
+ }
86
142
  }
87
143
  },
88
144
  "baseOption": {
89
145
  "type": "object",
90
146
  "properties": {
91
- "label": { "type": "string" },
92
- "hidden": { "type": "boolean" },
93
- "description": { "type": "string" }
147
+ "label": {
148
+ "type": "string"
149
+ },
150
+ "hidden": {
151
+ "type": "boolean"
152
+ },
153
+ "description": {
154
+ "type": "string"
155
+ }
94
156
  },
95
157
  "additionalProperties": false
96
158
  }
package/docs/apply.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Apply
2
2
 
3
- `kalup apply <plan-file> [--yes | --approve <writesHash>]` applies a plan saved by `kalup plan --out` to the target it names. `kalup apply [--target <name>] [--take config <selector>] [--yes]` plans the target now and applies that plan through the same checks: at a terminal it prints the whole plan first, as `plan` does, then asks. Apply writes property groups and properties, on custom objects too, and never a custom object schema.
3
+ `kalup apply <plan-file> [--yes | --approve <writesHash>]` applies a plan saved by `kalup plan --out` to the target it names. `kalup apply [--target <name>] [--take config <selector>] [--yes]` plans the target now and applies that plan through the same checks: at a terminal it prints the whole plan first, as `plan` does, then asks. Apply writes custom objects, property groups, properties, pipelines, stages and associations.
4
4
 
5
5
  This page is the reference. For the walk-through with examples, see [kalup apply](https://kalup.dev/docs/commands/apply) on the website.
6
6
 
@@ -25,21 +25,26 @@ Before any write, in order:
25
25
  3. The write key (`credentials.write`, else the read key) passes the portal guard. Every request, reads included, uses it.
26
26
  4. The policy equals the plan's: `protected`, `drift`, `adopt`, `allowDestroy`, `yesLimit` and the objects in takeover (`E_POLICY_CHANGED`); each step's API version is current and unexpired, and the normalizer versions match (`E_PLAN_VERSION`).
27
27
  5. Every step but a release is on an object `kalup.config.ts` declares, the name bindings match the target's name overrides, and no two steps but releases resolve to one portal resource (`E_BINDING_CHANGED`).
28
- 6. Each delete has a `destroy` tombstone in `hubspot/removed.ts` or takeover's leave (takeover mode, in the pull scope, not excluded), is gone from config, and no address in config names its portal resource, read as data (`E_PLAN_DELETE`). The object files also tell takeover's option removals from config's own.
28
+ 6. Each delete has a `destroy` tombstone in `hubspot/removed.ts` or takeover's leave (takeover mode, in the pull scope, not excluded), a delete labelled `takeover` has takeover's leave whatever `hubspot/removed.ts` says, is gone from config, and no address in config names its portal resource, read as data (`E_PLAN_DELETE`). The object files also tell takeover's option removals from config's own.
29
29
  7. Approval, then the portal lock (`E_LOCKED`).
30
30
  8. State: a plan already applied with outcome `done` exits 0 ("Already applied"); otherwise lineage and serial equal the plan's (`E_STATE_CHANGED`).
31
- 9. A fresh read of each object the plan changes, and of the schemas list for a custom object (`E_INCOMPLETE` on a 403, `E_BINDING_CHANGED` for another type ID). Every `expect` must hold (`E_PLAN_STALE`), a delete's covering each field its base holds. Kalup derives each step's risk, labels and blocked status again (`E_PLAN_RISK` when the plan states less); a takeover removal needs `allowDestroy`, and never takes a HubSpot-defined property.
31
+ 9. A fresh read of each object the plan changes, its pipelines when a step touches one, and of the schemas list for a custom object (`E_INCOMPLETE` on a 403, `E_BINDING_CHANGED` for another type ID). Every `expect` must hold (`E_PLAN_STALE`), a delete's covering each field its base holds. Kalup derives each step's risk, labels and blocked status again (`E_PLAN_RISK` when the plan states less); a takeover removal needs `allowDestroy`, and never takes a HubSpot-defined property; a delete labelled `takeover` is a property or group on an object in takeover mode that `hubspot/removed.ts` neither names nor covers, never a custom object, pipeline, stage or association; a custom object's display fields name only properties HubSpot gives every custom object, the read found, or the plan creates.
32
32
  10. Three calls per write plus the reads use at most half of HubSpot's daily remainder (`E_BUDGET`).
33
33
 
34
34
  ## Running the steps
35
35
 
36
- Apply records `lastApply.outcome: running`, then runs the steps one at a time: groups, properties, releases, then deletes, properties before groups. Each write reads the resource again and compares it with `expect`, builds the request from that read, sends it once, and reads it back for up to 60 seconds, saying so on stderr after a few seconds.
36
+ Apply records `lastApply.outcome: running`, then runs the steps one at a time: custom object creates, groups, properties, the display fields of new custom objects, custom object updates, pipeline creates, stage creates and updates (those that close a stage first, so a ticket pipeline keeps a closed stage), pipeline updates, plain association creates, label creates, label updates, releases, then deletes: association labels, plain associations, properties, groups, stages, pipelines, custom objects. Each write reads the resource again and compares it with `expect`, builds the request from that read, sends it once, and reads it back for up to 60 seconds, saying so on stderr after a few seconds.
37
37
 
38
38
  - A property update sends the approved fields with the live `type` and `fieldType`; options go as the full live list with the approved changes, new ones last.
39
+ - A pipeline create sends its stages with it. A stage update sends only the approved fields. A stage order change moves each stage that is out of place onto the slot of the stage it follows, one request each, reading the pipeline before each move: HubSpot renumbers the pipeline when a stage lands on a taken slot. If a move waits out a rate limit, the step stops stale; plan again to see what is left. Kalup never sends a pipeline PUT, which deletes every stage it does not name.
40
+ - A new custom object is created with its name, labels and description, and its primary display property when HubSpot gives every custom object that property, else `hs_object_id`. Its groups and properties follow, created on the type ID HubSpot returned. HubSpot makes the group `<name>_information` with the object, so when the object file lists that group, apply gives it config's label instead of creating it, and records it adopted. Once they have run, one schema update sets the display, required and searchable properties, since HubSpot refuses a field naming a property it does not hold. Apply derives that update from the create step and reports it inside the create's report as `display` (`outcome`, and `units`, `issue` when present), on an indented line under the create in the text. The create's own `outcome` stays the create's, and the run's outcome and exit code count the update too. When a property it names did not finish, it is `not-run`; the create stays `done`, and the next plan sets the fields as an update. A create HubSpot answers with a custom object a read in the run listed, or one the schemas list shows made before the answer, made nothing: it is `uncertain`, and nothing on the object runs.
41
+ - A custom object update sends every field of the schema, from a read made right before it, with the approved changes: HubSpot can set fields left out of a partial update back to older values. Custom objects are read from the schemas list, never the single read, which lags. A custom object delete archives it: HubSpot refuses while the object holds records, and Kalup never purges it.
42
+ - An association is read from both labels lists of its pair, by the name the schema read gives, else by the type IDs state records or the create answered with: HubSpot's schema read lists a new name only minutes after the create. A label update sends both labels with the type ID of its direction. A delete is done when neither list holds the association. State records each association's two type IDs.
43
+ - A pipeline or stage is read back with a GET of its pipeline. A stage delete is done only when that read lacks the stage: HubSpot answers 204 to a delete of any stage, one that does not exist included.
39
44
  - A 429, 423 or 477 is waited out three times, reading again before each resend. A daily 429 stops the run.
40
45
  - A timeout, network failure or 5xx is `uncertain` and never resent: HubSpot documents no idempotency keys. Only reading back the approved values settles it (`E_UNCERTAIN_WRITE`).
41
46
  - A value HubSpot stores differently is `W_UNVERIFIED`: state records both, and the next plan notes it instead of writing again.
42
- - A delete runs only once every earlier step verified. HubSpot keeps an archived property restorable in its UI for 90 days.
47
+ - A delete runs only once every earlier step verified. HubSpot keeps an archived property restorable in its UI for 90 days, and an archived custom object in its archive until someone purges it; a deleted pipeline, stage or association is gone for good.
43
48
 
44
49
  State is saved after each step that changes an entry, then the outcome: `done`, `partial` or `uncertain`. Each request is journaled in `.kalup/journal/portal-<id>/`, never with a key or body. SIGINT or SIGTERM stops before the next request and saves state. A second one exits at once, unless it comes within a second (npx passes one Ctrl-C on twice).
45
50
 
package/docs/config.md CHANGED
@@ -8,14 +8,16 @@ This page is the reference. For the walk-through with examples, see [Config file
8
8
 
9
9
  - `kalup.config.ts`: one `export default defineConfig({...})` and nothing after it. Fields: `name` (default: the name in the nearest `package.json` up to the repository root, else the directory name), `dir` (the folder of object files, relative to `kalup.config.ts` and inside the project, default `hubspot`; `E_SETTING_VALUE` otherwise), `state` (`'local'`, the default, or `'repo'`, state.md), `prefix`, `defaultTarget` (targets.md), `mode` (below), `objects` (the pull scope, pull.md) and `targets` (targets.md). A setting at a level that does not take it is `E_SETTING_LEVEL`, whose fix lists the levels that do; a value it does not take is `E_SETTING_VALUE`, with the nearest allowed one.
10
10
  - `hubspot/objects/<object>.ts`: one or more `export const <Name> = defineObject('<object>', {...})` or `defineCustomObject('<name>', {...})`. The writer adds an `export type <Name>Data = ...` line after each. A file with no such export is `E_MISSING_EXPORT`.
11
- - `hubspot/index.ts`: the barrel, written by `pull` and `fmt`. It imports each object file as `./objects/<object>.js`, which resolves under TypeScript `NodeNext`, `Node16` and `Bundler` resolution, bundlers such as Vite and Next.js, and plain Node running `tsc` output. Under `NodeNext`, import it as `./hubspot/index.js`.
11
+ - `hubspot/pipelines/<object>.ts`: one or more `export const <Name> = definePipeline('<object>', {...})`, below. A `definePipeline` export anywhere else is `E_UNSUPPORTED_FILE`.
12
+ - `hubspot/associations.ts`: one `export const Associations = defineAssociations({...})`, below. A `defineAssociations` export anywhere else is `E_UNSUPPORTED_FILE`.
13
+ - `hubspot/index.ts`: the barrel, written by `pull` and `fmt`. It imports each object file as `./objects/<object>.js`, each pipeline file as `./pipelines/<object>.js` and the associations file as `./associations.js`, which resolves under TypeScript `NodeNext`, `Node16` and `Bundler` resolution, bundlers such as Vite and Next.js, and plain Node running `tsc` output. Under `NodeNext`, import it as `./hubspot/index.js`.
12
14
  - `hubspot/removed.ts`: tombstones, below.
13
- - `hubspot/pipelines/*`, and a `defineConfig` or `defineRemoved` file elsewhere under `hubspot/`, are `E_UNSUPPORTED_FILE`.
15
+ - A `defineConfig` or `defineRemoved` file elsewhere under `hubspot/` is `E_UNSUPPORTED_FILE`.
14
16
  - `hubspot/blueprints.lock.json` and `hubspot/.blueprints/`: written by `kalup add` (blueprints.md).
15
17
 
16
18
  The folder belongs to Kalup alone: every `.ts` file in it is read as config, `pull` rewrites its `index.ts`, and `init` takes it out of the formatter's checks. Point `dir` at a folder of its own (`lib/config/hubspot`, not `lib/config` next to the app's modules); `init` refuses a folder that holds other `.ts` files (`E_DIR_IN_USE`). A 0.1 project keeps its `kalup/` folder while `dir` is unset and `hubspot/` holds no `.ts` file, with `W_LEGACY_DIR` on every command until you set `dir: 'kalup'` or move the folder. When both hold `.ts` files, every command stops with `E_DIR_AMBIGUOUS` until `dir` says which.
17
19
 
18
- Commit `kalup.config.ts` and the folder: the object files, `index.ts`, `removed.ts`, the blueprints lock, and `state/` only with `state: 'repo'` (state.md). Never commit `.kalup/` (local state, saved plans, journals, history, snapshots), a plan file, `.env` or any file holding a key.
20
+ Commit `kalup.config.ts` and the folder: the object, pipeline and associations files, `index.ts`, `removed.ts`, the blueprints lock, and `state/` only with `state: 'repo'` (state.md). Never commit `.kalup/` (local state, saved plans, journals, history, snapshots), a plan file, `.env` or any file holding a key.
19
21
 
20
22
  ## The grammar
21
23
 
@@ -23,10 +25,10 @@ Anything else is `E_NOT_DATA`.
23
25
 
24
26
  - `import` lines. Imports from `@kalup/core` and `kalup` are rewritten; others are kept, for `p.json` validators.
25
27
  - Object literals of `key: value` entries, arrays, strings in single or double quotes on one line, numbers, `true` and `false`. No template strings, identifiers as values, spreads, computed keys, shorthand, or calls other than the builders.
26
- - A `//` comment on its own line above an export, a group entry or a property entry, and a comment block above the imports (the file header). Every other comment is an error, including any in `kalup.config.ts` but the header.
28
+ - A `//` comment on its own line above an export, a group entry, a property entry, a stage entry or an association entry, and a comment block above the imports (the file header). Every other comment is an error, including any in `kalup.config.ts` but the header.
27
29
  - `p.<kind>('<internal name>')` or `p.<kind>('<internal name>', {...})`, then any of `.strict()` (`p.enum` and `p.multiEnum` only), `.required()`, `.readonly()` and `.managed(false)`, each once. Any other chain call is `E_BAD_CHAIN`. A kind not listed below is `E_UNKNOWN_BUILDER`.
28
30
  - `p.json('<name>', <validator>, {...})`. The validator is opaque text and may not hold a `//` comment.
29
- - A custom object needs `labels: { singular, plural }` and `primaryDisplayProperty`, and may set `requiredProperties`, `searchableProperties` and `secondaryDisplayProperties`.
31
+ - A custom object needs `labels: { singular, plural }` and `primaryDisplayProperty`, and may set `description`, `requiredProperties`, `searchableProperties` and `secondaryDisplayProperties`. Its name, the first argument of `defineCustomObject`, starts with a letter and holds only letters, digits and underscores, at most 50 characters, and each label at most 50; `secondaryDisplayProperties` holds at most two properties, each once. Validate warns about a value breaking these rules (`W_OBJECT_FIELD`), since an object HubSpot holds can already break them, and plan blocks a create or update that would send one. HubSpot never changes the name once it creates the object. A display, required or searchable field may name a property HubSpot gives every custom object, such as `hs_object_id` or `hs_createdate`, or one the object file lists; any other is `W_OBJECT_PROPERTY`, a warning, since the portal may hold it outside the pull scope. A new custom object gets HubSpot's own properties, the group `<name>_information` and associations with activities only; its associations with other objects go in `hubspot/associations.ts`.
30
32
 
31
33
  `E_DUPLICATE_KEY`: a key twice in one literal, an export name twice in one file, or one internal name under two keys. `E_DUPLICATE_ADDRESS`: one address from two files or two exports.
32
34
 
@@ -78,13 +80,13 @@ The object key is the app's name for the property. Two exports of one object usi
78
80
 
79
81
  ## Per-target definitions
80
82
 
81
- A target's override `definition` (targets.md) replaces each field it states there, whole, and owns it, empty values included: a property's `label`, `description`, `group`, `fieldType`, `formField`, `options` (no `as`), `hidden`, `displayOrder`, the display fields, `calculationFormula` and lifecycle but `preventDestroy`; a group's `label`. Else `E_OVERRIDE_DEFINITION`. `pull` writes these fields into the override.
83
+ A target's override `definition` (targets.md) replaces each field it states there, whole, and owns it, empty values included: a property's `label`, `description`, `group`, `fieldType`, `formField`, `options` (no `as`), `hidden`, `displayOrder`, the display fields, `calculationFormula` and lifecycle but `preventDestroy`; a group's `label`; a pipeline's `label` and `displayOrder`; a stage's `label` and metadata field; an association label's `label` and `inverseLabel` (a plain association takes none, and `label` alone leaves the other side as the file has it). Else `E_OVERRIDE_DEFINITION`. `pull` writes these fields into the override.
82
84
 
83
85
  ## Mode: addon and takeover
84
86
 
85
87
  `mode: 'addon' | 'takeover'` at the top level, under `objects.<object>`, under `targets.<target>`, or under `targets.<target>.objects.<object>`. The most specific wins, in that order from the last, and the default is `addon`: Kalup manages only what config names. A target `mode` that differs from an object's `mode` the target says nothing more about is `W_MODE_SHADOWED`.
86
88
 
87
- Under `takeover`, `plan` archives every custom property and group in the object's pull scope that config lacks and `hubspot/removed.ts` does not name (a group only once every property in it goes, and after them), and removes enum options only the portal holds. Never a HubSpot-defined or calculated property, a kind Kalup does not write, anything an object file lists, a name `exclude` covers, or a property a custom object schema names. Every takeover removal is destructive: it needs `allowDestroy: true` on the target and a person at a terminal, and `--yes` and `--approve` never cover it. Without `allowDestroy` it is blocked, reason `policy`; after an incomplete read, reason `scope`.
89
+ Under `takeover`, `plan` archives every custom property and group in the object's pull scope that config lacks and `hubspot/removed.ts` does not name (a group only once every property in it goes, and after them), and removes enum options only the portal holds. Never a HubSpot-defined or calculated property, a kind Kalup does not write, anything an object file lists, a name `exclude` covers, a property a custom object schema names, a pipeline or stage, or an association. Every takeover removal is destructive: it needs `allowDestroy: true` on the target and a person at a terminal, and `--yes` and `--approve` never cover it. Without `allowDestroy` it is blocked, reason `policy`; after an incomplete read, reason `scope`.
88
90
 
89
91
  ## Removed resources
90
92
 
@@ -97,8 +99,61 @@ export default defineRemoved({
97
99
  })
98
100
  ```
99
101
 
100
- `destroy` deletes the resource, only on a target with `allowDestroy: true` (default false). `release` stops managing it, leaving it there. Keys are property or group addresses (`E_TOMBSTONE_ADDRESS`) that config no longer defines (`E_TOMBSTONE_CONFLICT`).
102
+ `destroy` deletes the resource, only on a target with `allowDestroy: true` (default false). `release` stops managing it, leaving it there. Keys are custom object, property, group, pipeline, stage or association addresses (`E_TOMBSTONE_ADDRESS`) that config no longer defines (`E_TOMBSTONE_CONFLICT`). A pipeline's tombstone covers its stages, and a custom object's everything on it, its associations on either side included. HubSpot keeps no archive of a pipeline, a stage or an association: a delete is permanent.
103
+
104
+ ## Pipelines
105
+
106
+ ```ts
107
+ // hubspot/pipelines/deals.ts
108
+ import { definePipeline } from '@kalup/core'
109
+
110
+ export const RenewalsPipeline = definePipeline('deals', {
111
+ id: 'renewals',
112
+ label: 'Renewals',
113
+ displayOrder: 2,
114
+ stages: {
115
+ open: { id: 'renewals_open', label: 'Open', probability: 0.2 },
116
+ won: { id: 'renewals_won', label: 'Won', probability: 1 },
117
+ lost: { id: 'renewals_lost', label: 'Lost', probability: 0 },
118
+ },
119
+ })
120
+ ```
121
+
122
+ - The first argument is the object key as in `objects`. Kalup writes the pipelines of `deals`, `tickets` and custom objects; the pipelines of contacts, companies, appointments, services, listings, courses, orders and leads are read and compared, never written. A pipeline on an object HubSpot gives none is `E_PIPELINE_FIELD`.
123
+ - `id` is the pipeline or stage ID HubSpot stores, and the address: `pipeline:deals/renewals`, `stage:deals/renewals/renewals_won`. It never changes; a new ID is a new pipeline or stage. A pipeline ID is at most 36 characters, a stage ID at most 100, neither holds whitespace or `/`, a pipeline ID is used once in the project and a stage ID once per object (`E_PIPELINE_ID`): HubSpot keeps pipeline IDs unique across objects and stage IDs across one object's pipelines.
124
+ - `label` and `displayOrder` are required on a pipeline, `label` on a stage. Labels update in place. Two stages of a pipeline, or two pipelines of an object, may not share a label, ignoring case (`E_DUPLICATE_LABEL`).
125
+ - Stage order is the order of the entries under `stages`. Stages take no `displayOrder`.
126
+ - Each stage takes the metadata field of its object, and no other (`E_PIPELINE_FIELD`): `probability` on a deal stage, required, from 0 to 1; `ticketState` on a ticket stage, `'OPEN'` or `'CLOSED'`, default `'OPEN'`; `state` on a custom object stage, the same. HubSpot derives whether a stage is closed from it.
127
+ - A pipeline needs a stage, and a ticket pipeline a stage with `ticketState: 'CLOSED'` (`E_PIPELINE_STAGES`).
128
+ - The export name and the stage keys are free. In the app, `RenewalsPipeline.stages.won.id` is typed `'renewals_won'`, and `StageId<typeof RenewalsPipeline>` is the union of its stage IDs.
129
+
130
+ A target's override takes a pipeline's or stage's ID on that portal as `name`, `skip` (a skipped pipeline takes its stages), and a `definition` with a pipeline's `label` and `displayOrder` or a stage's `label` and metadata field.
131
+
132
+ ## Associations
133
+
134
+ ```ts
135
+ // hubspot/associations.ts
136
+ import { defineAssociations } from '@kalup/core'
137
+
138
+ export const Associations = defineAssociations({
139
+ // The person who signed the contract.
140
+ signer: { from: 'deals', to: 'contacts', name: 'deal_signer', label: 'Signer', inverseLabel: 'Signed deal' },
141
+ colleague: { from: 'companies', to: 'contacts', name: 'colleague', label: 'Colleague' },
142
+ visitCompany: { from: 'visit', to: 'companies', name: 'visit_to_company' },
143
+ })
144
+ ```
145
+
146
+ - Each entry is one association of a pair of objects. `from` and `to` are object keys under `objects`, both (`E_ASSOCIATION_FIELD`); a pair of one object with itself is not managed in this release.
147
+ - `name` is HubSpot's internal name: unique in the portal, set on create and never changed, so it is the address: `association:deals/contacts/deal_signer`. A label made in the HubSpot UI is named after its text in lower case with spaces as underscores. A name or object holding whitespace or `/` is `E_ASSOCIATION_NAME`; a name used twice is `E_DUPLICATE_KEY`.
148
+ - `label` is what records of `from` show, and `inverseLabel` what records of `to` show. Leave `inverseLabel` out and HubSpot shows the label on both sides; `pull` leaves it out when the two are equal. `inverseLabel` needs a `label` (`E_ASSOCIATION_FIELD`). A label is never empty, and two entries of a pair may not show one text from the same side, ignoring case (`E_DUPLICATE_LABEL`).
149
+ - An entry with no `label` is the plain association of the pair. A pair with a custom object has one HubSpot lets you manage, at most one per pair; between two standard objects HubSpot defines it, so an entry for it is `E_ASSOCIATION_FIELD`. Apply creates a pair's plain association before its labels. A label created on a pair with no plain association makes one too, under a name HubSpot picks: the plan says so.
150
+ - The name is the identity, not the direction: rewriting an entry from the other side is the same association, and state follows it. A tombstone on the other direction of an association config holds is `E_TOMBSTONE_CONFLICT`.
151
+ - The keys are free. In the app, `Associations.signer.name` is typed `'deal_signer'`, and `AssociationName<typeof Associations>` is the union of the names.
152
+ - Deleting a label is destructive: records lose that association. HubSpot refuses to delete a plain association while a label of its pair remains, so plan blocks it until the labels go.
153
+ - Kalup does not manage association limits, the most records one record may have under a label, in this release.
154
+
155
+ A target's override takes an association label's `label` and `inverseLabel` as a `definition`, and `skip`.
101
156
 
102
157
  ## Canonical form
103
158
 
104
- `kalup fmt` validates, then rewrites `kalup.config.ts`, `hubspot/removed.ts`, every object file and the barrel: groups and properties sorted by internal name, tombstones by address, fields in a fixed order, options in display order, quotes as biome writes them, 120 columns. It keeps every value you wrote, `description: ''`, `options: []`, `false` and an empty `lifecycle` included: a present field is owned. `fmt --check` lists the files it would change and exits 2 when there are any. Old files go to `.kalup/history/<timestamp>/` first; the last 20 runs are kept.
159
+ `kalup fmt` validates, then rewrites `kalup.config.ts`, `hubspot/removed.ts`, every object and pipeline file, the associations file and the barrel: groups and properties sorted by internal name, stages in the order written, association entries by name, tombstones by address, fields in a fixed order, options in display order, quotes as biome writes them, 120 columns. It keeps every value you wrote, `description: ''`, `options: []`, `false` and an empty `lifecycle` included: a present field is owned. `fmt --check` lists the files it would change and exits 2 when there are any. Old files go to `.kalup/history/<timestamp>/` first; the last 20 runs are kept.
@@ -10,8 +10,8 @@ This page is the reference. For the walk-through with examples, see [kalup docs]
10
10
  ## Layout
11
11
 
12
12
  1. `# <project> data dictionary` and a line naming the source: the config files, or the target, portal ID and `observedAt` of the snapshot.
13
- 2. `## Coverage`. For a snapshot: whether the read was complete, the objects not read with the missing scope, objects absent from the portal, what `skip` overrides left out, unsupported properties (Kalup does not write them), schemas without a label, `name` overrides, config properties in a group no address can hold, counts out of scope and shadowed, custom objects config does not name, and the fields Kalup does not capture. Reference properties record only their options.
14
- 3. One `## <object>` section per object key, sorted: a custom object's labels, display property and property lists; a groups table (internal name, label); a properties table (internal name, label, type, field type, group, managed or reference, description); and one options table per enumeration (value, label, hidden, description), in display order. Config adds the key, codec, required and alias columns. Unsupported properties appear only under Coverage.
13
+ 2. `## Coverage`. For a snapshot: whether the read was complete, the objects not read with the missing scope, objects absent from the portal, what `skip` overrides left out, unsupported properties (Kalup does not write them), schemas without a label, `name` overrides, config properties in a group no address can hold, counts out of scope and shadowed, custom objects config does not name, objects whose pipelines were not read, and the fields Kalup does not capture. Reference properties record only their options.
14
+ 3. One `## <object>` section per object key, sorted: a custom object's labels, description, display property and property lists; a groups table (internal name, label); a properties table (internal name, label, type, field type, group, managed or reference, description); one options table per enumeration (value, label, hidden, description), in display order; per pipeline, sorted by ID, a `### Pipeline <label> (<id>)` heading, its display order, and a stages table in stage order (stage ID, label, and its probability or state; config adds the key); and an associations table of the associations from the object, sorted (internal name, the other object, label and inverse label; config adds the key). Config adds the key, codec, required and alias columns. Unsupported properties appear only under Coverage.
15
15
  4. Config with `definition` overrides: `## Per-target overrides`, one row per address, field and target with its value, sorted.
16
16
 
17
17
  ## Escaping
@@ -20,7 +20,7 @@ Every string from a file or a portal is shown as text: newlines become spaces, c
20
20
 
21
21
  ## Deterministic
22
22
 
23
- The same source gives the same bytes: objects, groups and properties sorted, options in display order, and no timestamp but a snapshot's own `observedAt`. Commit the dictionary and check it in CI:
23
+ The same source gives the same bytes: objects, groups, properties, pipelines and associations sorted, options and stages in display order, and no timestamp but a snapshot's own `observedAt`. Commit the dictionary and check it in CI:
24
24
 
25
25
  ```sh
26
26
  npx --no-install kalup docs --out DATA-DICTIONARY.md
@@ -0,0 +1,21 @@
1
+ # E_ASSOCIATION_FIELD
2
+
3
+ An entry of `associations.ts` breaks a rule HubSpot keeps for association labels. Exit 3.
4
+
5
+ ## When
6
+
7
+ Both objects of an entry must be under `objects` in `kalup.config.ts`, so plan can read the pair. A label holds text: leave `label` out for the plain association of a pair. A pair has one plain association, and between two standard objects HubSpot defines it, so the files cannot. A label is unique per pair and direction (live runs, 2026-10-01 and 2026-10-05); two the same are `E_DUPLICATE_LABEL`. A pair of one object with itself is not managed in this release, and an inverseLabel needs a label.
8
+
9
+ ## Fix
10
+
11
+ Change or remove the entry the message names.
12
+
13
+ ## Example
14
+
15
+ ```ts
16
+ crew: { from: 'companies', to: 'contacts', name: 'crew_plain' },
17
+ ```
18
+
19
+ ```
20
+ hubspot/associations.ts:6: E_ASSOCIATION_FIELD: association:companies/contacts/crew_plain has no label, and HubSpot defines the plain association between companies and contacts (fix: give it a label, or remove it: the plain association between two standard objects is always there) (docs: errors/E_ASSOCIATION_FIELD.md)
21
+ ```
@@ -0,0 +1,17 @@
1
+ # E_ASSOCIATION_NAME
2
+
3
+ An entry of `associations.ts` names an object or an internal name holding whitespace or a slash. Exit 3.
4
+
5
+ ## When
6
+
7
+ An association is addressed as `association:<from>/<to>/<name>`, so from, to and name must each be non-empty and hold no whitespace or slash. The name is what HubSpot stores for both directions of the label, unique in the portal and never changed.
8
+
9
+ ## Fix
10
+
11
+ Use the object keys from `objects`, and an internal name of letters, digits and underscores, such as the label in lower case with underscores: `charter_signer`.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ hubspot/associations.ts:6: E_ASSOCIATION_NAME: from, to and name must each be non-empty and hold no whitespace or slash, so an address can hold them (fix: use object keys and an internal name without spaces or slashes) (docs: errors/E_ASSOCIATION_NAME.md)
17
+ ```
@@ -4,14 +4,14 @@ A blueprint needs a custom object config does not define. Exit 1. Nothing was wr
4
4
 
5
5
  ## When
6
6
 
7
- A blueprint's resources and its `requires` list name objects. A standard object (contacts, companies, deals, tickets and the rest) is always there, and `kalup add` adds it to `objects` in `kalup.config.ts` when missing. A custom object has to exist in the portal and in config first: Kalup does not create custom object schemas, so the blueprint's properties would have nowhere to go.
7
+ A blueprint's resources and its `requires` list name objects. A standard object (contacts, companies, deals, tickets and the rest) is always there, and `kalup add` adds it to `objects` in `kalup.config.ts` when missing. A custom object has to be in config first: a blueprint carries no custom object schema, so its properties would have nowhere to go.
8
8
 
9
9
  ## Fix
10
10
 
11
- Create the custom object in HubSpot, add its key under `objects` in `kalup.config.ts`, run `kalup pull` to write its object file, then run the command again.
11
+ Add the key under `objects` in `kalup.config.ts`. Define the object with `defineCustomObject` in its object file, or, when HubSpot has it already, run `kalup pull` to write that file. Then run the command again.
12
12
 
13
13
  ## Example
14
14
 
15
15
  ```
16
- E_BLUEPRINT_REQUIRES: the blueprint needs the custom object vineyard, which config does not define. Nothing was written. (fix: add vineyard: {} under objects in kalup.config.ts and run kalup pull to write its object file, or create the object in HubSpot first) (docs: errors/E_BLUEPRINT_REQUIRES.md)
16
+ E_BLUEPRINT_REQUIRES: the blueprint needs the custom object vineyard, which config does not define. Nothing was written. (fix: add vineyard: {} under objects in kalup.config.ts and define it with defineCustomObject, or run kalup pull to write its object file when HubSpot has it) (docs: errors/E_BLUEPRINT_REQUIRES.md)
17
17
  ```
@@ -0,0 +1,24 @@
1
+ # E_DUPLICATE_LABEL
2
+
3
+ Two stages of one pipeline, or two pipelines of one object, share a label. Exit 3.
4
+
5
+ ## When
6
+
7
+ HubSpot refuses a stage whose label another stage of the same pipeline has, ignoring case and spaces around it, and a pipeline whose label another pipeline of the same object has, ignoring case (live runs, 2026-10-05). The same label on stages of two pipelines, or on pipelines of two objects, is fine.
8
+
9
+ ## Fix
10
+
11
+ Give one of the two another label.
12
+
13
+ ## Example
14
+
15
+ ```ts
16
+ stages: {
17
+ tasting: { id: 'orchard_tasting', label: 'Tasting', probability: 0.2 },
18
+ retasting: { id: 'orchard_retasting', label: 'tasting', probability: 0.3 },
19
+ },
20
+ ```
21
+
22
+ ```
23
+ hubspot/pipelines/deals.ts:9: E_DUPLICATE_LABEL: stages 'orchard_tasting' and 'orchard_retasting' of pipeline:deals/orchard_sales share the label 'tasting', ignoring case (fix: give one of the two another label) (docs: errors/E_DUPLICATE_LABEL.md)
24
+ ```
@@ -1,10 +1,10 @@
1
1
  # E_MISSING_EXPORT
2
2
 
3
- A file under `hubspot/` has no `defineObject` or `defineCustomObject` export. Exit 3.
3
+ A file under `hubspot/` has no `defineObject` or `defineCustomObject` export, or a file under `hubspot/pipelines/` no `definePipeline` export. Exit 3.
4
4
 
5
5
  ## When
6
6
 
7
- Kalup reads every `.ts` file in the folder of object files (`hubspot/`, or the folder `dir` in `kalup.config.ts` names) except `index.ts` and `removed.ts` as an object file. A file with only imports, or an empty file, has nothing to read.
7
+ Kalup reads every `.ts` file in the folder of object files (`hubspot/`, or the folder `dir` in `kalup.config.ts` names) except `index.ts` and `removed.ts` as an object file, and each one under `pipelines/` as a pipeline file. A file with only imports, or an empty file, has nothing to read.
8
8
 
9
9
  ## Fix
10
10
 
@@ -0,0 +1,23 @@
1
+ # E_PIPELINE_FIELD
2
+
3
+ A pipeline or stage states a field HubSpot would refuse, or drop without a word. Exit 3.
4
+
5
+ ## When
6
+
7
+ A stage carries one metadata field, by its pipeline's object: `probability` on deals, from 0 to 1 and required, `ticketState` on tickets and `state` on custom objects, each `'OPEN'` or `'CLOSED'`. HubSpot drops any other metadata without an error, so a write would change nothing and every plan would show it again; HubSpot derives `isClosed` itself. A pipeline on another object, such as the contacts lifecycle pipeline, is read and compared, never written, so its stages carry no metadata. A pipeline's `displayOrder` is an integer from 0 up (live runs, 2026-10-01 and 2026-10-05).
8
+
9
+ A target's definition override that breaks one of these rules is `E_OVERRIDE_DEFINITION`.
10
+
11
+ ## Fix
12
+
13
+ Change or remove the field the message names.
14
+
15
+ ## Example
16
+
17
+ ```ts
18
+ won: { id: 'orchard_signed', label: 'Signed', ticketState: 'CLOSED' },
19
+ ```
20
+
21
+ ```
22
+ hubspot/pipelines/deals.ts:10: E_PIPELINE_FIELD: ticketState is for ticket stages; a deal stage takes probability (fix: replace ticketState with probability, from 0 to 1) (docs: errors/E_PIPELINE_FIELD.md)
23
+ ```
@@ -0,0 +1,17 @@
1
+ # E_PIPELINE_ID
2
+
3
+ A pipeline or stage ID cannot be used: it forms no address, is too long, or another one holds it. Exit 3.
4
+
5
+ ## When
6
+
7
+ A pipeline or stage ID is its address and what HubSpot stores, so it must hold no whitespace or slash. HubSpot stores a pipeline ID of at most 36 characters and a stage ID of at most 100, and answers 500 to a longer one. A pipeline ID is unique across the portal, deals and tickets included, and a stage ID across the pipelines of one object (live runs, 2026-10-01 and 2026-10-05), so two in config may not share one.
8
+
9
+ ## Fix
10
+
11
+ Give the pipeline or stage another ID. An ID is permanent once HubSpot creates it, so choose a short, readable one, such as the pipeline ID followed by the stage, `orchard_signed`.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ hubspot/pipelines/tickets.ts:5: E_PIPELINE_ID: pipeline:tickets/orchard_sales has the ID of pipeline:deals/orchard_sales, and HubSpot keeps pipeline IDs unique across objects (fix: give one of the two another ID) (docs: errors/E_PIPELINE_ID.md)
17
+ ```
@@ -0,0 +1,17 @@
1
+ # E_PIPELINE_STAGES
2
+
3
+ A pipeline has no stage, or a ticket pipeline has no closed stage. Exit 3.
4
+
5
+ ## When
6
+
7
+ HubSpot refuses a pipeline with no stage, and a ticket pipeline with no stage whose `ticketState` is `'CLOSED'` (live runs, 2026-10-05). `kalup rm` refuses to remove such a pipeline's last stage, or its last closed one, for the same reason.
8
+
9
+ ## Fix
10
+
11
+ Add a stage, or mark one ticket stage `ticketState: 'CLOSED'`. To drop the whole pipeline, run `kalup rm` on the pipeline.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ hubspot/pipelines/tickets.ts:3: E_PIPELINE_STAGES: pipeline:tickets/orchard_desk has no stage with ticketState 'CLOSED', and HubSpot needs one (fix: mark the stage tickets end in ticketState: 'CLOSED') (docs: errors/E_PIPELINE_STAGES.md)
17
+ ```
@@ -4,7 +4,7 @@ A saved plan deletes something config does not ask to delete. Exit 1. Nothing wa
4
4
 
5
5
  ## When
6
6
 
7
- A delete needs a `destroy` tombstone that `kalup rm` wrote, or takeover to ask for it (the mode of the object on the target is takeover, the address is in the pull scope, neither `exclude`, a `skip` override nor a tombstone names it, and the step carries the `takeover` label), and an address gone from config. Before approval, `kalup apply` reads `hubspot/removed.ts` and the object files as data, never running them, and refuses a delete step whose address has neither, is still in config, or sets `lifecycle.preventDestroy`; the message says why takeover does not archive it. It also refuses a delete of a portal resource that another address in config names through a name override on the target. The tombstone was removed after planning, the resource came back into config or into `exclude`, or the plan file was edited.
7
+ A delete needs a `destroy` tombstone that `kalup rm` wrote, or takeover to ask for it (the mode of the object on the target is takeover, the address is in the pull scope, neither `exclude`, a `skip` override nor a tombstone names it, and the step carries the `takeover` label), and an address gone from config. Before approval, `kalup apply` reads `hubspot/removed.ts` and the object files as data, never running them, and refuses a delete step whose address has neither, is still in config, or sets `lifecycle.preventDestroy`; the message says why takeover does not archive it. A step labelled `takeover` needs takeover to ask for it whatever `hubspot/removed.ts` says, so takeover never deletes a custom object, a pipeline or a stage. It also refuses a delete of a portal resource that another address in config names through a name override on the target, and a custom object archive while config still holds anything on the object, since the archive takes it along. The tombstone was removed after planning, the resource came back into config or into `exclude`, or the plan file was edited.
8
8
 
9
9
  ## Fix
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## When
6
6
 
7
- The file named on the command line is missing, is not JSON, or does not match the `plan/1` schema. The message names the first place that fails. Apply also refuses a file whose steps contradict themselves: a change that writes a value the step's `desired` values do not hold. `kalup plan` never writes such a file.
7
+ The file named on the command line is missing, is not JSON, or does not match the `plan/1` schema. The message names the first place that fails. Apply also refuses a file whose steps contradict themselves: a change that writes a value the step's `desired` values do not hold, or a custom object archive whose `expect` does not count the properties, groups and pipelines it takes along. `kalup plan` never writes such a file.
8
8
 
9
9
  ## Fix
10
10
 
@@ -4,7 +4,7 @@ A step in the plan does not match what Kalup derives from state and the portal.
4
4
 
5
5
  ## When
6
6
 
7
- Under the portal lock, `kalup apply` reads state and the portal again and derives each step's risk, labels and blocked status as `plan` does. It refuses when a step states a lower risk than derived, leaves out a derived label (`reverts-ui-edit`, `overwrites-portal`, `takeover`), or would be blocked: an update of what state does not own, a delete of what state does not own that takeover does not archive, an adopt of what it does, a delete or takeover option removal the target does not allow, a takeover archive of what HubSpot defines, of a property in a group a `skip` override covers or one a custom object schema names, of a group that held no property, or whose `expect` leaves out a field the base holds (so an edit made in HubSpot after the review would not stop it), a custom object schema change, or a group delete while properties still name the group. A plan `kalup plan` saved matches, unless config changed since, such as a `skip` override added; otherwise the file was edited.
7
+ Under the portal lock, `kalup apply` reads state and the portal again and derives each step's risk, labels and blocked status as `plan` does. It refuses when a step states a lower risk than derived, leaves out a derived label (`reverts-ui-edit`, `overwrites-portal`, `takeover`), or would be blocked: an update of what state does not own, a delete of what state does not own that takeover does not archive, a delete labelled `takeover` of a custom object, a pipeline, a stage, something on an object whose mode is not takeover, or something `hubspot/removed.ts` names or covers, an adopt of what it does, a delete or takeover option removal the target does not allow, a takeover archive of what HubSpot defines, of a property in a group a `skip` override covers or one a custom object schema names, of a group that held no property, or whose `expect` leaves out a field the base holds (so an edit made in HubSpot after the review would not stop it) or, for a custom object archive, a count of what it takes along, a custom object schema change, or a group delete while properties still name the group. A plan `kalup plan` saved matches, unless config changed since, such as a `skip` override added; otherwise the file was edited.
8
8
 
9
9
  ## Fix
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## When
6
6
 
7
- `preventDestroy: true` in a property's `lifecycle` says the property must never be deleted through Kalup. `kalup rm <address>` writes a `destroy` tombstone, which a later plan turns into a delete, so rm refuses it before it changes any file.
7
+ `preventDestroy: true` in a property's `lifecycle` says the property must never be deleted through Kalup. `kalup rm <address>` writes a `destroy` tombstone, which a later plan turns into a delete, so rm refuses it before it changes any file. A custom object's archive takes every group, property and pipeline on it along, so `kalup rm object:<name>` refuses while any of them sets `preventDestroy`, and names them. After `kalup rm object:<name> --release` they have left config, so rm will not turn that release into a destroy: remove the release tombstone, pull the object back, then remove it again.
8
8
 
9
9
  ## Fix
10
10
 
@@ -1,10 +1,10 @@
1
1
  # E_TOMBSTONE_ADDRESS
2
2
 
3
- A key in `hubspot/removed.ts`, or the address given to `kalup rm`, is not the address of a property or group. Exit 3.
3
+ A key in `hubspot/removed.ts`, or the address given to `kalup rm`, is not the address of a property, group, pipeline or stage. Exit 3.
4
4
 
5
5
  ## When
6
6
 
7
- Each key in `hubspot/removed.ts` is an address, such as `property:companies/legacy_score`: the type, a colon, the object, a slash and the name. A key with no object, such as `property:legacy_score`, names nothing and is refused. This version removes properties and property groups only, so a key of another type, such as `object:parcels`, is refused as well.
7
+ Each key in `hubspot/removed.ts` is an address, such as `property:companies/legacy_score`: the type, a colon, the object, a slash and the name. A key with no object, such as `property:legacy_score`, names nothing and is refused. A stage address names its pipeline as well: `stage:deals/renewals/won`. This version removes custom objects, properties, property groups, pipelines and stages, so a key of another type, such as `list:renewals`, is refused as well, and so is `object:<name>` for a standard object, which HubSpot defines, or for a key that is not under `objects` in `kalup.config.ts`.
8
8
 
9
9
  ## Fix
10
10
 
@@ -19,5 +19,5 @@ export default defineRemoved({
19
19
  ```
20
20
 
21
21
  ```
22
- hubspot/removed.ts:4: E_TOMBSTONE_ADDRESS: 'legacyScore' is not an address (fix: write the address of a property or group, such as 'property:companies/legacy_score') (docs: errors/E_TOMBSTONE_ADDRESS.md)
22
+ hubspot/removed.ts:4: E_TOMBSTONE_ADDRESS: 'legacyScore' is not an address (fix: write the address of a property, group, pipeline or stage, such as 'property:companies/legacy_score') (docs: errors/E_TOMBSTONE_ADDRESS.md)
23
23
  ```
@@ -4,7 +4,7 @@ An address is in `hubspot/removed.ts` and still defined in config. Exit 3.
4
4
 
5
5
  ## When
6
6
 
7
- A tombstone takes a resource out of config: `destroy` deletes it in the portal, `release` stops managing it and leaves it there. Config may not define the same address at the same time, not even as a reference without `label`, `group` and `fieldType`. This usually means the entry was added to `hubspot/removed.ts` by hand and the property or group was left in its object file.
7
+ A tombstone takes a resource out of config: `destroy` deletes it in the portal, `release` stops managing it and leaves it there. Config may not define the same address at the same time, not even as a reference without `label`, `group` and `fieldType`. This usually means the entry was added to `hubspot/removed.ts` by hand and the property or group was left in its object file. A custom object tombstone takes everything on the object along, so config may not hold any group, property, pipeline or stage on that object either; `kalup rm object:<name>` takes them out with it.
8
8
 
9
9
  ## Fix
10
10