Skip to content

Commit 56da9b6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20802-relation-filter-lowering
2 parents a70adfc + 660a9b2 commit 56da9b6

20 files changed

Lines changed: 115 additions & 107 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
objectql refusals, log lines and metadata text no longer cite tracker numbers; each states the reason in words
6+
7+
Clause-②: no
8+
9+
Many messages the query engine shows to authors, administrators and operators ended with an
10+
issue-tracker number where the reason belonged. The number goes, and where the sentence did not
11+
already say what was decided, it now does:
12+
13+
- Refusals: the bulk update and bulk delete row-scoping refusals now name the seed they are missing
14+
(the AST seeded before the middleware chain, which RLS and sharing compose their row-scoping onto,
15+
so a bulk write reaches only the rows the caller may edit); the hook-target rebind refusal says
16+
why `delete()` stopped honouring a rebind (a handler that silently redirects which row gets
17+
deleted is a trap) and names the `dispatchUnscopedMultiWrite` registration the whole-operation
18+
dispatch goes to, on update and delete alike. The unknown-option, filter-array,
19+
credential-aggregation, HAVING-operator, empty-hook-target, strict read-only and system-write
20+
organization refusals lose only the citation, because their sentences already said it.
21+
- Metadata text: the lifecycle `retention_overrides` setting description and the search companion
22+
field description lose their citation.
23+
- Log lines: the non-atomic cascade warning says a single-datasource cascade is now one
24+
transaction; the system-ledger transaction line calls the ledger the one class carved out of the
25+
cross-datasource write refusal; the dangling-reference audit summary says findings are reported,
26+
never rewritten, because a system-context write is exempt from the write-time reference check;
27+
the legacy `apiMethods` warning says the authorable values are the six primitives only, every
28+
other operation being derived from them or retired; the two unevaluable-rule warnings say such a
29+
rule fails closed and is never skipped. The ADR-0104 value-shape gate lines, the delegated
30+
protocol-assembly line and the read-only and runtime-owned strip warnings lose only the citation.
31+
32+
The `findOne` no-predicate refusal keeps its citation for now: `@objectstack/metadata-core`
33+
carries a byte-identical copy that this package's tests compare against, and both move together.
34+
35+
Text only: no error code, field name, status or behaviour changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `BlueprintNavItemSchema.label` says an absent label is inherited at render time, not defaulted by the expander
6+
7+
Clause-②: no
8+
9+
The `label` describe on a blueprint nav item read "defaults to the target label/name". An
10+
expander or an AI author that follows "defaults" copies the target's label into the entry, and
11+
the entry then stops following a rename of that target. The runtime nav entry's `label` has
12+
meant something else since it became optional: absent, the entry inherits the CURRENT label of
13+
what it opens at render time; present, it renders verbatim. The blueprint describe now says
14+
exactly that, and tells the author not to copy the target's label in as a default.
15+
16+
Describe text only: the key stays `z.string().optional()`, so the schema accepts and refuses
17+
the same blueprints. The reference page `content/docs/references/ai/solution-blueprint.mdx`
18+
is regenerated from it.

‎content/docs/references/ai/solution-blueprint.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const result = BlueprintAppSchema.parse(data);
5353
| :--- | :--- | :--- | :--- |
5454
| **type** | `Enum<'object' \| 'dashboard'>` | optional (default: `"object"`) | What this nav entry opens |
5555
| **target** | `string` | ✅ | Object or dashboard machine name to surface (snake_case) |
56-
| **label** | `string` | optional | Nav entry label (defaults to the target label/name) |
56+
| **label** | `string` | optional | Nav entry label. Optional: absent ⇒ the entry inherits the CURRENT label of what it opens at render time (a renamed target shows its new name); present ⇒ rendered verbatim, so never copy the target's label in as a default. |
5757
| **icon** | `string` | optional | Lucide icon name for the nav entry |
5858
| **viewName** | `string` | optional | For type:"object" only — the `views[].name` this entry opens (e.g. "ticket_status_board"). Omit for the object's default list. SET it whenever this blueprint authors a kanban/calendar/gallery/gantt view the menu should reach: give the object ONE entry per view (a 「工单列表」 entry with no viewName plus a 「工单看板」 entry with viewName:"ticket_status_board"). Without it every entry on the same target opens the SAME default list, and a label/icon saying otherwise is decoration. |
5959

