kalup 0.4.0 → 0.6.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.
@@ -0,0 +1,17 @@
1
+ # W_OBJECT_PROPERTY
2
+
3
+ A warning from validate: a custom object's `primaryDisplayProperty`, `secondaryDisplayProperties`, `requiredProperties` or `searchableProperties` names a property its object file does not list. Exit stays 0.
4
+
5
+ ## When
6
+
7
+ HubSpot refuses a schema create or update that names a property it does not hold (live runs, 2026-10-05). A property HubSpot gives every custom object, such as `hs_object_id` or `hs_createdate`, needs no entry, and neither does any other `hs_` name, a prefix HubSpot reserves. Any other one the object file does not list may still be in the portal, outside the pull scope, so validate only warns. `plan` blocks a schema write that names a property neither the portal holds nor the plan creates, and raises this warning itself for an `hs_` name it takes as HubSpot's that is not one HubSpot is known to give every custom object.
8
+
9
+ ## Fix
10
+
11
+ Add the property to the object's `properties`: with its definition when Kalup should create it, or as a reference, `p.string('<name>')`, when HubSpot holds it already.
12
+
13
+ ## Example
14
+
15
+ ```
16
+ hubspot/objects/orchard_visit.ts:3: W_OBJECT_PROPERTY: primaryDisplayProperty of object:orchard_visit names visit_title, which the object file does not list; HubSpot refuses a schema write naming a property it does not hold (fix: add visit_title to the object's properties, as a reference if HubSpot holds it already: p.string('visit_title')) (docs: errors/W_OBJECT_PROPERTY.md)
17
+ ```
@@ -0,0 +1,19 @@
1
+ # W_SETTLING
2
+
3
+ A warning from every command that reads a target: for some minutes after a write HubSpot may serve an older copy of what was written, so the read cannot be trusted on those resources yet. Exit stays 0, except as below.
4
+
5
+ ## When
6
+
7
+ After `apply` verifies a write, state records when it did and the units it wrote. For 5 minutes after that, a read that shows another value than apply verified on one of those units, or does not show the resource, is settling: HubSpot has been seen to serve a custom object schema's fields from before a PATCH, to leave a new custom object out of the schemas list, and to leave a new association's name out of its schema read for about 5 minutes. A label HubSpot lists that its schema read does not name yet settles the same way.
8
+
9
+ A settling resource is unknown, never absent, drifted or held: `plan` blocks its step with reason `settling` and never creates, deletes or writes it, a destroy tombstone on it included, `pull` keeps the file as it is, `compare` reports it `unknown` (`E_INCOMPLETE`, exit 1), and the read is incomplete. Takeover waits only while something on the object it removes from settles; `state rebuild --write` and `target rebind` only while something config names does. Its base in state never moves on such a read. A difference on a unit apply did not write is drift as ever.
10
+
11
+ ## Fix
12
+
13
+ Run the command again after the time the warning names. Nothing to change.
14
+
15
+ ## Example
16
+
17
+ ```
18
+ W_SETTLING: HubSpot still serves an older copy of 1 resource apply wrote minutes ago, so this read is not trusted on it until 2026-10-06T09:05:00.000Z: property:companies/billing_status (fix: run the command again after 2026-10-06T09:05:00.000Z) (docs: errors/W_SETTLING.md)
19
+ ```
package/docs/plan.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Plan
2
2
 
3
- `kalup plan [--target <name>] [--take config <address[#unit]>] [--out [<file>]] [--exit-code]` shows what apply would do to one target: a step per object, group, property, pipeline and stage config manages, `definition` overrides applied (config.md), then the releases and deletes tombstones ask for. It writes neither portal nor state. `--out <file>` saves the plan/1 document; `--out` alone saves it as `.kalup/plans/<target>-<planId>.json` and prints the path. `kalup apply <file>` applies either.
3
+ `kalup plan [--target <name>] [--take config <address[#unit]>] [--out [<file>]] [--exit-code]` shows what apply would do to one target: a step per object, group, property, pipeline, stage and association config manages, `definition` overrides applied (config.md), then the releases and deletes tombstones ask for. It writes neither portal nor state. `--out <file>` saves the plan/1 document; `--out` alone saves it as `.kalup/plans/<target>-<planId>.json` and prints the path. `kalup apply <file>` applies either.
4
4
 
