Skip to content

Commit f300a25

Browse files
committed
Merge origin/main into claude/issue-20751-services-strings-stage4
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
2 parents 1af6bed + 7e7e64b commit f300a25

92 files changed

Lines changed: 5502 additions & 781 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
Flow, hook, action, approval and expression rule findings no longer cite tracker numbers; each one states the decision behind it in words
6+
7+
Clause-②: no
8+
9+
Some findings these rules show to authors through `os validate`, `os lint` and `os build`, and the startup-registry findings a plugin author reads, pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
10+
11+
- Flow patterns: the record-change date-equality hint and the date-equality filter hint name the declarative alternative, a `schedule` flow whose start node carries a `config.timeRelative` descriptor; the unscoped `runAs` hint says `runAs` is enforced, so a run with no trigger user has its data operations refused rather than run unscoped; the unbounded bulk-write hint says `multi: true` is how a flow declares bulk intent and that the engine admits a whole-object write declared that way; the revise-target hint says the run-resume route continues a pause on a service-owned node type only through the service that owns it; the two interpolation hints say a flow node value is a string template in which only single-brace tokens resolve.
12+
- Startup-registry findings: the open-vocabulary notes say the engine judges node types only once the vocabulary is sealed at `kernel:bootstrapped`; the prescription describes the lazy cache resolution and the ADR-0104 attestation by what each does; the assertive-wording finding describes its two incidents, and how each was fixed, in words.
13+
- Expression findings: the field-level `visibleWhen` consequence names the `current_user` binding ADR-0089 D1 gives every runtime record surface; the retired `script` keys finding says spec 17 made `script` a call to a registered function and nothing else.
14+
- Trigger readiness: the array `triggerType` hint says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated.
15+
- Body writes, readonly writes and approvals: the discarded `ctx.record` write says the snapshot stays read-only by design and an action writes through `ctx.api`; the `readonlyWhen` write finding says a bulk update strips the field from every matched row once any one of them is locked; the `queue` approver finding says the type was deprecated rather than built; the empty-slate hint says the admin override may act on any pending request, so that one nobody in its slate can decide never stays stuck.
16+
- The other findings drop a citation the sentence already explained.
17+
18+
Text only: no rule id, severity, condition or finding moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/lint': patch
4+
---
5+
6+
`page.requires` says what the runtime now does with it: refused at save, reported at load (ADR-0080 §5).
7+
8+
Clause-②: no
9+
10+
The key's description used to say the list is "validated at save and load" while the liveness ledger recorded it as not enforced yet. Both are now true and say so. On a server that has the deployment's SDUI component manifest, saving a `kind: 'html'` page compiles its source, refuses a written `requires` that disagrees with it (`422 INVALID_METADATA`, `page-requires-disagrees-with-source`; a draft at its publish) and stores the derived list. At load, a stored page whose list names a plugin no manifest component carries is reported and still served. A server with no manifest checks neither and says so once at boot. Omit `requires`: it is derived from the source. The liveness row moves from `planned` to `live`, and the generated page reference carries the new description.
11+
12+
`validateJsxPages`' reason for staying off the runtime publish gate no longer says it parses through `typescript`/`sucrase`. It parses with the dependency-free `@objectstack/sdui-parser`, and it stays CLI-only because the save door already runs that compiler on every html page. The `ui-html-page-div-refused` upgrade-guide entry now names that save door too: on a server with a manifest, a `div` page saved from Studio or through the metadata API is refused under the same rule ids.
13+
14+
No schema accepts or refuses anything it did not before, and no runtime behaviour changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/service-analytics': minor
3+
---
4+
5+
fix(service-analytics)!: both analytics strategies refuse a cube measure whose `type` names no aggregate, in the spec's words — the custom-SQL `EXPRESSION_METRIC_TYPES` partition is gone with the three types it named (#21000)
6+
7+
**BREAKING** — `@objectstack/spec` retired the cube metric types `number`, `string`
8+
and `boolean` from `AggregationMetricType` (a measure's `sql` is a column reference,
9+
so they had nothing left to compute). Every door that parses a cube refuses them;
10+
this release removes the runtime branches that still served them for a cube that
11+
reached the analytics service WITHOUT meeting that parse — one a host registers
12+
in-process from a literal, through `AnalyticsServicePlugin({ cubes })` or
13+
`AnalyticsService({ cubes })` (the registry never parses).
14+
15+
| | before | now |
16+
| --- | --- | --- |
17+
| `NativeSQLStrategy`, a measure typed `number` / `string` / `boolean` | served: the column emitted UNAGGREGATED in the statement (`amount AS "m"` beside `GROUP BY`) | refused, nothing executed |
18+
| `ObjectQLStrategy`, the same measure | refused `INVALID_FIELD` / 400 | refused, nothing executed |
19+
| either strategy, a type the spec never declared (`median`) | native: refused; ObjectQL: forwarded to `executeAggregate` as the method (the auto-bridge refused it; a host's own executor received it), and `/analytics/sql` echoed `MEDIAN(amount)` | refused, nothing executed |
20+
21+
**The one refusal** is `aggregateOfMeasure`'s, shared by both strategies and both
22+
doors (`POST /analytics/query` and `POST /analytics/sql`): it names the measure and
23+
the cube, then quotes the spec's own verdict on the type — for a retired type the
24+
retirement prescription (the six aggregates to choose from, and where a per-row or
25+
derived value goes instead), for anything else zod's message listing the six. It is
26+
a bare `Error`, the undeclared-500 tier this package assigns to a cube that never
27+
met the parse, so the HTTP answer is `500` with the message readable in the body
28+
(measured through the dispatcher's analytics route), never a caller-blaming `400`.
29+
The ObjectQL envelope for the three retired types therefore moves from
30+
`INVALID_FIELD` / 400 to that tier.
31+
32+
**The fix:** give the measure one of the six aggregate types — `count`, `sum`,
33+
`avg`, `min`, `max`, `count_distinct` — or parse the cube through `CubeSchema`
34+
before registering it, which refuses the same types with the same prescription.
35+
36+
**Removed export:** `EXPRESSION_METRIC_TYPES` from
37+
`strategies/native-sql-strategy.ts` (internal to the package; not re-exported from
38+
its entry point). **Unchanged:** every aggregate measure on both strategies, the
39+
auto-bridge's own parse of an engine method (still pinned, driven directly), and
40+
`GET /analytics/meta`, which keeps publishing each registered measure's `type` as
41+
registered.
42+
43+
Clause-②: no (narrowing)
44+
45+
<!-- adr-0087: registered cube-metric-expression-types-retired -->
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire the cube metric types `number`, `string` and `boolean` — a measure's `sql` is a column reference, so the custom-SQL-expression types had nothing left to compute (#21000)
6+
7+
**BREAKING** — three members leave `AggregationMetricType`, so a cube measure's
8+
`measures.<metric>.type` no longer accepts `number`, `string` or `boolean`. ADR-0049
9+
enforce-or-remove. They declared "a custom SQL expression returning a number /
10+
string / boolean": the measure's `sql` was the whole computation. A cube member's
11+
`sql` is a column reference since `cube-member-sql-expression-retired` (#20943), so
12+
the three were left naming nothing: measured before this change, the raw-SQL
13+
analytics path returned the referenced column UNAGGREGATED (a bare column in a
14+
grouped statement — by SQL's own rules an error on PostgreSQL and an arbitrary row's
15+
value on SQLite), and the ObjectQL path refused the measure. The six aggregates — `count`, `sum`,
16+
`avg`, `min`, `max`, `count_distinct` — are unchanged and are now the whole
17+
vocabulary.
18+
19+
### FROM → TO
20+
21+
| removed | what to write instead |
22+
| --- | --- |
23+
| `measures.<metric>.type: 'number'`, `'string'` or `'boolean'` | the aggregate the measure means: `sum`, `avg`, `min` or `max` over the column; `count` (over `'*'` for a row count, or over a column for its non-null values); or `count_distinct`. |
24+
| a measure whose old expression computed a value per row | keep that value as a field of the object (a stored or formula field) and aggregate the field. |
25+
| a measure whose old expression combined measures (a ratio, a difference) | `derived: { op, of: [...] }` on an ADR-0021 dataset over the same object. |
26+
27+
**The one-line fix: give the measure an aggregate type.** There is no mechanical
28+
rewrite — the column alone does not say whether `amount` meant its sum, its average
29+
or its largest value — so `os migrate meta` lists nothing for this change.
30+
31+
Each retired member is refused at parse with a prescription naming the six
32+
aggregates, at the measure's `type`, and in `tsc` (the members are gone from the
33+
`AggregationMetricType` type). A value the enum never declared keeps zod's own
34+
message.
35+
36+
### The retirement kit
37+
38+
- **Value-level retirement.** `AggregationMetricType` is declared through
39+
`enumWithRetiredValues` (`shared/retired-key.ts`), with the prescriptions
40+
module-private. No authorable KEY and no def changed, so nothing lands in
41+
`RETIRED_KEYS_BY_MAJOR`, and the four surface ratchets (`api-surface`,
42+
`authorable-surface`, `json-schema.manifest`, `api-surface-signatures`) are
43+
byte-identical.
44+
- **No D2 conversion, by design.** A stored or built cube that still carries one of
45+
the three is REFUSED, never rewritten or dropped: the boot door
46+
(`ObjectStackDefinitionSchema`, which a built artifact is parsed through), the
47+
`analytics_cube` write door and `defineStack` refuse it with the prescription, and
48+
the rehydration seam replays no conversion over it.
49+
- **D3 entry `cube-metric-expression-types-retired`**, with its step-18 rationale
50+
fragment, carries the judgement the upgrader owes: which aggregate each measure
51+
meant.
52+
- **Liveness.** The `analytics_cube` row `measures.type` stays `live`, re-verified
53+
2026-10-02, with the narrowing recorded.
54+
- **Docs.** The `data/analytics` reference page is regenerated.
55+
- **No deprecation window**, per the project's startup-stage posture.
56+
57+
### Reach, measured
58+
59+
- This repository authors no cube measure of the three types outside tests:
60+
`examples/**`, `packages/**` (the platform objects included) and the skills and
61+
docs carry none. The showcase cube's `type: 'string'` entries are dimensions,
62+
whose `DimensionType` is a separate enum and is unchanged.
63+
- objectui at its pinned commit carries no `AggregationMetricType` mirror and no
64+
cube measure of the three types.
65+
- Out-of-repo authored cubes: NOT MEASURED.
66+
67+
Clause-②: no (narrowing)
68+
69+
<!-- adr-0087: registered cube-metric-expression-types-retired -->
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/objectql': minor
4+
'@objectstack/mcp': minor
5+
'@objectstack/plugin-audit': minor
6+
'@objectstack/service-analytics': minor
7+
'@objectstack/cli': minor
8+
---
9+
10+
fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207)
11+
12+
Clause-②: yes (narrowing)
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) the stored content hash of a metadata body stays the canonical hash at rest and no metadata body, authorable key, spelling or export moves; what changes is the form a door serves the hash in (a keyed digest: the crypto provider's, or a process-scoped ephemeral key's when none is registered), the form an inbound version token is compared in, and which query shapes the doors accept over the two hash columns, so `objectstack migrate meta` has nothing to rewrite. The operator-run rewrite this release asks for is of audit, activity and decision-audit copies, not of metadata. The other categories are closed on facts: every package here publishes (not `unpublished`); no ADR-0087 id covers a served version token or a refused query shape (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
15+
16+
**BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings.
17+
18+
**Three things change for callers and operators.**
19+
20+
1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin".
21+
2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged.
22+
3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage.
23+
24+
**What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-sharing': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat(plugin-sharing): the record owner and an explicit Modify-All holder may mint a share link on a record the data door refuses them (ADR-0111 D8 rule 1, ruling A′) (#21329)
7+
8+
Clause-②: yes (widening)
9+
10+
- **Who may mint.** `ShareLinkService.createLink` admits the caller when they can see the record, **or** own it, **or** hold `modifyAllRecords` on the object. The object's `publicSharing` opt-in is still checked first, and `publicSharing.eligibility` still last. On an object declared `access: { default: 'private' }` no wildcard grant covers the record, so its owner's own read is refused; the owner can now share it anyway. A member who neither sees nor owns the record is refused exactly as before, with the same envelope.
11+
- **Who still needs visibility.** A hierarchy manager whose write depth covers the record's owner manages the record's shares (revoke, grant, list), but is not admitted to mint without seeing the record: a link creates access.
12+
- **The organization wall.** Under the `group` and `isolated` tenancy postures the owner and Modify-All alternatives are withheld and visibility alone admits, as before this release. A member who left an organization still owns the records they created there, and must not be able to publish them by link.
13+
- **A required capability.** Neither alternative applies past a capability the object requires (`requiredPermissions`). An owner or Modify-All holder who lacks it is refused with the capability gate's own refusal, as before this release; an owner who holds it, refused only because no permission set grants the object, mints. The verdict is read from the `required_permissions` layer of `ISecurityService.explain`, so a security service the sharing service reaches must implement `explain`. If it does not, the two alternatives are withheld.
14+
- **API.** `SharingService.canMintWithoutVisibility(object, recordId, context)` answers the two alternatives with the owner and Modify-All branches `canManageShares` reads. `ShareLinkServiceOptions.canMintWithoutVisibility` is the late-bound probe `createLink` asks once the visibility read refuses, and `SharingServicePlugin` wires it. A host that constructs `ShareLinkService` itself without it keeps the visibility rule alone. The probe slice `SharingServiceOptions.securityService` returns gains an optional `explain`, the part of `ISecurityService.explain` the capability verdict reads.
15+
- **`@objectstack/spec` (documentation only).** The `IShareLinkService.createLink` TSDoc states who may mint, replacing "you may only link-share a record you can yourself see". The `ISharingService.canManageShares` TSDoc describes the hierarchy-manager branch, which is implemented, and says it is not mint authority. No schema, key, type or export changes.

0 commit comments

Comments
 (0)