@@ -187,7 +187,7 @@ const result = BlueprintAppSchema.parse(data);
187187
| :--- | :--- | :--- | :--- |
188188
| **type** | `Enum<'object' \| 'dashboard'>` | optional (default: `"object"`) | What this nav entry opens |
189189
| **target** | `string` | ✅ | Object or dashboard machine name to surface (snake_case) |
190-
| **label** | `string` | optional | Nav entry label (defaults to the target label/name) |
190+
| **label** | `string` | optional | Nav entry label. Optional: absent ⇒ the entry inherits the CURRENT label of what it opens at render time (a renamed target shows its new name); present ⇒ rendered verbatim, so never copy the target's label in as a default. |
191191
| **icon** | `string` | optional | Lucide icon name for the nav entry |
192192
| **viewName** | `string` | optional | For type:"object" only — the `views[].name` this entry opens (e.g. "ticket_status_board"). Omit for the object's default list. SET it whenever this blueprint authors a kanban/calendar/gallery/gantt view the menu should reach: give the object ONE entry per view (a 「工单列表」 entry with no viewName plus a 「工单看板」 entry with viewName:"ticket_status_board"). Without it every entry on the same target opens the SAME default list, and a label/icon saying otherwise is decoration. |
193193

‎packages/objectql/src/engine-dropped-fields-primary-key.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ describe('#6437 — the refusal message is composed from `drops`, not from the c
315315
`API-boundary caller, isSystem included. A value DERIVED by a beforeUpdate hook is ` +
316316
`not a caller write and is never stripped — that is the sanctioned write path for a ` +
317317
`conditionally-locked derived field). To let the strip happen and merely observe it, drop ` +
318-
`strictReadonlyWrites and pass options.onFieldsDropped instead (#3407).`,
318+
`strictReadonlyWrites and pass options.onFieldsDropped instead.`,
319319
);
320320
});
321321

‎packages/objectql/src/engine.ts‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ function rejectUnknownEngineOptions(
781781
throw new Error(
782782
`${operation}('${object}') does not recognise option${unknown.length > 1 ? 's' : ''} ` +
783783
`${details.join('; ')}. The engine executes none of ${unknown.length > 1 ? 'them' : 'it'}, ` +
784-
`so the call would succeed with the option silently ignored (#4371). ` +
784+
`so the call would succeed with the option silently ignored. ` +
785785
`Legal keys for ${operation}: ${[...legal].sort().join(', ')}.`,
786786
);
787787
}
@@ -1089,7 +1089,7 @@ function lowerWhereFilterArray<T extends object | undefined>(
10891089
`${JSON.stringify(where)}. A filter array is a comparison [field, operator, value], ` +
10901090
`a logical node ["and"|"or", ...conditions], or a list of those — it is INPUT-ONLY ` +
10911091
`sugar (spec 'FilterArray'), lowered to a FilterCondition here before any driver sees ` +
1092-
`it (#5158). This value cannot be lowered, and an unapplied filter would have returned ` +
1092+
`it. This value cannot be lowered, and an unapplied filter would have returned ` +
10931093
`the UNFILTERED result set. Recognised operators: ` +
10941094
`${[...VALID_AST_OPERATORS].sort().join(', ')}. Infix joins ([condA, "or", condB]) are ` +
10951095
`NOT one of the shapes — write the prefix form ["or", condA, condB].`,
@@ -1112,7 +1112,7 @@ function lowerWhereFilterArray<T extends object | undefined>(
11121112
throw new Error(
11131113
`${operation}('${object}'): filter array ${JSON.stringify(where)} passed isFilterAST() ` +
11141114
`but parseFilterAST() lowered it to nothing. Refusing rather than running the query ` +
1115-
`unfiltered (#5158).`,
1115+
`unfiltered.`,
11161116
);
11171117
}
11181118
// [#5869] Door 2's half of the same check USED to be a second
@@ -9688,7 +9688,7 @@ export class ObjectQL implements IObjectQLEngine {
96889688
FILE_REFERENCES_MIGRATION_ID,
96899689
'[value-shape] this deployment has verified the file-as-reference migration — ' +
96909690
'media value shapes are enforced and released field files may be collected ' +
9691-
'(ADR-0104 / #3617)',
9691+
'(ADR-0104)',
96929692
);
96939693
}
96949694

