Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .changeset/upgrade-objectstack-17-rc2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
'hotcrm': minor
---

Upgrade HotCRM to ObjectStack 17.0.0-rc.2, and migrate the six places where the
rc.1 → rc.2 window changed what the platform does with metadata this app already
had. Four of them were live defects, not tidy-ups — each is verified against a
booted server and a real browser session, not only against the unit harness.

**`demo_bootstrap` could no longer find its user.** The sweep opened with
`get_record(sys_user)` and an empty filter, which `findOne` used to answer with
an arbitrary row. 17.0.0-rc.2 refuses a `findOne` that names no record (#4419),
so the flow failed on its second node and every seeded record stayed ownerless —
"My Leads", "My Deals" and "My Cases" empty for everyone, and every owner-addressed
`notify` reaching nobody. The read is now an explicit `find` whose first row is
bound by an `assignment` node, which states the arbitrary pick instead of
smuggling it through a call that claimed to name one. FROM `filter: {}` TO
`limit: 2` + `firstUser = {userList.0}`, with `has(vars.firstUser)` guards on the
branch so a zero-user org still completes.

**`lead_conversion` could no longer convert a lead.** rc.2 holds a screen resume
to the screen's declared field contract (#4477), and `createOpportunity` — a
checkbox with `defaultValue: false` — was marked `required`. A runner that posts
only what the user touched left it out, and the resume was refused outright with
`INVALID_SCREEN_INPUT`. A checkbox has no unanswered state, so the flag is gone;
the default and the `init_defaults` assignment supply the answer, as they always
actually did.

**Nine `decision` nodes carried an inert copy of their branch predicate.** rc.2
flags it (`flow-inert-node-condition`, #4414): the engine reads the out-edges, so
a second copy on the node restates the gate without being the gate, and a copy
that drifts is a lie about what the flow does. The copies are deleted and the
totality rationale moved to the edges that decide. Behaviour is unchanged — the
edges always were the live sites.

**`translation.validationMessages` is removed** from all four locale bundles.
rc.2 retires the key (#4667); the three messages under it matched no rule in this
app and had never been read.

Two pinned "platform gap" assertions flip because the platform closed the gap,
which is what they were written to detect: a filtered measure that selects nothing
now reports `0` rather than nothing at all, so a lead source that only ever lost
reads **0%** instead of blank (#4708); and a bare-string condition inside a `loop`
body is now CEL-parsed like its envelope twin (#4336). The explicit envelopes stay
— they say which dialect a predicate is in, and they keep these flows correct on a
runtime that still carries the old path.

Finally, validation predicates now fail **closed** (#4649) — the upstream question
`test/object-validation-predicates.test.ts` filed, answered. An unevaluable
predicate used to be skipped silently; it now rejects the write. HotCRM's
predicates are already total, so nothing changes at runtime, but the house rule in
AGENTS.md and that file now describe the outcome an author actually gets.
14 changes: 9 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,19 @@ fills absent fields with `null` **only on insert**. On update, `previous` is
whatever the driver returned — and a driver that stores only the columns a row
was actually written with (`driver-memory`, `driver-mongodb`) hands back a
record with the key **absent**, not null. Strict CEL then aborts the whole
predicate with `No such key`, and the engine's answer to a predicate that
cannot answer is to **skip the rule**:
predicate with `No such key` — and what the engine does next changed under us:

```
WARN Validation rule 'x' predicate failed to evaluate (type: No such key: y) — skipped
≤ 17.0.0-rc.1 WARN Validation rule 'x' predicate failed to evaluate (…) — skipped
≥ 17.0.0-rc.2 WARN … — write rejected (#4649)
ValidationError: Validation rule 'x' could not be evaluated … — write rejected.
```

No error, no failed save — just a rule that reads as enforced and requires
nothing. So **every `record.x` read carries a `has(record.x)` guard**:
Through rc.1 a rule that could not answer required nothing at all, silently.
From rc.2 it **fails closed**: the same abort rejects the save. Neither is the
rule the author wrote — one under-enforces, the other blocks an ordinary save on
a record shape nobody considered — and one guard prevents both. So **every
`record.x` read carries a `has(record.x)` guard**:

| intent | write this |
| --- | --- |
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ Everything merged after the 2.2.2 tag. Not yet versioned or published.

### Changed

- **ObjectStack platform → 17.0.0-rc.2** across all `@objectstack/*` packages (from 17.0.0-rc.1); `specVersion` and `engines.protocol` follow. Four of the rc.1 → rc.2 changes were live defects in metadata this app already had, and each was verified against a booted server and a real browser session:
- **`demo_bootstrap` could no longer find its user.** The sweep opened with `get_record(sys_user)` on an empty filter, which `findOne` used to answer with an arbitrary row; rc.2 refuses a `findOne` that names no record (upstream #4419), so the flow failed on its second node and every seeded record stayed ownerless — the "My …" views empty for everyone and every owner-addressed `notify` reaching nobody. It now reads a list and binds its first row through an `assignment` node, which states the arbitrary pick rather than smuggling it through a call that claimed to name one. Guarded with `has(vars.firstUser)` so a zero-user org still completes. Re-verified end to end: one manual run claimed 115 records across all eight objects, both ownership columns.
- **`lead_conversion` could no longer convert a lead.** rc.2 holds a screen resume to the screen's declared field contract (#4477), and `createOpportunity` — a checkbox with `defaultValue: false` — was marked `required`, so a runner posting only what the user touched had its resume refused with `INVALID_SCREEN_INPUT`. A checkbox has no unanswered state; the flag is gone and the default (plus the `init_defaults` assignment) supplies the answer, as it always actually did.
- **Nine `decision` nodes carried an inert copy of their branch predicate**, now flagged by rc.2's `flow-inert-node-condition` (#4414). The engine reads the out-edges, so the node copy restated the gate without being the gate. Copies deleted, totality rationale moved to the edges. Behaviour unchanged, and re-verified per flow in the browser: two-tier approval routing (>$500K → `director_signoff`, ≤$500K → approved), `lead_assignment`'s 1-day/3-day SLA partition including the unrated lead, and `quote_generation` advancing `needs_analysis` → `proposal`.
- **`translation.validationMessages` removed** from all four locale bundles — rc.2 retires the key (#4667), and the three messages under it matched no rule in this app.
- Two pinned "platform gap" assertions flip because the platform closed the gap: a filtered measure that selects nothing now reports `0`, so a lead source that only ever lost reads **0%** rather than blank (#4708); and a bare-string condition inside a `loop` body is now CEL-parsed like its envelope twin (#4336). The explicit envelopes stay — they declare the dialect and keep these flows correct on a runtime that still carries the old path.
- Validation predicates now fail **closed** (#4649) — the upstream question `test/object-validation-predicates.test.ts` filed, answered. An unevaluable predicate used to be skipped silently and now rejects the write. HotCRM's predicates are already total, so nothing changes at runtime; AGENTS.md and that file now state the outcome an author actually gets.
- **ObjectStack platform → 17.0.0-rc.1** across all `@objectstack/*` packages (from 16.1.0). The manifest protocol handshake and marketplace `specVersion` now declare the 17.0.0-rc.1 compatibility line; API methods, skills and flow notifications use only live metadata keys; pre-17 required fields preserve their database `NOT NULL` constraints explicitly; and the analytics semantic layer now declares the month/day/quarter buckets v17 executes correctly.
- **`allowExport` is authored on every profile that needs it (17.0 opt-in export axis, upstream #3544).** Before 17.0 an unset `allowExport` inherited read, so "can list ⇒ can export". 17.0 inverted the default: `resolveUserExportAllowed` demands an explicit `allowExport: true` and neither `viewAllRecords` nor `modifyAllRecords` substitutes, so an unset bit now DENIES — at both bulk-egress doors, the list views' built-in `exportOptions` and `ReportService.assertExportAllowed`. No profile carried the bit, which would have 403'd every CSV/XLSX and report export for every user, `system_admin` included, while `os validate`, `os build` and the whole test suite stayed green. The five objects with a real export surface (`crm_account`, `crm_case`, `crm_contact`, `crm_lead`, `crm_opportunity`) now carry the grant on each profile that already reads them; `guest_portal` deliberately carries none (ADR-0090 D9 forbids binding a high-privilege set to the `guest` anchor). Pinned by `test/authorization-coverage.test.ts`, which fails both on an export surface nobody can reach and on a grant with no surface behind it.
- Approval nodes in `opportunity_approval` use the native `onEmptyApprovers: 'admin_rescue'` policy instead of the hand-rolled `org_membership_level: 'owner'` approver. That entry existed only to avoid the empty-position dead-end, and with `behavior: 'first_response'` it overshot — it made an org owner a routine approver on every deal over $100K, not a rescue when the bench is empty.
Expand Down
4 changes: 2 additions & 2 deletions objectstack.manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"$schema": "https://schemas.objectstack.dev/template-manifest.json",
"name": "hotcrm",
"specVersion": "^17.0.0-rc.1",
"specVersion": "^17.0.0-rc.2",
"engines": {
"protocol": "^17.0.0-rc.1"
"protocol": "^17.0.0-rc.2"
},
"manifestId": "app.objectstack.hotcrm",
"displayName": "HotCRM",
Expand Down
Loading
Loading