Skip to content

Commit 0583520

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21467-fold-proposal-and-uncarded-follow-up
2 parents acb54ea + 417443e commit 0583520

22 files changed

Lines changed: 1824 additions & 56 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
fix(objectql,spec)!: a hook's `handler` name resolves inside the hook's own package only (#21604)
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) no authorable key, spelling, export of a published release or stored shape moves: `HookSchema`'s shape is unchanged (only `HookSchema.handler`'s doc changes), so `objectstack migrate meta` has nothing to rewrite. What changes is which function a string `handler` may bind to at registration. Census of compositions relying on cross-package resolution by name, at the claim: zero in objectstack `examples/**` (15fe567c9c, whose only string `handler` is a job's), hotcrm (f24c196588) and objectos (7612ffebd1); this repository commits no `--artifact` runtime module; cloud is NOT MEASURED (unreachable from the claim's session). The other categories are closed on facts: both packages publish (not `unpublished`); no ADR-0087 id covers a binding rule (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
11+
12+
**BREAKING**: a hook whose `handler` is a function NAME (the deprecated form, `handler: 'my_fn'`, with no `body`) now binds only to a function its own package holds. It used to fall back to the engine-wide function registry, which is keyed by bare name, so the hook could bind to a function another package registered under the same name and run that package's code on its own events.
13+
14+
- **Accepted before:** a string `handler` resolved against the functions handed to the hook's bind, then against every function any package had registered on the engine. A name found nowhere was skipped with a `warn`.
15+
- **Accepted now:** a string `handler` resolves against the functions handed to the hook's bind (the package's `functions`, which an `--artifact` runtime module supplies), then against the functions the same package (`packageId`) registered on the engine. Nothing else.
16+
- **Refused now, at registration:** a name the hook's own package does not hold, whether another package registered it or nobody did. The hook is not bound. The refusal carries `INVALID_REFERENCE` with status `400` (ADR-0112), names the hook, the function and the package, and is recorded on the bind result (`BindHooksResult.errors[]` gains `code` and `status`) and logged at `error`. Under `strict` (`OBJECTQL_STRICT_HOOKS=1`) it is thrown.
17+
- **The doors:** a hook authored at runtime through the metadata API (`PUT /api/v1/meta/hook/:name`) ships with no code package and holds no functions, so a `handler`-only hook authored there is refused when the door binds it; the save itself still answers as before. In a composition of several apps, one app's hook can no longer bind to another app's function. A bind that names no owning package (direct `bindHooksToEngine` use without `packageId`) resolves only the functions handed to it.
18+
- **Unchanged:** a hook with a `body` binds as before. An app's hook naming its own `defineStack({ functions })` entry, or a function its own `--artifact` runtime module exports, binds as before. The install-local door's refusal of a hook with no `body` is unchanged.
19+
20+
What to do with a refused hook: give it a `body` (sandboxed JS), or declare the function in the hook's own package's `functions`. To reuse another package's function, import it from the package that owns it and declare it there. This ships as `minor`, under the launch-window convention for narrowings of an accept set.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cli': patch
3+
'@objectstack/service-storage': patch
4+
---
5+
6+
`os migrate value-shapes` and `os migrate files-to-references` record the deployment-level ADR-0104 flag only from a run over every object, and every command in the `os migrate` data-migration family refuses an `--object` name the deployment does not declare (#21644).
7+
8+
Clause-②: no
9+
10+
- **A narrowed `--apply` records no deployment flag.** The flag attests the stored data of every object and turns strict enforcement on, but a run narrowed by `--object` reads only the named objects. Such a run still applies its fixes: `files-to-references` converts the named objects' values. It records no flag, whether it passes or fails, and leaves a flag that an earlier full-scope run recorded exactly as it was. Its output says why and names the run that records the flag: the same command without `--object`. The `--json` document carries `filter: { objects }`, which is `null` on a full-scope run, so a narrowed run is never mistaken for a full one. Any `--object` narrows, even a list that names every object. A full-scope `--apply` records the flag as before.
11+
- **`runFilesToReferencesMigration`** (`@objectstack/service-storage`) skips the flag write when it is given `objects`. That includes `[]`, which walks nothing. Its `flag` result is `null` on a narrowed run.
12+
- **The column step of `files-to-references` does not run on a narrowed run.** It retypes every single-value media column in the database on the authority of the gate, and a narrowed gate vouches only for the named objects. Before this change, a narrowed `--apply` or a misspelled one moved those columns and stamped `columns_moved_at`.
13+
- **An unknown `--object` is an error.** This applies to `value-shapes`, `files-to-references`, `summary-nulls` and `duplicates`. A name the booted registry does not declare exits 1 with `OBJECT_NOT_FOUND`, and the error names that name and the declared objects. The check runs before anything is read or written. Until now, such a name was filtered out of the scan without a word, so a typo scanned nothing and read as a clean run. `duplicates` reports the refusal as `{ error: 'report_failed', detail, code }`. A declared object that the command has nothing to check on is still accepted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
A seed row's authored `created_at` is kept when the row is first inserted, the same as when a later boot replays it (#21646).
6+
7+
Clause-②: no
8+
9+
- **Before.** The built-in audit stamp (`sys_stamp_audit_insert`) replaced a seed row's authored `created_at` with the boot instant on insert. A later boot's upsert update then wrote the authored value, so a fresh or reset database showed every seeded record as created at boot until the next restart. This held for a literal instant and for a `cel` value such as ``cel`daysAgo(5)` ``.
10+
- **After.** Under the seed write context (`ExecutionContext.seedReplay`, set by `SEED_WRITE_EXECUTION_CONTEXT`), the insert stamp keeps an authored `created_at` and stamps the boot instant only when the row has none. Both paths now store the authored value. The update stamp is unchanged, so `updated_at` still moves on a replay. All three seed writers pass that context: `SeedLoaderService`, `AppPlugin`'s replay of a stack's `data[]`, and `@objectstack/verify`'s `seed()`.
11+
- **Unchanged.** A REST create, a create from a bare `isSystem` context and every other caller still have `created_at` stamped now. `preserveAudit` is unchanged, and the seed context does not gain it. A non-system create that requests `preserveAudit` gets the same warning as before. `created_by` is not stamped on a seed write, because the seed context has no user. An authored value is kept on insert and on replay, as it was before this change.
12+
- ⛔ No schema, key, export or error code is added or removed.

‎content/docs/deployment/cli.mdx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,11 +1132,25 @@ report no secret and no file. A read the command cannot avoid and that fails for
11321132
other reason still refuses and exits 1. Point `--database-url` at the deployment's
11331133
database, or boot the deployment once first, to see what it holds.
11341134
1135+
**`--object` narrows a run, and only a run over every object records a flag.**
1136+
`files-to-references`, `value-shapes`, `summary-nulls` and `duplicates` take
1137+
`--object` to restrict the run to the objects you name. `duplicates` takes one name,
1138+
and the others are repeatable. A name your deployment does not declare is refused
1139+
with `OBJECT_NOT_FOUND` and exit 1 before anything is read or written. The error
1140+
names the unknown name and the declared objects, so a misspelling is never answered
1141+
as a clean run over nothing. A narrowed `--apply` applies its fixes to the named
1142+
objects. `files-to-references` and `value-shapes` then record **no** deployment flag,
1143+
because the flag is a claim about every object's stored data and a narrowed run read
1144+
only some. A narrowed `files-to-references` run does not move the media columns
1145+
either. The output says so, a flag that an earlier full run recorded is left as it
1146+
was, and `--json` carries `filter: { objects }`. Any `--object` narrows, even a list
1147+
that names every object, so run the command without `--object` to record the flag.
1148+
11351149
```bash
11361150
os migrate files-to-references # Dry run: full report, writes nothing
11371151
os migrate files-to-references --apply # Convert, verify, record the flag (prompts)
11381152
os migrate files-to-references --apply --yes --json # CI / scripts
1139-
os migrate files-to-references --object product # Restrict to one object (repeatable)
1153+
os migrate files-to-references --object product # Restrict to one object (repeatable); records no flag
11401154
```
11411155
11421156
A `file` / `image` / `avatar` / `video` / `audio` field value is an opaque
@@ -1189,7 +1203,7 @@ The same gate for the **non-media** value classes — references (`lookup`,
11891203
os migrate value-shapes # Scan: full report, writes nothing
11901204
os migrate value-shapes --apply # Scan, then record the flag if clean (prompts)
11911205
os migrate value-shapes --apply --yes --json # CI / scripts
1192-
os migrate value-shapes --object contact # Restrict to one object (repeatable)
1206+
os migrate value-shapes --object contact # Restrict to one object (repeatable); records no flag
11931207
```
11941208
11951209
**This one converts nothing.** Its sibling rewrites legacy file values because

‎packages/cli/src/commands/migrate/duplicates.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { Command, Flags } from '@oclif/core';
4-
import { emitJson, isExitSignal } from '../../utils/format.js';
4+
import { emitJson, errorCodeFields, isExitSignal } from '../../utils/format.js';
55
import { bootSchemaStack } from '../../utils/schema-migrate.js';
6+
import { refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
67
// The `objectql` slot's contract (#4251) — read the registry through it rather
78
// than erasing the lookup to `any`, so a rename breaks this at compile time
89
// instead of silently reporting zero objects.
@@ -874,7 +875,9 @@ export default class MigrateDuplicates extends Command {
874875
env: 'OS_DATABASE_URL',
875876
}),
876877
object: Flags.string({
877-
description: 'Restrict the scan to one object (recorded in the report, so a narrowed run cannot be mistaken for a full one)',
878+
description:
879+
'Restrict the scan to one object (recorded in the report, so a narrowed run cannot be mistaken for a full ' +
880+
'one). A name the deployment does not declare is refused',
878881
}),
879882
};
880883

@@ -906,6 +909,18 @@ export default class MigrateDuplicates extends Command {
906909
}
907910

908911
try {
912+
// [#21644] `collectScanTargets` keeps only the objects it would probe, so
913+
// a name this registry does not declare would be dropped without a word
914+
// and the report would read "no duplicates" over a scan of nothing.
915+
// Refused before any probe, against the set the scan draws from.
916+
refuseUndeclaredObjects(
917+
flags.object === undefined ? undefined : [flags.object],
918+
stack
919+
.allObjects()
920+
.map((o) => (o as { name?: unknown } | null)?.name)
921+
.filter((name): name is string => typeof name === 'string'),
922+
);
923+
909924
const {
910925
resolveSeedTenancyExec,
911926
normalizeRows,
@@ -973,7 +988,11 @@ export default class MigrateDuplicates extends Command {
973988
await emitJson(report);
974989
} catch (error: unknown) {
975990
if (isExitSignal(error)) throw error;
976-
await emitJson({ error: 'report_failed', detail: messageOf(error) }, 1, { compact: true });
991+
await emitJson(
992+
{ error: 'report_failed', detail: messageOf(error), ...errorCodeFields(error) },
993+
1,
994+
{ compact: true },
995+
);
977996
} finally {
978997
await stack.shutdown();
979998
}

0 commit comments

Comments
 (0)