Skip to content

Commit 4692e33

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-7004-changeset-trailing-comment
2 parents f6b5800 + debe2f6 commit 4692e33

20 files changed

Lines changed: 944 additions & 543 deletions
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
'@objectstack/spec': major
3+
'@objectstack/metadata-protocol': major
4+
'@objectstack/objectql': major
5+
---
6+
7+
refactor(spec)!: `api` is code-only — withdraw a runtime create door the endpoint matcher could never read (#5488, ADR-0049 remove side)
8+
9+
<!-- adr-0087: registered api-runtime-create-withdrawn -->
10+
11+
**FROM → TO:** `PUT /api/v1/meta/api/{name}` (200 "Saved") → declare the endpoint as a
12+
stack artifact (`**/*.api.ts`, or `defineStack({ apis })`) and ship it through
13+
`publishPackage`. The runtime write now answers **403 `NOT_CREATABLE`**, in `?mode=draft`
14+
as well as direct-active. The artifact route is **unchanged** — a `**/*.api.ts` file valid
15+
before this release is valid after it, byte for byte.
16+
17+
`DEFAULT_METADATA_TYPE_REGISTRY`'s `api` entry declared `allowRuntimeCreate: true` and the
18+
runtime never honoured it. Measured on a real showcase boot (`objectstack dev --fresh`, 47
19+
plugins):
20+
21+
```
22+
PUT /api/v1/meta/api/e8_backdoor → 200 {"success":true,…,"message":"Saved …"}
23+
GET /api/v1/apps/showcase/backdoor → 404 (anonymous AND authenticated)
24+
```
25+
26+
…and **no** `[EndpointMatcher] … EXCLUDED` line anywhere in the boot log: the endpoint was
27+
not gated out, it was never in the index at all. The serving criterion belongs to
28+
`IMetadataService.matchEndpoint``EndpointMatcher``MetadataManager.listForIndex('api')`,
29+
which reads the manager's own registry plus its registered loaders
30+
(`["filesystem","memory"]` on dev/serve). A runtime write lands in `sys_metadata`, which is
31+
in neither. So the declaration promised a capability that could not exist.
32+
33+
A declared-but-unhonoured capability is ADR-0049 false compliance, and "answers Saved, then
34+
404s forever" is its most dangerous shape for the AI authors ADR-0033 targets. The
35+
maintainer ruled REMOVE on 2026-08-07 rather than converge the read path: making the matcher
36+
read `sys_metadata` re-opens cache, invalidation, tenancy and the ADR-0110 D3
37+
miss-vs-outage distinction on a new read path, and there is no business pull for
38+
Studio-authored endpoints today — 17.x serves declarative endpoints through stack artifacts,
39+
which is what showcase uses (#5040 E8, LIVE).
40+
41+
## The retirement kit
42+
43+
- **`allowRuntimeCreate: false`** on the `api` registry entry. With `allowOrgOverride`
44+
already `false`, the type is now **code-only** — the `job` / `agent` / `capability` shape —
45+
so the existing #5086 inlet refuses before persistence, on every kernel, with
46+
`code: 'NOT_CREATABLE'`, `status: 403` and a prescription derived from the entry's own
47+
`filePatterns[0]`. No new refusal mechanism was written for this.
48+
- **`gateApiDraftsForPublish` is retired** (`metadata-protocol`), together with its nine
49+
tests and the `PUBLISH_DRAFTS_NAMESPACE_REMEDY` string only it appended. It landed two
50+
days earlier in PR #5279 and is removed **deliberately and on the record**, not lost in a
51+
refactor: it gated a draft→active promotion into a state the matcher can never read, and
52+
with the inlet closed no `api` draft can exist for it to judge. The in-place comment at
53+
its old call site carries the reasoning.
54+
- **The `metadata-plugin.zod.ts` decision block is rewritten as a recorded overturn.** It
55+
used to record CODE-ONLY as "considered and rejected"; its three bullets are kept verbatim
56+
with what became of each, so the reversal is auditable rather than silently contradicted.
57+
- **The `api` create seed is removed** and `api` joins `KNOWN_UNSEEDED`. A pre-filled "New
58+
API Endpoint" form whose save can only 403 is the UI half of the same false compliance.
59+
- **Pins, not deletions.** The two #5271 tripwire pins that asserted
60+
`allowRuntimeCreate: true` are **replaced** by retirement pins asserting the new verdict —
61+
their comments predicted this exact consequence, and both predictions were correct. Every
62+
rejection case asserts `code` **and** `status` (ADR-0112 envelope), never `toThrow()`
63+
alone (#6142).
64+
65+
## What did NOT change
66+
67+
`validateApiEndpointDeclarations` / `identityFreeEndpointGateFailure` remain the one judge
68+
of what is servable, on the route that serves: the stack schema, `publishPackage` (#5189),
69+
and again at load in `buildEndpointIndex` (PR #5203). ADR-0121's "publish REJECTS" ruling is
70+
intact. `deleteMetaItem` stays ungated so pre-existing rows can be cleaned up, and
71+
`OS_METADATA_WRITABLE=api` remains the single operator escape hatch — note it unlocks the
72+
**write** only; the endpoint still will not be served, which is why it is a diagnostic
73+
rather than a workaround.
74+
75+
**Re-entry path**, recorded by the ruling: if #2657 Part B promotes `apis` to a registered
76+
type **with a real consumption path**, the flag and the publish gate come back together —
77+
implementation first, declaration second.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/example-todo": patch
3+
---
4+
5+
fix(example-todo): `task_completion` is a real record-change flow again — it bound to nothing and gated on a key nothing reads (#6882)
6+
7+
`examples/app-todo`'s `TaskCompletionFlow` declared `type: 'record_change'` and then
8+
declared neither key that arms one. It was 1 of the 34 authored flows across the three
9+
bundled apps, and the only dead one.
10+
11+
**Two faults on one start node, both silent.**
12+
13+
1. **No `triggerType` at all.** `AutomationEngine.resolveTriggerBinding` claims a
14+
record-change flow only when the authored token starts with `record-`. With the key
15+
absent every later branch missed too (`timeRelative`, `config.schedule`,
16+
`flow.type === 'schedule'`, `flow.type === 'api'`), the method returned `undefined`,
17+
and `activateFlowTrigger` returned without binding. The flow declared itself
18+
record-triggered and was, at runtime, a manual flow that never fired.
19+
2. **The predicate was written to `triggerCondition`.** The trigger gate is
20+
`config.condition` — the key the binding copies and `execute()` evaluates. A node
21+
`config` is an open slot by design (ADR-0018), so the misspelling parsed silently.
22+
Fixing (1) alone would have been *worse* than dead: the flow would have fired on every
23+
update of every task.
24+
25+
**Why no channel reported it.** `getTriggerBindingAudit` — the platform's own silent-miss
26+
surface, and the source for both the automation plugin's `kernel:bootstrapped` warn loop
27+
and the CLI startup summary's `unbound` list — opens with `if (!resolved) continue`,
28+
reading "no binding" as "manual/screen flow, nothing to bind". So the missing key did not
29+
*add* a diagnostic; it removed the flow from every diagnostic channel there is. The only
30+
trace anywhere was the startup banner counting one more flow registered than bound, with
31+
no name and no reason.
32+
33+
**The repair.** `triggerType: 'record-after-update'` plus the predicate moved to
34+
`config.condition` as `status == "completed" && previous.status != "completed"` — the
35+
shape `showcase_task_completed` already uses for this exact semantic. `-after-update`
36+
rather than `-after-write` on purpose: "marked as complete" is a transition, and the
37+
insert leg has no `previous` to transition from — `previous` binds to `null` there, and
38+
`previous.status` against `null` aborts the whole CEL predicate with `No such key:
39+
status` rather than answering false.
40+
41+
A third fault surfaced the moment the flow could run: `get_task` filtered on `{taskId}`,
42+
an `isInput` variable nothing ever bound (a record-change run seeds `params` from the
43+
triggering record, which carries `id`, not `taskId`), so the first armed run failed with
44+
"1 filter condition(s) resolved to nothing and were dropped from the query". It now reads
45+
`{record.id}`, the handle every other record-change flow in the corpus uses, and the dead
46+
declaration is gone.
47+
48+
`@objectstack/example-todo` also runs its own vitest suite now (`vitest run`, as
49+
`app-crm` and `app-showcase` already do) instead of `objectstack test`, which is a
50+
Quality-Protocol runner that needs a live server and matched no `qa/*.test.json` here —
51+
so the package's test files had never executed in CI.

0 commit comments

Comments
 (0)