Skip to content

Commit 77d6bbb

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-10840-invisible-bare-root-worklist
2 parents 9fd9370 + 409077e commit 77d6bbb

23 files changed

Lines changed: 2183 additions & 76 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
Publish refusals no longer render each validation finding twice (#10524) — declare-then-trim.
8+
9+
**Declared (spec, additive):** `PublishPackageDraftsResponseSchema.failed[]` elements now
10+
declare `issues[]` (the `RuntimeAuthoringIssueSchema` findings the producer has emitted
11+
since #8333 but no declared parse could carry), and `seedApplied` declares `issues[]`
12+
(`{ path, message, code? }`, the seed-body schema refusal's findings). Typed consumers —
13+
the SDK's `PublishPackageDraftsResponse`, any `parse` through the schema — can now read
14+
the structured findings back instead of having them silently stripped.
15+
16+
**Trimmed (producers):** the #4463 author-time gate's 422 message and
17+
`seedRequestValidationError`'s message are one-sentence headlines — total count plus up to
18+
three `path [rule]` / `path [zod-code]` locators — instead of restating the issue prose
19+
that `issues[]` carries on the same response. Consumers that render only `error` (CLI,
20+
logs) keep what failed, where, under which rule, and how many; consumers that render both
21+
channels stop repeating themselves. The old `(+N more)` tail is subsumed by the leading
22+
count. Both catches that surface the seed refusal onto `seedApplied` now thread the
23+
structured findings beside the headline.
24+
25+
Error `code`/`status` vocabularies, `advisories`, the DESTRUCTIVE_CHANGE (409) message,
26+
and `saveMetaItem`'s spec-validation 422 message are unchanged. Messages are not contract
27+
(the machine-readable channels are `code` and `issues[]`), so this is not a breaking
28+
change and registers no migration.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/runtime": patch
3+
"@objectstack/hono": patch
4+
"@objectstack/plugin-security": patch
5+
"@objectstack/service-package": patch
6+
---
7+
8+
docs: repair the dead repo-relative targets in four published READMEs (#10813)
9+
10+
A published README ships inside the npm tarball, so a dead relative link in one
11+
is shipped to every reader who installs the package. Nine of them were measured
12+
across four packages, and nothing read them: `check:published-readme-links`
13+
checked docs-site URLs, `check:published-readme-exports` checked fenced import
14+
lines, and the lychee lane never sees `packages/**/README.md`.
15+
16+
`@objectstack/runtime` carried six dead targets. Each was traced to where the
17+
content actually went rather than deleted:
18+
19+
- `MINI_KERNEL_GUIDE.md`, `MINI_KERNEL_ARCHITECTURE.md` and
20+
`MINI_KERNEL_IMPLEMENTATION.md` were deleted from the repo root in January as
21+
"redundant markdown files" (d709ecce68 — 14 files, 5051 deletions, nothing
22+
added). The kernel reference they described is the docs site now, so the
23+
Documentation section is the same footer eight sibling READMEs already use.
24+
- `examples/host/` was renamed to `examples/app-host`, then `apps/server`, then
25+
`apps/objectos`, and finally split out to `objectstack-ai/cloud`. In-repo, an
26+
HTTP server in front of the runtime is `@objectstack/plugin-hono-server` plus
27+
the `@objectstack/hono` adapter, so the bullet points there.
28+
- `examples/msw-react-crud/` became `examples/app-react-crud`, then
29+
`apps/console`, and now ships as `@object-ui/console` from another repo.
30+
- `test-mini-kernel.ts` was a root-level scratch script; this package's suite is
31+
179 test files under `src/`.
32+
- The section also ended on a truncated bullet with an unterminated backtick
33+
(`` - `packages/runtime/src/ ``), which is now a real pointer to that suite.
34+
35+
The other three packages: `@objectstack/hono` and `@objectstack/service-package`
36+
still spelled `@objectstack/driver-sql` as `../../plugins/driver-sql`, stale
37+
since the driver moved to `packages/drivers/` (#5618). `@objectstack/plugin-security`
38+
and `@objectstack/service-package` linked three packages that are in no directory
39+
of this repo (`plugin-org-scoping`, `service-tenant`, `service-marketplace`);
40+
those links are dropped and the names kept as code spans, which is the spelling
41+
those same files already use for a package they cannot point at in-tree. Whether
42+
those three packages exist at all is a separate question, filed separately.

.github/workflows/lint.yml

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,45 @@ jobs:
184184
# `ts-parse` spawns ~20 real node children (~10 s measured, and the spawns
185185
# are the point — they pin that a caller's try/catch cannot swallow the
186186
# refusal); the other two are in-process fixtures, ~0.5 s combined.
187+
#
188+
# ⭐ Collected rather than sequenced, for the reason spelled out at the
189+
# `Shallow-history guard self-tests` step below (#10814): under `bash -e` a
190+
# bare sequence stops at the first failure, so a red `ts-parse` would leave
191+
# the entry-predicate and comment-mask self-tests UNRUN while the log shows
192+
# only the one failure. `ts-parse` is both the slowest of the three and the
193+
# one that spawns real children, so it is the likeliest to be red —
194+
# precisely the masking direction. The three modules are independent of one
195+
# another, so collecting loses nothing.
187196
- name: scripts/ shared-module self-tests (parse · entry predicate · comment mask)
188197
run: |
189-
node scripts/ts-parse.mjs --self-test
190-
node scripts/invoked-as.mjs --self-test
191-
node scripts/js-comment-mask.mjs --self-test
198+
# Tolerate-and-collect (#10814) — see the note above this step. Each
199+
# self-test runs unconditionally and prints its own verdict; the step
200+
# still FAILS when any of them does, naming every one that failed.
201+
# ⛔ Never let the collector swallow the exit code — a green step over
202+
# a red self-test looks identical to success. Both directions are pinned
203+
# by `node scripts/check-step-collectors.mjs --self-test`, which extracts
204+
# THIS block from THIS file and drives it under `bash -e` with stubs.
205+
failed=""
206+
run_self_test() {
207+
echo "-- $*"
208+
if "$@"; then
209+
echo "PASS $*"
210+
else
211+
echo "FAIL $*"
212+
failed="${failed} $*"$'\n'
213+
fi
214+
return 0
215+
}
216+
run_self_test node scripts/ts-parse.mjs --self-test
217+
run_self_test node scripts/invoked-as.mjs --self-test
218+
run_self_test node scripts/js-comment-mask.mjs --self-test
219+
if [ -n "$failed" ]; then
220+
echo ""
221+
echo "scripts/ shared-module self-tests — the following FAILED:"
222+
printf "%s" "$failed"
223+
exit 1
224+
fi
225+
echo "scripts/ shared-module self-tests — all three ran and passed"
192226
193227
# Every `scripts/**` entry guard goes through ONE predicate (#10086).
194228
# The hand-typed forms of "did node run me, or did someone import me?"
@@ -549,11 +583,116 @@ jobs:
549583
# (`check-governed-merges.mjs`'s own cases run in the
550584
# `pnpm check:pm-governed-merges` step above, which is already its
551585
# self-test.)
586+
#
587+
# ⭐ The three run through a COLLECTOR rather than as a bare sequence
588+
# (#10814). A `run:` block is executed by `bash -e`, so the first non-zero
589+
# exit aborts the STEP and every command after it is never reached —
590+
# neither green nor red, and nothing in the log tells those apart. Not
591+
# hypothetical here: while `git-history.mjs --self-test` was red on `main`
592+
# for ~10 h on 2026-08-21 (#10807), the two self-tests listed after it did
593+
# not execute in CI once — on the step that gates every PR. Both were
594+
# green, so that time the mask hid nothing; the compounding shape is that a
595+
# SECOND regression can land unnoticed while the first is red, and then
596+
# reads as though the fix broke it. #4690 one level up: a partial result
597+
# that reads like a complete one.
598+
#
599+
# These three are INDEPENDENT — none is a precondition for reading the
600+
# next — which is what makes collecting correct HERE and
601+
# abort-on-first-failure correct in this job's many
602+
# `<gate> --self-test` + `<gate>` steps, where the self-test IS the
603+
# precondition for trusting the run after it. The census behind that
604+
# distinction: of 200 `run:` steps in lint.yml + ci.yml, 21 hold two or
605+
# more substantive commands, and exactly two were independent self-tests
606+
# sequenced together — this step and the `scripts/` shared-module step
607+
# above. Every other one is a precondition or a dependency, where the
608+
# abort is the correct semantics.
609+
#
610+
# ⛔ Not one step per self-test: a plain step split does not fix this at
611+
# all — Actions skips a job's remaining steps once a step fails, so the
612+
# mask survives the split verbatim. Restoring it would take an `if:` on
613+
# each gate step, and a condition is a way for a PR to arrange that a gate
614+
# does not run on it (the reason the required-context pin step carries
615+
# none). Both gates that read step structure were checked and would
616+
# TOLERATE a split — `check-shard-attestation` scans ci.yml only, and
617+
# `check-required-contexts` pins job-level properties plus the one
618+
# `check:required-contexts` step — so this is a choice on the merits,
619+
# not a constraint.
552620
- name: Shallow-history guard self-tests
553621
run: |
554-
node scripts/pm/git-history.mjs --self-test
555-
node scripts/check-engine-split-ratio.mjs --self-test
556-
bash scripts/collect-release-notes.sh --self-test
622+
# Tolerate-and-collect (#10814) — see the note above this step. Each
623+
# self-test runs unconditionally and prints its own verdict; the step
624+
# still FAILS when any of them does, naming every one that failed.
625+
# ⛔ Never let the collector swallow the exit code — a green step over
626+
# a red self-test looks identical to success. Both directions are pinned
627+
# by `node scripts/check-step-collectors.mjs --self-test`, which extracts
628+
# THIS block from THIS file and drives it under `bash -e` with stubs.
629+
failed=""
630+
run_self_test() {
631+
echo "-- $*"
632+
if "$@"; then
633+
echo "PASS $*"
634+
else
635+
echo "FAIL $*"
636+
failed="${failed} $*"$'\n'
637+
fi
638+
return 0
639+
}
640+
run_self_test node scripts/pm/git-history.mjs --self-test
641+
run_self_test node scripts/check-engine-split-ratio.mjs --self-test
642+
run_self_test bash scripts/collect-release-notes.sh --self-test
643+
if [ -n "$failed" ]; then
644+
echo ""
645+
echo "Shallow-history guard self-tests — the following FAILED:"
646+
printf "%s" "$failed"
647+
exit 1
648+
fi
649+
echo "Shallow-history guard self-tests — all three ran and passed"
650+
651+
# Step-collector gate (#10814) — the guard over the two steps above, and
652+
# over any step that grows their shape later. It has a static half and a
653+
# dynamic half, and the dynamic one is the load-bearing part:
654+
#
655+
# STATIC: one `run:` block invoking `--self-test` on TWO OR MORE DISTINCT
656+
# scripts must route them through a collector. Distinct scripts testing
657+
# themselves are independent by construction, so there is no reading under
658+
# which a failure in one should skip the others. Deliberately narrow: it
659+
# does NOT flag `<gate> --self-test` + `<gate>`, this job's dominant shape,
660+
# where the abort IS the point (a checker whose self-test failed has no
661+
# verdict worth printing), nor ci.yml's `mkdir -p` / `psql ALTER SYSTEM`
662+
# dependency sequences. Swept when it was written: 343 `run:` steps across
663+
# 26 workflows, exactly 2 matched, both of them above.
664+
#
665+
# DYNAMIC: nothing static can tell a collector that PROPAGATES the exit
666+
# code from one that swallows it, and the swallowing kind is worse than
667+
# the masking it replaces — a green step over a red self-test, which from
668+
# outside is indistinguishable from success. So `--self-test` extracts each
669+
# live block out of THIS file and runs it as `bash -e <file>` — the same
670+
# invocation Actions uses — against stubs with controlled exit codes, and
671+
# reads "did this command run" from the STUB's own side effect rather than
672+
# from the block's output, so the block cannot vouch for itself. Both
673+
# directions are pinned in every position, and the same command list is
674+
# also driven through the PRE-FIX bare sequence, which must mask — 1 of 3
675+
# commands executing when the first fails, 3 of 3 when none does. A harness
676+
# that cannot reproduce the defect cannot certify the fix.
677+
#
678+
# Invoked as `node scripts/…` rather than through a `pnpm check:*` alias,
679+
# on the precedent this job already sets: several gate steps here are
680+
# invoked directly, and dispatch-gates.mjs derives gate families from
681+
# either spelling, so the direct form loses no discovery and adds no key
682+
# to the root manifest.
683+
#
684+
# ⚠️ NOT because root package.json is off limits. The #9465 changeset lane
685+
# fences that file's `@changesets/cli` range and its `version` script — the
686+
# parenthetical in the issue body is scoping, not illustrative — and not
687+
# the file, so a `check:step-collectors` key would have been allowed. Said
688+
# plainly because the over-broad reading is easy to acquire and then
689+
# propagates as a constraint nobody actually has, which is this step's own
690+
# defect class wearing a different hat: a claim that reads as verified.
691+
# Temp-dir fixtures, no network, ~1 s.
692+
- name: Step-collector gate (self-tests that mask each other)
693+
run: |
694+
node scripts/check-step-collectors.mjs --self-test
695+
node scripts/check-step-collectors.mjs
557696
558697
# Verify-lock entry-point self-test (#9661). `scripts/pm/os-verify-lock.sh`
559698
# is the ONE way an agent takes the container's shared heavy-verify lock,

content/docs/references/api/protocol.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1248,7 +1248,7 @@ List packages response
12481248
| **publishedCount** | `integer` || Number of drafts promoted to active — `published.length`. 0 on every refusal path (the batch is all-or-nothing, ADR-0067 D2). |
12491249
| **failedCount** | `integer` || Number of items that did not publish — `failed.length`. On a rollback this counts the WHOLE batch: the causal item plus every sibling marked BATCH_ABORTED. |
12501250
| **published** | `{ type: string; name: string; version: string; advisories?: object[] }[]` || Every draft promoted to active, in publish order. Empty on every refusal path. |
1251-
| **failed** | `{ type: string; name: string; error: string; code?: string }[]` || Items that did not publish. Because the batch is all-or-nothing (ADR-0067 D2), a non-empty list means NOTHING landed: `published: []`, `publishedCount: 0`. |
1251+
| **failed** | `{ type: string; name: string; error: string; code?: string; … }[]` || Items that did not publish. Because the batch is all-or-nothing (ADR-0067 D2), a non-empty list means NOTHING landed: `published: []`, `publishedCount: 0`. |
12521252
| **seedApplied** | `{ success: boolean; inserted?: integer; updated?: integer; error?: string; … }` | optional | Aggregate outcome of materializing EVERY published `seed` body in one multi-pass loader run (cross-seed references need the whole set). Present ONLY when the batch published at least one seed. Two producers, one key: the batch itself self-applies (`applySeedBodies`), and the REST door back-fills the same key for custom protocols that do not — never both (an externalId-less seed would double-insert). Best-effort: a seed problem is surfaced here, never thrown. |
12531253
| **materializeApplied** | `{ success: boolean; inserted: integer; updated: integer; failures: object[] }` | optional | ADR-0086 P2 — aggregate result of publish-time materializers across the batch (e.g. `permission``sys_permission_set`), including side-effect failures surfaced by the per-item effects loop. Present ONLY when at least one published item had a registered materializer or a side-effect failure. Best-effort, same contract as `seedApplied`. |
12541254
| **probes** | `any` | optional | ADR-0038 L3 post-publish runtime probe report — one real read per published artifact (seeded objects have rows, views are readable, widget dataset selections execute). DELIBERATELY OPAQUE in this contract (#9406): the key is declared and carried through verbatim, but its inner shape is intentionally not modeled until a consumer needs a field of it. Present only when something was publishable; probes never fail the publish. |

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ directory rather than per file.
257257
| Dir | Sites |
258258
|---|---|
259259
| `ai/` | 77 |
260-
| `api/` | 406 |
260+
| `api/` | 407 |
261261
| `cloud/` | 83 |
262262
| `identity/` | 32 |
263263
| `integration/` | 10 |

packages/adapters/hono/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ app.use('/api/*', objectStackMiddleware(kernel));
6666
## Edge runtime notes
6767

6868
- Hono adapter is the **preferred** adapter for Cloudflare Workers, Deno Deploy, Bun, and Vercel Edge.
69-
- Drivers differ by runtime: use [`@objectstack/driver-sql`](../../plugins/driver-sql) on Node. (Edge/multi-tenant turso driver ships in ObjectStack Cloud.)
69+
- Drivers differ by runtime: use [`@objectstack/driver-sql`](../../drivers/driver-sql) on Node. (Edge/multi-tenant turso driver ships in ObjectStack Cloud.)
7070
- Persist no long-lived state in module scope beyond the `kernel` instance.
7171

7272
## When to use

packages/metadata-protocol/src/protocol-publish-drafts-closure.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,14 @@ describe('publishPackageDrafts judges each draft against the BATCH closure (#103
380380
const causal = res.failed.find((f) => f.name === 'customer_dashboard')!;
381381
expect(causal.code).toBe('INVALID_METADATA');
382382
expect(causal.error).toMatch(/widget-dataset-unknown/);
383-
expect(causal.error).toMatch(/no_such_dataset_xyz/);
383+
// [#10524] `error` is a headline now (path + rule locators); the
384+
// dataset NAME lives in the finding's message, once, on the
385+
// structured channel the batch response declares (`failed[].issues`).
386+
const unknownDs = (causal as any).issues.find(
387+
(i: any) => i.rule === 'widget-dataset-unknown',
388+
);
389+
expect(unknownDs.message).toMatch(/no_such_dataset_xyz/);
390+
expect(causal.error).not.toContain(unknownDs.message);
384391
// ADR-0067 D2 — all-or-nothing: the healthy sibling is aborted, not
385392
// published around the refusal.
386393
expect(res.failed.find((f) => f.name === 'shyx_customer_ds')?.code).toBe('BATCH_ABORTED');

packages/metadata-protocol/src/protocol.batch-verb-driver-code.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,16 +441,20 @@ describe('[#8441] [GUARD] the Studio publish surface still gets the code it bran
441441
// WHICH DRAFT.
442442
expect(failure.type).toBe('flow');
443443
expect(failure.name).toBe('leave_approval');
444-
// WHICH FIELD — the located path, in the human sentence (#8333's half).
444+
// WHICH FIELD — the located path, in the human sentence (#8333's
445+
// half). [#10524] The sentence is a HEADLINE: path and rule id stay
446+
// in it, and the message prose lives once, in `issues[]` below.
445447
expect(failure.error).toContain('flows[0].nodes[1].config.approvers[0].value');
446-
expect(failure.error).toContain('does not parse as CEL');
448+
expect(failure.error).toContain('[approval-expression-invalid]');
449+
expect(failure.error).not.toContain('does not parse as CEL');
447450
// …and the machine-readable halves the form highlights with. `code` is
448451
// THIS card's field: catalogued, so it passes through byte for byte.
449452
expect(failure.code).toBe('INVALID_METADATA');
450453
expectCataloged(failure.code);
451454
expect(Array.isArray(failure.issues)).toBe(true);
452455
expect(failure.issues[0].path).toBe('flows[0].nodes[1].config.approvers[0].value');
453456
expect(failure.issues[0].rule).toBe('approval-expression-invalid');
457+
expect(failure.issues[0].message).toMatch(/does not parse as CEL/);
454458
});
455459
});
456460

0 commit comments

Comments
 (0)