@vectros-ai/cli 0.12.0 → 0.13.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.
package/AUTHORING.md CHANGED
@@ -134,7 +134,9 @@ schemas:
134
134
  required: true
135
135
  lookupFields: [title] # extra O(1) exact-match lookup indexes (max 10);
136
136
  # object form { fieldName, unique?, rangeEnabled?,
137
- # sortBy?, allowOverflow? } for constraints/range.
137
+ # sortBy?, allowOverflow? } for constraints/range, or
138
+ # { fieldNames: [a, b], sortBy?, allowOverflow? } for
139
+ # a composite (conjunctive) lookup over 2-3 fields.
138
140
  # externalId has a built-in finder — don't list it.
139
141
  capabilities: { auditHistory: true } # default true; surfaced for a self-documenting audit posture
140
142
 
@@ -191,12 +193,15 @@ before customers build on the schema. Three knobs, each permanent:
191
193
  set `allowOverflow: true` on it, which accepts a higher-cost secondary index.
192
194
  Keep equality lookups lean.
193
195
 
194
- - **`sortBy` — the order results come back in.** For an *equality* lookup,
195
- `sortBy` sets the listing order: `createdAt` (default), `lastUpdated`, or a
196
- declared field. *Trap:* sorting by an **optional** payload field silently drops
197
- records that don't have it from that lookup — and it's locked. Use `createdAt`/
198
- `lastUpdated` (always present) unless every record is guaranteed to carry the
199
- field.
196
+ - **`sortBy` — the order results come back in, and what a sort-key window
197
+ narrows.** For an *equality* lookup, `sortBy` sets the listing order:
198
+ `createdAt` (default), `lastUpdated`, or a declared field. It is also what
199
+ `sortFrom`/`sortTo` bound when you narrow a `value` lookup. An **optional**
200
+ field is fine: records that don't carry it are listed ahead of those that do,
201
+ and a bounded window never includes them. *Locked:* `sortBy` itself AND the
202
+ sorted field's declared `fieldType` are both migration-locked — pick a new field
203
+ name if you need to change either. An `array`/`object` field is rejected as a
204
+ `sortBy` target (no meaningful order).
200
205
 
201
206
  **Sensitive fields can still be looked up — by exact match only.** A
202
207
  `sensitive` field is HMAC'd into a per-tenant **blind index**, so you *can* list
@@ -205,6 +210,49 @@ without the value ever being stored in the clear or entering the search index. A
205
210
  blind hash is not orderable, so a sensitive lookup **cannot** be `rangeEnabled`,
206
211
  and no lookup's `sortBy` may name a sensitive field (the platform rejects both).
207
212
 
213
+ ### Composite (conjunctive) lookups — matching several fields at once
214
+
215
+ `{ fieldNames: [a, b] }` (2-3 fields) matches ALL of them together in one clause —
216
+ "every record where `status` is `open` **and** `area` is `billing`" — exact,
217
+ complete, in the declared order. This is a different query shape from declaring
218
+ `status` and `area` as two separate equality lookups: two separate lookups can
219
+ each tell you "records with this status" or "records with this area", but never
220
+ "records with both", short of fetching one side and filtering client-side.
221
+
222
+ ```yaml
223
+ lookupFields:
224
+ - externalId
225
+ - { fieldNames: [status, area], sortBy: createdAt, allowOverflow: true }
226
+ ```
227
+
228
+ Like a single-field lookup, the shape is **migration-locked** at create — the same
229
+ "decide deliberately" rule as above applies, doubly so here since a composite
230
+ also can't be swapped for two plain lookups later without a new field name.
231
+ Rules the platform enforces, all offline-checkable and caught by `blueprint
232
+ validate` before they'd 400 at `bootstrap`:
233
+
234
+ - **2-3 fields, order significant.** A query may match a leading run of the list
235
+ (the first field alone, the first two together, …) but never a later field
236
+ alone — declare a separate lookup for that. No leg may repeat.
237
+ - **Each leg must be a scalar, single-valued field** — not `array`/`object`, and
238
+ not a `many`-cardinality `reference`. A leg occupies one sort-key position and
239
+ holds one value; a multi-valued field there would index only one rendering and
240
+ the conjunction would silently return a subset while looking complete.
241
+ Cardinality-`one` references are fine.
242
+ - **A leg can't ALSO be its own `sortBy` target**, on this same lookup or a
243
+ separate one that treats that field as `rangeEnabled`. Within a fixed tuple
244
+ every matching record has the same value for that leg, so ordering by it (or
245
+ range-querying it) is meaningless — pick a different field, `createdAt`, or
246
+ `lastUpdated`.
247
+ - **`unique`/`rangeEnabled` are not available on a composite** — it's an
248
+ exact-match index over its fields; declare a range lookup separately if you
249
+ need one. `sortBy` and `allowOverflow` ARE available; `sortBy` on a composite
250
+ orders *within* a group when a query supplies fewer values than the lookup
251
+ declares, not across the whole result.
252
+ - **Record-only.** The schema's `allowedSurfaces` must be omitted or exactly
253
+ `[record]` — the platform's composite index has no document/user/entity reader
254
+ yet.
255
+
208
256
  ### Reference fields — typed links between record types