5
5
  This page is the reference. For the walk-through with examples, see [kalup plan](https://kalup.dev/docs/commands/plan) and [Drift](https://kalup.dev/docs/concepts/drift) on the website.
6
6
 
@@ -9,8 +9,8 @@ This page is the reference. For the walk-through with examples, see [kalup plan]
9
9
  1. Validate (`E_NO_CONFIG` exit 1, other issues exit 3), then pick the target (targets.md).
10
10
  2. The read key, then the portal guard (`E_TARGET_PORTAL_MISMATCH`, exit 4).
11
11
  3. State for that portal, `.kalup/state/portal-<portalId>.json`; an unusable file is `E_STATE_INVALID`.
12
- 4. Pull's read and scope, plus tombstoned properties, and the pipelines of each object whose files define one, with `pipelines: true`, or with a tombstoned pipeline or stage. A 403 leaves that object unread (`E_SCOPE`); on the pipelines list, only its pipelines.
13
- 5. Limits Tracking (403 without a `crm.objects.*` scope; `W_LIMIT_UNREADABLE` for property and pipeline creates), then the three `archived=true` lists of each object with a property create or an owned property HubSpot no longer holds. A group delete reads no archived list: only active properties block it. A 403 there is exit 1.
12
+ 4. Pull's read and scope, plus tombstoned properties, and the pipelines of each object whose files define one, with `pipelines: true`, or with a tombstoned pipeline or stage, and the labels of each object pair in association scope. A 403 leaves that object unread (`E_SCOPE`); on the pipelines list, only its pipelines; on a labels list, only that pair's associations.
13
+ 5. Limits Tracking (403 without a `crm.objects.*` scope; `W_LIMIT_UNREADABLE` for property and pipeline creates; the association label counts of each pair the plan creates labels on, which only warn, `W_LIMIT_HEADROOM`, since HubSpot counts a label deleted in the last 40 seconds), then the three `archived=true` lists of each object with a property create or an owned property HubSpot no longer holds. A group delete reads no archived list: only active properties block it. A 403 there is exit 1.
14
14
  6. The plan, checked against `plan-1.schema.json` (`E_PLAN_SCHEMA` is a bug).
15
15
 
16
16
  ## State and the base
@@ -39,7 +39,17 @@ With `drift: 'overwrite'`, drift and conflicts are written labelled `reverts-ui-
39
39
 
40
40
  ## What blocks
41
41
 
42
- The first rule that matches: a `skip` override (no step, `coverage.excluded`); a `lookup` override; an unread object (`scope`, action `unknown`); a blocked parent or missing group (`dependency-blocked`); a property Kalup does not write (pull.md), whose fix makes it a `p.string` reference; for a create, a missing `name` override target, an archived property name (a create restores it; an archived group's name is created anew), or no limit room; HubSpot-defined or calculated, a `type` or `hasUniqueValue` difference, or read-only definition or options. A custom object schema is compared and never written: a missing one is blocked, and its differences are held or noted.
42
+ The first rule that matches: a `skip` override (no step, `coverage.excluded`); a `lookup` override; a resource settling after an apply (`settling`, action `unknown`, below); an unread object (`scope`, action `unknown`); a blocked parent or missing group (`dependency-blocked`); a property Kalup does not write (pull.md), whose fix makes it a `p.string` reference; for a create, a missing `name` override target, an archived property name (a create restores it; an archived group's name is created anew), or no limit room; HubSpot-defined or calculated, a `type` or `hasUniqueValue` difference, or read-only definition or options.
43
+
44
+ For 5 minutes after apply wrote a unit and read it back, HubSpot can still serve the copy from before the write. So within that window a read that shows another value than the base on a unit apply wrote, or leaves out a resource apply wrote, makes the resource settling, with what lies under a custom object left out that way (what is under a resource HubSpot serves an older copy of plans as ever): its step is `unknown`, blocked `settling`, its fix says when to plan again, and `W_SETTLING` warns. Nothing is held, created, deleted or written for it, a destroy tombstone's release or delete included, and the read is incomplete. A release tombstone still releases. A difference on a unit apply did not write is drift as ever, and after the window the read is believed again. State records what apply wrote and when in each entry's `written` and `writtenAt`. Takeover waits only while a property or group of the object it removes from settles.
45
+
46
+ ## Custom objects
47
+
48
+ - A new custom object is one `create` step. Apply creates it with its name, labels and description, then its groups and properties, then sets the display, required and searchable properties, since HubSpot refuses those fields until the properties they name exist. The step's notes say what HubSpot adds to a new object (its own properties, the group `<name>_information` and associations with activities) and which fields wait for the properties. When the object file lists `<name>_information`, as a pull writes it once a property sits in it, that group's create step notes that apply gives HubSpot's group config's label instead.
49
+ - HubSpot's schemas list can show a custom object's values from before a write for some seconds after apply verified it, and can leave out an object just created. A plan made then blocks the object `settling` (above), never holds the write as drift.
50
+ - An update writes the whole schema in one request. A create and every update are `safe`: the display fields change forms and record views, not record values. A delete is `destructive`.
51
+ - Blocked `unsupported`: a create whose name HubSpot holds archived, since the create would purge the archived object with its records and association labels (restore it in HubSpot and pull, purge it in HubSpot, or choose another name); a create whose name differs only in case from a custom object HubSpot holds, since HubSpot keeps names unique ignoring case; and a create or update whose display, required or searchable field names a property the portal does not hold and the plan does not create. When the plan blocks that property's create (a limit, for example), the object's step is blocked `dependency-blocked`, naming the property.
52
+ - `kalup rm object:<name>` writes one tombstone that covers everything on the object: the plan has one delete step, which archives the object. Its title says how many of the object's properties, groups and pipelines go with it, and that HubSpot keeps no properties on an archived custom object; apply stops before any write when its own read finds more than the plan did. When the key cannot read the object's pipelines, the step is blocked `scope`, since the plan cannot count them. HubSpot refuses the archive while the object holds records. A tombstone on something under the object that asks otherwise than the object's is blocked with the reason. Takeover never archives a custom object.
43
53
 
44
54
  ## Pipelines and stages
45
55
 
@@ -55,7 +65,18 @@ The first rule that matches: a `skip` override (no step, `coverage.excluded`); a
55
65
 
56
66
  ## Tombstones, missing and orphans
57
67
 
58
- `hubspot/removed.ts` tombstones name properties, groups, pipelines and stages:
68
+ ## Associations
69
+
70
+ - A create sends the name and both labels; a plain association's create sends an empty label. Apply creates a pair's plain association before its labels. A label created on a pair with a custom object and no plain association makes one too, under a name HubSpot picks: the step's note says so, and pull writes it once an object of the pair sets `associations: true`.
71
+ - An update sends both labels. Risk: a create and an update are `safe`; a delete is `destructive`, since records lose the association.
72
+ - A plain association delete is blocked `unsupported` while a label of its pair remains, one HubSpot does not name yet included, unless the plan deletes that label first: HubSpot refuses it.
73
+ - Blocked `unsupported` too: a plain association config gives a label, or a label config holds as a plain association (add an entry under another name instead); a create whose label the pair shows already from the same object, a plain create on a pair that holds a plain association, and a label create that fits HubSpot's cap only once a delete in this plan has run (deletes run last: apply the delete, then plan again).
74
+ - An association the read did not find, on a pair whose lists hold a type HubSpot's schema read does not name yet, is blocked `settling`: it may be that type. `W_SETTLING` warns, and the read is incomplete. Plan again in a few minutes.
75
+ - HubSpot holds at most 50 labels per pair. Plan warns `W_LIMIT_HEADROOM` when its creates would pass the count Limits Tracking reports, and never blocks on it; HubSpot refuses the 51st with HTTP 437, which apply reports.
76
+ - Blocked `scope`: an association of a pair whose labels were not read or answered 403.
77
+ - Takeover never deletes an association. `notCovered` names association limits, which Kalup does not manage yet.
78
+
79
+ `hubspot/removed.ts` tombstones name custom objects, properties, groups, pipelines, stages and associations:
59
80
 
60
81
  - `release`: a `release` step drops the entry, even one naming another portal name; nothing is sent.
61
82
  - `destroy`, present: a `delete`, risk `destructive`, labelled `existed-before-kalup` for an adopted resource, expecting every base unit's live value. Blocked with `policy` without `allowDestroy: true`, `unsupported` when it is not archivable or a group still holds active properties the plan does not delete (archived ones do not block: HubSpot archives a group once every property in it is archived), `not-owned` without an owning entry.
@@ -64,7 +85,7 @@ The first rule that matches: a `skip` override (no step, `coverage.excluded`); a
64
85
 
65
86
  Under takeover (config.md), a `delete` labelled `takeover` archives each custom property and group in the pull scope that config lacks, with a `mode` note naming the statement that asked for it; an option removal takeover asks for carries the note too. One `Takeover on <objects>` heading precedes the first such step and says whether each is confirmed at a terminal or all are blocked. Blocked with `policy` without `allowDestroy`, `scope` after an incomplete read, `unsupported` when not archivable or a group keeps an active property. The `policy` fix leads with `kalup pull --target <t> --only <address>`, which keeps it in config, then `exclude` or `lifecycle: { options: 'additive' }` to leave it unmanaged, then `allowDestroy`. A delete expects every captured field's live value. Apply checks the same rules against its own read (a skipped group, a schema's properties, an empty group).
66
87
 
67
- Releases follow the config steps, then deletes, the tombstones' and then takeover's, properties before groups, then stages, then pipelines.
88
+ Releases follow the config steps, then deletes, the tombstones' and then takeover's, properties before groups, then stages, then pipelines, then association labels, then plain associations.
68
89
 
69
90
  `missing` lists owned resources a complete read did not find, with `archived` (`null` for a group) and the exits; `orphans`, owned entries config no longer names, with both `kalup rm` commands; one naming another portal name, only `--release`.
70
91
 
package/docs/pull.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pull
2
2
 
3
- `kalup pull [--target <name>]` reads one target and merges it into `hubspot/objects/*.ts`, `hubspot/pipelines/*.ts` and the target's `definition` overrides. It never writes to the portal (`E_WRITE_IN_READ_MODE`), records in state what the files and the portal agree on (below), and sanitizes portal strings it prints.
3
+ `kalup pull [--target <name>]` reads one target and merges it into `hubspot/objects/*.ts`, `hubspot/pipelines/*.ts`, `hubspot/associations.ts` and the target's `definition` overrides. It never writes to the portal (`E_WRITE_IN_READ_MODE`), records in state what the files and the portal agree on (below), and sanitizes portal strings it prints.
4
4
 
5
5
  This page is the reference. For the walk-through with examples, see [kalup pull](https://kalup.dev/docs/commands/pull) on the website.
6
6
 
@@ -8,7 +8,7 @@ This page is the reference. For the walk-through with examples, see [kalup pull]
8
8
 
9
9
  1. Validate, then pick the target (targets.md).
10
10
  2. The read key, then the portal guard (targets.md).
11
- 3. The read: custom object schemas when `objects` names one (or with `--discover`), then each object's properties (sensitive ones too) and groups, and its pipelines when they are in scope (below). A 403 on a list is `E_SCOPE`: that object is skipped and pull ends with `E_INCOMPLETE`. A 403 on the pipelines list skips only the object's pipelines.
11
+ 3. The read: custom object schemas when `objects` names one (or with `--discover`), then each object's properties (sensitive ones too) and groups, and its pipelines when they are in scope (below). A 403 on a list is `E_SCOPE`: that object is skipped and pull ends with `E_INCOMPLETE`. A 403 on the pipelines list skips only the object's pipelines. Then the labels of each object pair in association scope, both ways, and the schema read of each object of such a pair, for the internal names; a 403 on a labels list skips only that pair.
12
12
  4. Normalize. A `hubspotDefined` property, or one HubSpot calculates with a field type other than `calculation_equation`, becomes a reference; a custom `calculation_equation` property is managed with its `calculationFormula`. An owner property (select or radio) is `p.owner`, a `phone_number` one `p.phoneNumber`, rich text a `p.string` with `fieldType: 'html'`. A property Kalup does not write becomes a `p.string` reference, with `W_UNSUPPORTED_TYPE`: a `type` or custom `fieldType` no builder carries (`object_coordinates`, a rollup), or a custom `externalOptions` property that is no owner select or radio. A display field is kept only on the types that show it, and a field holding HubSpot's default is left out. Options are ordered by `displayOrder`, missing or negative last.
13
13
  5. Merge, with state where it owns a resource (below), then validate: an issue is `E_PULL_INVALID`, even with `--check`, and nothing is written.
14
14
  6. Write the changed files and `hubspot/index.ts` as one, each first copied to `.kalup/history/<timestamp>/`; a failure puts all back (`E_PROJECT_WRITE`).
@@ -22,6 +22,7 @@ This page is the reference. For the walk-through with examples, see [kalup pull]
22
22
  - `exclude: [...]`: internal names left out, `*` matching any run: never pulled, never archived by takeover. `include` wins over a pattern; a name in both is `E_SETTING_VALUE`, so `--discover` and the plan's notes say to take a listed name out of `exclude` rather than add it to `include`.
23
23
  - `as`: the export name for the first pull, by default PascalCase singular (`line_items` to `LineItem`).
24
24
  - `pipelines` (default `false`): every pipeline of the object. Without it, pull refreshes only the pipelines `hubspot/pipelines/<object>.ts` defines, and `--discover` lists the rest. `kalup init` sets it for deals and tickets.
25
+ - `associations` (default `false`): every association label and plain association between the object and the other objects under `objects`. Without it on either object of a pair, pull refreshes only the associations `hubspot/associations.ts` defines, and `--discover` lists the rest. `kalup init` sets it on every object it writes. HubSpot's own labels, and the plain association it defines between two standard objects, are never written.
25
26
 
26
27
  Under takeover (config.md), pull ends with a line naming what a plan for the target would archive once the files are as pull leaves them, such as what `--only` left out.
27
28
 
@@ -29,7 +30,7 @@ A portal property in scope is written unless `hubspot/removed.ts` names it or it
29
30
 
30
31
  ## Merge rules
31
32
 
32
- **Custom object schema**: its fields (config.md) take the portal value.
33
+ **Custom object schema**: its fields (config.md) take the portal value. A custom object config defines and the portal lacks is kept as the file has it, printed `missing in portal`; plan creates it. One `hubspot/removed.ts` names is never written back, with its pipelines, printed `in hubspot/removed.ts, not written back`, whether or not HubSpot still holds it. A key under `objects` that is neither a standard object, nor defined in config, nor in the portal is `E_UNKNOWN_OBJECT`.
33
34
 
34
35
  **Properties already in the file**:
35
36
 
@@ -48,6 +49,8 @@ A portal property in scope is written unless `hubspot/removed.ts` names it or it
48
49
 
49
50
  **Pipelines** merge by ID. A pipeline in both takes the portal's `label` and `displayOrder`; its stages take the portal's `label` and metadata field and follow the portal's order. A portal-only stage is added; a file-only stage is kept where it stood, printed `missing in portal`. A file pipeline the portal lacks is kept, printed `missing in portal`. With `pipelines: true`, a pipeline only the portal holds is appended to `hubspot/pipelines/<object>.ts`, the file created when needed, in `displayOrder` then ID order. Its export name is PascalCase of its label followed by `Pipeline` unless the label ends with it (`Sales Pipeline` to `SalesPipeline`); a stage's key is camelCase of its ID when the ID is a lowercase slug, else of its label, with `stage` in front of one starting with a digit. Names are made unique. The barrel re-exports every pipeline.
50
51
 
52
+ **Associations** merge by name. An entry in both takes the portal's `label` and `inverseLabel`, and keeps its key and comments; `inverseLabel` is left out when it equals the label. A file entry the portal lacks is kept, printed `missing in portal`. With `associations: true` on an object of the pair, an association only the portal holds is added to `hubspot/associations.ts`, the file created when needed, as `Associations`: from the object whose key sorts first, keyed camelCase of its name. The barrel re-exports `Associations`. A label HubSpot's schema read does not name yet, a few minutes after a create in the HubSpot UI, is not written until it does; one Kalup created is named by the type IDs state records.
53
+
51
54
  ## With state
52
55
 
53
56
  Where state holds agreed values for a resource (its base), each unit is compared with it, as `plan` does:
@@ -61,6 +64,8 @@ Where state holds agreed values for a resource (its base), each unit is compared
61
64
 
62
65
  `--accept <address[#unit]>` (repeatable, `*` as in `--only`) takes the portal side of those units, as each kept line prints; one matching nothing is `E_ACCEPT_UNMATCHED`.
63
66
 
67
+ A resource settling after an apply (plan.md) is left as the file has it and gets no base, as for an address `--only` leaves out, with `W_SETTLING`: for minutes after a write HubSpot can serve the copy from before it, and pulling that copy would undo the change in config. `--check --exit-code` counts it as pending, exit 2.
68
+
64
69
  After the files are written, and only after a complete read, pull records in state the base of every unit the files and the portal agree on, for the addresses `--only` selects: under the portal lock (`E_LOCKED`) with the serial check, as apply saves. An owned entry keeps its origin; an address no entry owns gets origin `pulled`, which owns nothing: the next plan still adopts it, but compares against that base, so a later file edit is a `config-change`, not `diverged`. A field the files leave out because HubSpot holds its default (an empty description, `formField` off, an option with no description) is recorded at that default, so adding it to the file later is a `config-change` too. A unit that still differs keeps its base. `--check` and `--discover` record nothing and take no lock. A plan saved before the pull no longer applies (`E_STATE_CHANGED`). The pull that creates the state file prints its path. A new object file that gets more than 200 properties warns `W_LARGE_SCOPE`: the scope `init` writes takes every custom property.
65
70
 
66
71
  ## Target overrides
@@ -72,9 +77,9 @@ After the files are written, and only after a complete read, pull records in sta
72
77
  ## Flags
73
78
 
74
79
  - `--only <glob>`: merge only matching addresses. `*` matches any characters, `/` included: `property:companies/*`, `stage:deals/renewals/*`.
75
- - `--discover`: list what is outside the scope, write nothing: custom objects config does not name, properties, and the pipelines of each object without `pipelines: true` that the files do not define.
80
+ - `--discover`: list what is outside the scope, write nothing: custom objects config does not name, properties, the pipelines of each object without `pipelines: true` that the files do not define, and the associations between objects under `objects` that pull does not write.
76
81
  - `--check`: print the changes and `would write <file>`, write nothing. With `--exit-code`, exit 2 on any difference: a change line but `skipped`, `in hubspot/removed.ts`, a new property in a removed group, `config change kept` or `ignored on this target`, a `W_CODEC_MISMATCH`, or a file to rewrite.
77
- - `--json`: `data` holds `target`, `portalId`, `objects` (counts and `changes[]` per object: `kind`, `address`, and `field`, `before` and `after` when one field differs; a kept value has the file's side in `before`, the portal's in `after`; `kind` is `added`, `changed`, `missing`, `local-only`, `excluded`, `shadowed`, `removed`, `removed-group`, `kept`, `conflict`, `removed-in-hubspot`, `ignored` or `override-group`), `files` and `state` (`path`, `recorded`, the resources whose base changed, and `serial`; absent with `--check` or after an incomplete read); with `--discover`, what is outside the scope: `objects`, `properties` and `pipelines` (pipeline IDs by object).
82
+ - `--json`: `data` holds `target`, `portalId`, `objects` (counts and `changes[]` per object: `kind`, `address`, and `field`, `before` and `after` when one field differs; a kept value has the file's side in `before`, the portal's in `after`; `kind` is `added`, `changed`, `missing`, `local-only`, `excluded`, `shadowed`, `removed`, `removed-group`, `kept`, `conflict`, `removed-in-hubspot`, `ignored` or `override-group`), `files` and `state` (`path`, `recorded`, the resources whose base changed, and `serial`; absent with `--check` or after an incomplete read); with `--discover`, what is outside the scope: `objects`, `properties`, `pipelines` (pipeline IDs by object) and `associations` (addresses).
78
83
 
79
84
  ## Exit codes
80
85
 
package/docs/rm.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Remove
2
2
 
3
- `kalup rm <address> [--release] [--json]` takes a property, property group, pipeline or stage out of config and writes its tombstone in `hubspot/removed.ts`. Absence never deletes: a resource dropped from an object file by hand stays in the portal and comes back on the next pull. `rm` is the only way to ask for a delete, and it works offline: it reads no key, sends no request and never touches state.
3
+ `kalup rm <address> [--release] [--json]` takes a custom object, property, property group, pipeline, stage or association out of config and writes its tombstone in `hubspot/removed.ts`. Absence never deletes: a resource dropped from an object file by hand stays in the portal and comes back on the next pull. `rm` is the only way to ask for a delete, and it works offline: it reads no key, sends no request and never touches state.
4
4
 
5
5
  This page is the reference. For the walk-through with examples, see [kalup rm](https://kalup.dev/docs/commands/rm) on the website.
6
6
 
7
7
  ## What it writes
8
8
 
9
- - The address must be `property:<object>/<name>`, `group:<object>/<name>`, `pipeline:<object>/<id>` or `stage:<object>/<pipelineId>/<stageId>` (`E_TOMBSTONE_ADDRESS`, exit 3). Custom objects are not removed in this release.
9
+ - The address must be `object:<name>`, `property:<object>/<name>`, `group:<object>/<name>`, `pipeline:<object>/<id>`, `stage:<object>/<pipelineId>/<stageId>` or `association:<from>/<to>/<name>` (`E_TOMBSTONE_ADDRESS`, exit 3).
10
+ - A custom object leaves config with everything on it: its export, every pipeline export on the object and every association entry with the object on either side, and each file left with no export or entry goes. Its one tombstone covers its groups, properties, pipelines, stages and associations. Its entry under `objects` in `kalup.config.ts` stays, so the plan reads the object.
10
11
  - The property, or the group entry, leaves the export that defines it, in whichever file holds it. An export left with no properties keeps its groups; `defineObject` accepts it.
11
- - A pipeline's export leaves `hubspot/pipelines/<object>.ts`, and the file goes when it held no other. Its one tombstone covers its stages. A stage leaves its pipeline's `stages`.
12
+ - A pipeline's export leaves `hubspot/pipelines/<object>.ts`, and the file goes when it held no other. Its one tombstone covers its stages. A stage leaves its pipeline's `stages`. An association's entry leaves `hubspot/associations.ts`, and the file goes when it held no other.
12
13
  - `hubspot/removed.ts` gets `'<address>': { action: 'destroy' }`, or `'release'` with `--release`. An address already there gets its action changed; the same action writes nothing.
13
14
  - An address config does not define (an orphan the plan lists) gets the tombstone alone.
14
15
  - `hubspot/index.ts` is written again.
@@ -17,13 +18,13 @@ Before writing, rm validates the project as it would leave it; any issue is exit
17
18
 
18
19
  ## What it refuses
19
20
 
20
- - `destroy` for a resource with `lifecycle: { preventDestroy: true }` (`E_PREVENT_DESTROY`, exit 3). Remove preventDestroy first, or use `--release`.
21
+ - `destroy` for a resource with `lifecycle: { preventDestroy: true }` (`E_PREVENT_DESTROY`, exit 3), or for a custom object while anything on it sets it, since the archive takes them along. Remove preventDestroy first, or use `--release`.
21
22
  - A group that config properties use, or a property a custom object schema in config names as a display, required or searchable property (`E_RM_DEPENDENTS`, exit 3). This applies to `--release` too.
22
23
  - A pipeline's last stage, or a ticket pipeline's last closed stage (`E_PIPELINE_STAGES`, exit 3): HubSpot refuses both. Remove the pipeline instead, or mark another stage closed first.
23
24
 
24
25
  ## Destroy and release
25
26
 
26
- A `destroy` tombstone becomes a delete in the next plan only when all of these hold: the portal's state owns the resource (Kalup created or adopted it there), the target sets `allowDestroy: true`, and a person at a terminal types the target name and the number of destructive steps when applying (apply.md). HubSpot archives a deleted property; it can be restored in HubSpot for 90 days. A deleted pipeline or stage is gone for good: HubSpot keeps no archive, and refuses the delete while a record sits in the stage.
27
+ A `destroy` tombstone becomes a delete in the next plan only when all of these hold: the portal's state owns the resource (Kalup created or adopted it there), the target sets `allowDestroy: true`, and a person at a terminal types the target name and the number of destructive steps when applying (apply.md). HubSpot archives a deleted property; it can be restored in HubSpot for 90 days. HubSpot archives a deleted custom object with what is on it, and refuses while the object holds records; Kalup never purges it. A deleted pipeline or stage is gone for good: HubSpot keeps no archive, and refuses the delete while a record sits in the stage. A deleted association label is gone for good too, and records lose it; HubSpot refuses to delete a plain association while a label of its pair remains, so remove the labels first.
27
28
 
28
29
  A `release` tombstone stops Kalup managing the resource. The portal keeps it, the next apply drops its state entry without a request, and pull never writes it back into config (pull.md).
29
30
 
package/docs/snapshot.md CHANGED
@@ -7,7 +7,7 @@ This page is the reference. For the walk-through with examples, see [kalup snaps
7
7
  ## Order of work
8
8
 
9
9
  1. Validate (exit 3), pick the target (targets.md), the read key, then the portal guard (exit 4).
10
- 2. Pull's read and scope: three properties lists per object, and its pipelines when they are in scope, `skip` and `name` overrides applied. A 403 leaves that object unread; on the pipelines list, only its pipelines.
10
+ 2. Pull's read and scope: three properties lists per object, its pipelines when they are in scope, and the associations of each pair in association scope, `skip` and `name` overrides applied. A 403 leaves that object unread; on the pipelines list, only its pipelines; on a labels list, only that pair's associations.
11
11
  3. Write the file, then print a summary.
12
12
 
13
13
  ## The file
@@ -16,7 +16,7 @@ By default the file is `.kalup/snapshots/<target>/<stamp>.json` under the projec
16
16
 
17
17
  The file is an `ir/1` document with `generator.frontend: 'portal'`, keys sorted, and U+007F to U+009F, U+2028 and U+2029 escaped:
18
18
 
19
- - `resources`: what the read captured, under local addresses. Groups carry their label. A custom property carries its definition; a HubSpot-defined or calculated one at most its options. A custom object carries its labels, display property and property lists. Where one names a portal resource a `name` override shadows (a property's group, a schema's property), it reads `shadowed:<name>`, which never equals a config name.
19
+ - `resources`: what the read captured, under local addresses. Groups carry their label. A custom property carries its definition; a HubSpot-defined or calculated one at most its options. A custom object carries its labels, description, display property and property lists. Where one names a portal resource a `name` override shadows (a property's group, a schema's property), it reads `shadowed:<name>`, which never equals a config name.
20
20
  - `observation`: the target's name and `portalId`, `observedAt`, the one timestamp an IR document may hold, and `coverage`.
21
21
 
22
22
  `coverage` has `complete`, `otherObjects` (custom objects config does not name, or `unknown` when the schemas list was not read), `notCaptured` (documented fields Kalup drops) and one entry per object key: `status` (`read`, `unreadable`, `absent` or `excluded`), `missingScope` when unreadable, `objectTypeId`, the names out of scope, `unaddressable` (config names them), shadowed by a `name` override or unsupported (Kalup does not write it; `externalOptions` and `referencedObjectType` are kept), `unsupportedSchema` for a schema without a label, and the addresses `skip` and `name` overrides exclude or rename.
@@ -31,7 +31,7 @@ A snapshot holds the scope and the captured fields, and is no backup of the port
31
31
 
32
32
  ## Incomplete reads
33
33
 
34
- The file is still written, with `complete: false`, the objects not read marked `unreadable` and `unaddressable` properties listed, and `W_INCOMPLETE` names the fix. The exit stays 0.
34
+ The file is still written, with `complete: false`, the objects not read marked `unreadable` and `unaddressable` properties listed, and `W_INCOMPLETE` names the fix. The exit stays 0. A resource settling after an apply (plan.md) is listed under `coverage.settling` with when its window ends, compare reports it unknown, and `W_INCOMPLETE` says to take a new snapshot after that time.
35
35
 
36
36
  ## Output
37
37
 
@@ -40,7 +40,7 @@ Snapshot of target sandbox, portal 1111111, observed at 2026-09-23T10:15:30.123Z
40
40
  Wrote .kalup/snapshots/sandbox/20260923T101530123Z.json
41
41
  ```
42
42
 
43
- `--json` gives `file`, `target`, `portalId`, `observedAt`, `complete` and `counts` (objects read, groups, properties, pipelines and stages held). The text names pipelines and stages only when the snapshot holds any.
43
+ `--json` gives `file`, `target`, `portalId`, `observedAt`, `complete` and `counts` (objects read, groups, properties, pipelines, stages and associations held). The text names pipelines, stages and associations only when the snapshot holds any.
44
44
 
45
45
  ## Exit codes
46
46
 
package/docs/state.md CHANGED
@@ -15,9 +15,10 @@ Without `--write` it is read-only: it checks the read key's portal, reads the ta
15
15
  - `found`: config resources the portal holds, with how many units config and the portal agree on.
16
16
  - `missing`: config resources a complete read did not find.
17
17
  - `stale`: current entries that record another portal name, whose resource the portal no longer holds, or whose address is no longer in config.
18
- - `excluded`: tombstoned addresses, skipped ones, and ones the read could not see or Kalup does not write.
18
+ - `kept`: entries of a type this version does not manage, which a later version of Kalup wrote. The rebuild keeps them as they are, unchecked, and keeps the state file's fields it does not know.
19
+ - `excluded`: tombstoned addresses, skipped ones, ones the read could not see or does not trust yet because they are settling after an apply (plan.md), and ones Kalup does not write.
19
20
 
20
- With `--write`, only a person at a terminal may run it (else `E_APPROVAL_REQUIRED`, exit 4); `--yes` and `--approve` are refused. It checks the write key's portal and refuses an incomplete read (`E_INCOMPLETE`: a rebuild would drop what it could not check). It shows the report and what the current file loses for good (the `created` origin, agreed values, entries not in config), asks for the target name, takes the portal lock, refuses a state file changed since the report (`E_STATE_CHANGED`), archives the current file under `.kalup/state/archive/` (ending its lineage; also with `state: 'repo'`), and writes a new lineage at serial 1: an `adopted` entry for every found resource, with a base for the units that agree. A tombstoned address is never adopted. Nothing is sent to the portal.
21
+ With `--write`, only a person at a terminal may run it (else `E_APPROVAL_REQUIRED`, exit 4); `--yes` and `--approve` are refused. It checks the write key's portal and refuses a read that left out what config names (`E_INCOMPLETE`: a rebuild would drop what it could not check): a list it could not read, a property it could not capture, a resource settling after an apply (run it again after the time it names), or an association a type HubSpot does not name yet may be. What settles where config names nothing does not stop it. It shows the report and what the current file loses for good (the `created` origin, agreed values, entries not in config), asks for the target name, takes the portal lock, refuses a state file changed since the report (`E_STATE_CHANGED`), archives the current file under `.kalup/state/archive/` (ending its lineage; also with `state: 'repo'`), and writes a new lineage at serial 1: an `adopted` entry for every found resource, with a base for the units that agree. A tombstoned address is never adopted. Nothing is sent to the portal.
21
22
 
22
23
  Plans saved before a rebuild are refused by apply (`E_STATE_CHANGED`); plan again.
23
24
 
@@ -27,7 +28,7 @@ Plans saved before a rebuild are refused by apply (`E_STATE_CHANGED`); plan agai
27
28
 
28
29
  ## Output
29
30
 
30
- `--json` data for rebuild: `target`, `portalId`, `statePath`, `found`, `missing`, `stale`, `excluded`, `loses` and `written`, always `false`, since `--write --json` exits 4. Rebind with `--json` exits 4 with no data.
31
+ `--json` data for rebuild: `target`, `portalId`, `statePath`, `found`, `missing`, `stale`, `excluded`, `kept` when there are any, `loses` and `written`, always `false`, since `--write --json` exits 4. Rebind with `--json` exits 4 with no data.
31
32
 
32
33
  ## Exit codes
33
34
 
package/docs/targets.md CHANGED
@@ -45,7 +45,7 @@ The key goes out as `Authorization: Bearer`. `init` prints the read scopes the p
45
45
  `overrides` is keyed by address: `property:<object>/<name>`, `group:<object>/<name>` or `object:<name>`. A key that is not an address in config is `E_UNKNOWN_OVERRIDE`. Any field other than these four is `E_NOT_DATA`:
46
46
 
47
47
  - `name: '<portal name>'`: the resource's internal name in this portal, or for a pipeline or stage its ID there. Every read uses it; `plan` blocks it when the portal lacks it. `E_OVERRIDE_AMBIGUOUS` when the portal holds both names; `E_OVERRIDE_NAME` when two addresses would read one portal resource.
48
- - `skip: true`: left out on this target by every read, a group with its config properties, a pipeline with its stages.
48
+ - `skip: true`: left out on this target by every read, a group with its config properties, a pipeline with its stages, an association alone.
49
49
  - `definition: {...}`: the fields that differ on this target (config.md).
50
50
  - `lookup`: no lookup resource is managed yet; `plan` blocks the resource, `compare` reports it unknown.
51
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kalup",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Configuration as code for HubSpot: pull, compare, plan and apply properties and property groups",
5
5
  "keywords": [
6
6
  "hubspot",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@oclif/core": "^5.1.2",
66
- "@kalup/core": "0.4.0"
66
+ "@kalup/core": "0.6.0"
67
67
  },
68
68
  "engines": {
69
69
  "node": ">=22.13.1"