@@ -9736,7 +9736,7 @@ export class ObjectQL implements IObjectQLEngine {
97369736
'valueShapesMigrationVerified',
97379737
VALUE_SHAPES_MIGRATION_ID,
97389738
'[value-shape] this deployment has verified the value-shape scan — reference and ' +
9739-
'structured-JSON value shapes are enforced (ADR-0104 / #3438)',
9739+
'structured-JSON value shapes are enforced (ADR-0104)',
97409740
);
97419741
}
97429742

@@ -10039,7 +10039,7 @@ export class ObjectQL implements IObjectQLEngine {
1003910039
'no byte is deleted on evidence this deployment has contradicted. Fix the data and run ' +
1004010040
'`os migrate ' +
1004110041
(migrationId === FILE_REFERENCES_MIGRATION_ID ? 'files-to-references' : 'value-shapes') +
10042-
' --apply` to clear it (ADR-0104 / #4797).',
10042+
' --apply` to clear it (ADR-0104).',
1004310043
);
1004410044
})
1004510045
.catch((err: any) => {
@@ -10050,7 +10050,7 @@ export class ObjectQL implements IObjectQLEngine {
1005010050
`[value-shape] could not record the observed deviation for '${migrationId}' ` +
1005110051
`(${err?.message ?? err}) — the ledger still authorises irreversible collection while ` +
1005210052
'this deployment holds a value its own contract rejects; run the migration to ' +
10053-
're-derive the gate (#4797)',
10053+
're-derive the gate',
1005410054
);
1005510055
});
1005610056
}
@@ -10141,7 +10141,7 @@ export class ObjectQL implements IObjectQLEngine {
1014110141
`(${tally?.first.object}.${tally?.first.field}: ${tally?.first.detail}). ` +
1014210142
'The gate is closed again — fix the data, then run `os migrate ' +
1014310143
(migrationId === FILE_REFERENCES_MIGRATION_ID ? 'files-to-references' : 'value-shapes') +
10144-
' --apply` to re-earn it (ADR-0104 / #4769).',
10144+
' --apply` to re-earn it (ADR-0104).',
1014510145
);
1014610146
})
1014710147
.catch((err: any) => {
@@ -10150,7 +10150,7 @@ export class ObjectQL implements IObjectQLEngine {
1015010150
this.logger.warn(
1015110151
`[value-shape] could not revoke the creation attestation for '${migrationId}' ` +
1015210152
`(${err?.message ?? err}) — the ledger still claims this deployment is verified ` +
10153-
'while its data contradicts that; run the migration to re-derive it (#4769)',
10153+
'while its data contradicts that; run the migration to re-derive it',
1015410154
);
1015510155
});
1015610156
}
@@ -10226,15 +10226,15 @@ export class ObjectQL implements IObjectQLEngine {
1022610226
'[value-shape] media values are checked but NOT enforced here, and released files are ' +
1022710227
'never collected — this deployment has not verified its file migration. Run ' +
1022810228
'`os migrate files-to-references` (dry run) to see what it would do, then `--apply` ' +
10229-
'to close the gate (ADR-0104 / #3617).',
10229+
'to close the gate (ADR-0104).',
1023010230
);
1023110231
}
1023210232
if (covered && !(await this.readMigrationFlagVerified(VALUE_SHAPES_MIGRATION_ID)).verified) {
1023310233
this.logger.info(
1023410234
'[value-shape] reference and structured-JSON values are checked but NOT enforced here — ' +
1023510235
'this deployment has not verified its value-shape scan. Run `os migrate value-shapes` ' +
1023610236
'(dry run) to see what it would report, then `--apply` to close the gate ' +
10237-
'(ADR-0104 / #3438).',
10237+
'(ADR-0104).',
1023810238
);
1023910239
}
1024010240
} catch {
@@ -14191,7 +14191,9 @@ export class ObjectQL implements IObjectQLEngine {
1419114191
if (!ast) {
1419214192
throw new Error(
1419314193
`[Security] Refusing bulk update on '${object}': row-scoping AST was not seeded ` +
14194-
`(the predicate branch was reached without the #2982 seed).`,
14194+
`(the predicate branch was reached without the AST seeded before the middleware ` +
14195+
`chain — the one RLS and sharing compose their row-scoping onto, so that a bulk ` +
14196+
`write reaches only the rows this caller may edit).`,
1419514197
);
1419614198
}
1419714199
// [#9974] The unscoped-multi shape check, BEFORE the matched-row
@@ -15373,7 +15375,8 @@ export class ObjectQL implements IObjectQLEngine {
1537315375
`Cascade delete of '${object}' cannot run as one unit of work: the cascade reaches an object routed ` +
1537415376
`to a datasource other than the default one ('${this.defaultDriver ?? '<none>'}'), and a transaction ` +
1537515377
"covers one driver's connection only (ADR-0119 D1 — no two-phase commit). The cascade therefore runs " +
15376-
'UNWRAPPED, exactly as it did before #7413: if a later dependent refuses the delete, the rows already ' +
15378+
'UNWRAPPED, as every cascade did before a single-datasource cascade was made one transaction: if a ' +
15379+
'later dependent refuses the delete, the rows already ' +
1537715380
'removed stay removed while the call rejects. Route the cascading objects to one datasource to get the ' +
1537815381
'atomic path. Reported once per object per engine instance.',
1537915382
{ object, defaultDatasource: this.defaultDriver ?? undefined },
@@ -16511,7 +16514,9 @@ export class ObjectQL implements IObjectQLEngine {
1651116514
if (!ast) {
1651216515
throw new Error(
1651316516
`[Security] Refusing bulk delete on '${object}': row-scoping AST was not seeded ` +
16514-
`(the predicate branch was reached without the #2982 seed).`,
16517+
`(the predicate branch was reached without the AST seeded before the middleware ` +
16518+
`chain — the one RLS and sharing compose their row-scoping onto, so that a bulk ` +
16519+
`write reaches only the rows this caller may edit).`,
1651516520
);
1651616521
}
1651716522
// [#9719] The unscoped-multi shape check, BEFORE the matched-row read:
@@ -16835,7 +16840,7 @@ export class ObjectQL implements IObjectQLEngine {
1683516840
+ 'secret/password fields are masked on read and `internal: true` fields are omitted '
1683616841
+ 'outright, so the value never leaves the engine on the generic data path; aggregating '
1683716842
+ 'them (group-by, min/max, array_agg, …) would surface it. '
16838-
+ 'Refusing (fail-closed) — see ADR-0100 / #3171 / #7922.',
16843+
+ 'Refusing (fail-closed) — see ADR-0100.',
1683916844
);
1684016845
}
1684116846
}
@@ -17599,7 +17604,8 @@ export class ObjectQL implements IObjectQLEngine {
1759917604
this.logger.debug(
1760017605
`${operation} of '${objectName}' inside transaction() is routed to datasource '${target}' while the ` +
1760117606
`transaction is open on '${scope.datasource}' — executing it OUTSIDE the transaction, on its own ` +
17602-
'connection (ADR-0057 §3.6 system ledger, carved out by #5351). It commits independently and will ' +
17607+
'connection (ADR-0057 §3.6 system ledger — the one class carved out of the cross-datasource write ' +
17608+
'refusal). It commits independently and will ' +
1760317609
'SURVIVE a rollback of this transaction: an audit/telemetry/event row may describe a write that was ' +
1760417610
'undone. That is the decided direction of error for an append-only ledger — an extra reconcilable ' +
1760517611
'row beats a missing row for a write that did commit. Said once per transaction per datasource.',

‎packages/objectql/src/having-filter.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ function unknownOperator(
325325
return invalidFilterError(
326326
`Unsupported operator '${op}' in \`${clause.root}\`. ${clause.semantics} and supports: ${supported}. `
327327
+ `An unknown operator is refused rather than ignored — ignoring it would silently `
328-
+ `return unfiltered aggregates (#4286, ADR-0078).`,
328+
+ `return unfiltered aggregates (ADR-0078).`,
329329
);
330330
}
331331

‎packages/objectql/src/hook-binder.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export function bindHooksToEngine(
205205
result.skipped += 1;
206206
const reason =
207207
'hook target names no object — an empty `object` is refused rather than widened to '
208-
+ "the wildcard '*' (#4001). Name the object(s), or write `object: '*'` if firing on "
208+
+ "the wildcard '*'. Name the object(s), or write `object: '*'` if firing on "
209209
+ 'every object is the intent.';
210210
result.errors.push({ hook: hook.name, reason });
211211
if (opts.strict) {

‎packages/objectql/src/hook-target-rebind-errors.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,20 @@ function buildMessage(info: {
167167
? cleared
168168
? ` The capability this used to have is RETIRED: clearing 'input.id' in a '${event}' handler ` +
169169
`converted a by-id write into a PREDICATE write over the caller's 'where'. Since ADR-0058 ` +
170-
`Addendum II (#5574 / #5846) the dispatch ladder is resolved BEFORE the before phase — the ` +
170+
`Addendum II the dispatch ladder is resolved BEFORE the before phase — the ` +
171171
`predicate path has to read its matched rows first, to build one context per row — so there ` +
172172
`is no ladder left to re-enter.`
173173
: ` The capability this used to have is RETIRED: rebinding 'input.id' in a '${event}' handler ` +
174174
`moved the write to another row. The engine now resolves the target BEFORE the before phase ` +
175175
`and computes the whole write against it — the pre-image, the 'readonlyWhen' locks, the ` +
176176
`validation rules — so a by-id target is immutable once a handler runs, on BOTH verbs. ` +
177-
`'delete()' honoured a rebind until #6752 by re-resolving the new target; that is retired ` +
178-
`too, so one rule now covers both.`
177+
`'delete()' used to honour a rebind by re-resolving the new target; that is retired ` +
178+
`too, because a handler that silently redirects which row gets deleted is a trap — so ` +
179+
`one rule now covers both.`
179180
: path === 'unscoped-multi'
180181
? ` This is the whole-operation dispatch an UNSCOPED predicate write delivers to a declared ` +
181-
`shape guard (#9719, both write verbs since #9974): its 'id' is present-but-undefined ON ` +
182+
`shape guard (one registered with 'dispatchUnscopedMultiWrite', on update and delete ` +
183+
`alike): its 'id' is present-but-undefined ON ` +
182184
`PURPOSE — there is no target row — and the dispatch ladder was resolved before any handler ` +
183185
`ran, so binding 'input.id' here retargets nothing. It is refused rather than ignored, ` +
184186
`because a silent no-op is the failure this contract exists to abolish.`

0 commit comments

Comments
 (0)