209
257
 
210
258
  A `reference` field is a foreign key to another record. It needs **both** the
package/CHANGELOG.md CHANGED
@@ -3,6 +3,92 @@
3
3
  All notable changes to `@vectros-ai/cli` are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org).
5
5
 
6
+ ## 0.13.0
7
+
8
+ ### Added
9
+
10
+ - **`access explain` shows the row for "this record IS the identity you asked about".** A clause
11
+ bounding a namespace that belongs to an identity entity — a value the platform derives from the
12
+ entity's own id rather than one stored on the record — previously produced no matrix row for it at
13
+ all, because the matrix only built rows from values a clause spelled out literally. The matrix now
14
+ always carries that row, so a clause confining a credential to its own entity can be checked the same
15
+ way as every other ownership shape.
16
+ - **`access explain`'s grant matrix shows exactly which record types a row reaches, not only whether
17
+ it's granted.** A new "record types" column (`recordTypes` in `--json` output) names the types a
18
+ row's granting clause(s) actually cover — every type, a specific list, or none. Previously an
19
+ ownership-unscoped but type-restricted clause (a curated shared-content role, for example) rendered as
20
+ a plain "granted", which read as reaching every record type sharing that ownership shape, including
21
+ ones the clause never actually named.
22
+ - **`access explain` understands the placement matchers.** `${{ any }}` renders as `any` and matches
23
+ any value present in that dimension — deliberately *not* a record with no value there, since it does
24
+ not subsume the `null` sentinel (`[${{ any }}]` and `[${{ any }}, null]` are different grants).
25
+ `${{ under.self.* }}` renders as `under-self`; deciding it requires the parent edge of every
26
+ candidate value, which only the server has, so the matrix reports it as undecidable rather than
27
+ guessing. Previously both were compared as literal strings, so a clause using either was shown as
28
+ matching only a record whose owner value was the placeholder text itself.
29
+ - **New lint rule `datascope-unknown-placeholder` (error).** A `${{ … }}` value in a `dataScope` that
30
+ is not one of the platform's placeholders is now caught offline, with the correct spellings in the
31
+ message, instead of surfacing as a `400` at `apply`. Blueprint-level tokens (`${{ identities.* }}`,
32
+ `${{ inputs.* }}`, …) are substituted before the value reaches the platform and are not flagged.
33
+ - **A blueprint schema can now declare a composite (conjunctive) lookup** — `lookupFields: [{ fieldNames:
34
+ ['status', 'area'] }]` matches on several fields at once ("every record where `status` is `open` **and**
35
+ `area` is `billing`"), 2-3 fields, exact and complete, in the declared field order. `blueprint validate`
36
+ now catches every leg-shape mistake the platform would otherwise 400 on mid-`bootstrap`: an undeclared
37
+ leg (naming the exact leg, not just the first), a repeated leg, a leg that's `array`/`object`-typed or a
38
+ many-cardinality reference, a leg that's separately declared `rangeEnabled`, and a leg used as its own
39
+ `sortBy` target. `bootstrap`/`blueprint-test apply` forward a composite to the partner API unchanged, and
40
+ the approval plan (`bootstrap`'s "Apply this plan?" preview) now renders one as `(status+area)` rather
41
+ than dropping it from the lookup list. `unique`/`rangeEnabled` aren't available on a composite; `sortBy`
42
+ and `allowOverflow` are. No bundled blueprint declares one yet — adopting the shape in the bundled
43
+ library is a separate, deliberate content decision.
44
+
45
+ ### Changed
46
+
47
+ - **`--scope` now validates the value locally, on every command that takes it** — `join`,
48
+ `access grant` and `identity create`. A scope value must be 1-128 characters, starting with a letter
49
+ or digit and otherwise containing only letters, digits, `_` and `-` — the grammar the API enforces,
50
+ because a scope value becomes part of a storage key. `--scope org:a:b:c` previously passed a
51
+ colon-bearing value straight through; it now fails immediately, naming the offending value, instead
52
+ of returning a server error later in the call.
53
+ - **`access grant --identity-overrides` validates its values too.** The JSON form wins on key
54
+ conflicts with `--scope`, so a value rejected on one flag could previously be reinstated by the other
55
+ in the same command. Both are now checked after the merge.
56
+ - **Dependency maintenance** — repinned the bundled `@vectros-ai/sdk` to `0.38.0` (composite/conjunctive
57
+ record lookups — a schema can now be looked up by 2-3 fields matched together in one clause, not just
58
+ one — plus the placement matchers `${{ any }}` / `${{ under.self.* }}`, the `"*"` dimension wildcard in
59
+ `data_scope`, and sealed pagination cursors) and the bundled `@vectros-ai/blueprints` to `0.10.0` (the
60
+ composite-lookup authoring format above). The CLI bundles both into its published binary, so this is a
61
+ rebuild against their current versions.
62
+
63
+ ### Fixed
64
+
65
+ - **`access explain` no longer reports a fully-permissive scope as granting nothing.** A clause using
66
+ the dimension wildcard — `data_scope: {"*": [...]}`, which grants over every dimension the clause
67
+ does not name explicitly — was read as naming an *unrecognized* dimension, and an unrecognized
68
+ dimension kills a whole clause. The most permissive shape there is therefore rendered as an empty
69
+ matrix. `*` is now recognized, and it is applied only to the dimensions a record actually carries,
70
+ with an explicit entry for a dimension always taking precedence over it.
71
+ - **`access explain` now says plainly that its "granted?" and "actions" columns model reading a record,
72
+ not writing one.** A clause that doesn't mention an ownership dimension no longer authorizes placing a
73
+ value there, but it never narrowed reads to begin with — so a create/update verb next to a row is not
74
+ confirmation that a write with that row's exact ownership shape will succeed. The command's output
75
+ now says so directly instead of leaving it implicit.
76
+ - **`access explain` recognizes a `${{ self.scope.<ns> }}` / `${{ under.self.scope.<ns> }}` placeholder
77
+ for every namespace the platform accepts, not just letters and dots.** A namespace containing a digit,
78
+ `_`, or `-` (`${{ self.scope.eng_team }}`) previously fell through to being matched as an opaque
79
+ literal string instead of resolving to the caller, silently under-reporting that clause's grant.
80
+ - **Corrected the `sortBy` guidance in `AUTHORING.md`.** It carried a "trap"
81
+ warning that sorting an equality lookup by an **optional** payload field
82
+ "silently drops records that don't have it." That was never accurate — an
83
+ equality lookup resolves on the partition key, and `sortBy` only orders within
84
+ it — and records with no value for the sorted field now take their own ordered
85
+ position ahead of the rest, and are never inside a `sortFrom`/`sortTo` window.
86
+ That ordering applies to records written from this platform release onward;
87
+ earlier records take their new position once they are next updated.
88
+ The section now also names what `sortBy` really locks: the sorted field's
89
+ declared `fieldType` is migration-locked alongside `sortBy` itself, and an
90
+ `array`/`object` field is rejected as a `sortBy` target.
91
+
6
92
  ## 0.12.0
7
93
 
8
94
  ### Added