You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(adr): migrate rotted ADR line anchors to resolver-gated symbol anchors (#13556) (#14277)
* docs(adr): migrate rotted line anchors to resolver-gated symbol anchors (#13556)
The #13556 census enumerated every line anchor in `docs/adr/**` — 343 across
35 of the 134 records, plus 52 continuation anchors — and resolved each against
main. Excluding 4 HISTORICAL and 2 EXTERNAL, 243 of 337 live anchors were
broken: 72.1%, declared as a one-way LOWER bound. Rot tracked target-file CHURN
rather than record age: the four hottest targets were 100% broken across 50
anchors. A rotted line anchor does not fail — it silently points a reader at
unrelated code inside the document whose purpose is to be a durable record.
Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the corpus to
symbol anchors behind a resolver gate that reds on an absent symbol, in ONE
migration with no transition period, sharing a single resolver with #13788.
- `scripts/symbol-anchors.mjs` — the SHARED core: grammar, extractor and the
resolution rule. A corpus is a `defineCorpus` registration, so #13788 joins
by registering rather than by forking the resolver.
- `scripts/check-adr-symbol-anchors.mjs` — the `docs/adr/**` registration.
- 469 occurrences rewritten across 35 records: 188 became symbol anchors (173
resolved at a declaration site, 15 as a quoted data identifier), 211 dropped
to file level, 15 to cross-repo file level, 10 dead paths lost their number,
24 duplicate or orphaned continuations were removed, and 6 HISTORICAL /
EXTERNAL anchors carry an inline exemption marker.
Resolution is deliberately stricter than the census: comments are stripped
before matching, so a symbol named only in a comment does not resolve, and a
token that merely repeats the anchor's own path is refused. That strictness is
what confirms 72.1% was a floor — `object.zod.ts` was credited with
`stateMachines` by the census, and the only occurrence today is a comment
saying that map no longer exists.
Nine spellings of a line reference are recognised, three of which the census's
own extractor could not see: comma and slash lists, the trailing `+` form, bare
anchors in running prose, fenced comment headers, and the tilde form.
⛔ No ADR's decision content changes; only anchor form does. ADR-0113's Context
row is known to state the inverse of today's mechanism — carded as #14193,
deliberately NOT repaired here, with a note in the record pointing at it.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
* ci(lint): keep the ADR symbol-anchor sweep out of its collector block
`check-step-collectors --self-test` was red on this branch:
• lint.yml "ADR symbol anchors resolve (no line numbers survive)":
all green => every command runs (3/2)
The counts are (executed / collected), so three ran where two were collected —
not the reverse. That gate drives the real `run:` block under `bash -e` with a
STUB planted at each collected command's SCRIPT PATH, then holds "all green =>
every command runs" as an equality. The step ended with a bare
`node scripts/check-adr-symbol-anchors.mjs` after the collector's exit guard,
which re-invokes the very stub planted for that script's `--self-test` entry —
a third execution against two collected commands.
Split into two steps: the collector carries the two self-tests and nothing
else, and the production sweep is its own step. That is also the shape both
existing collectors in this file already have, and it restores the property
the split was for — the sweep now runs on its own footing instead of behind
the self-tests' `exit 1`.
⛔ No scope change: `docs/adr/**`, the resolver and its corpus registration are
untouched. Both invocations the gate's own `--self-test` pins in lint.yml are
still present, so the wiring assertion still holds.
Refs #13556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mszp61SetNhQfVGvWh9bNn
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/adr/0005-metadata-customization-overlay.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -77,8 +77,8 @@ Studio ships an in-browser editor for views and dashboards. Customers expect "Sa
77
77
Until today, the runtime did not:
78
78
79
79
1.`PUT /api/v1/meta/view/<name>` in **project-kernel mode** updated only the in-memory registry and returned `200 { success: true, message: "Saved to memory registry (project kernel — sys_metadata is control-plane only)" }`. The change vanished on restart.
80
-
2.`GET /api/v1/meta/view/<name>` skipped `sys_metadata` entirely in project-kernel mode (`packages/objectql/src/protocol.ts:357`, `:369`).
81
-
3.`loadMetaFromDb()` returned `{ loaded: 0 }` early in project-kernel mode (`:1230`), so even if rows existed they would never be hydrated.
80
+
2.`GET /api/v1/meta/view/<name>` skipped `sys_metadata` entirely in project-kernel mode (`packages/metadata-protocol/src/protocol.ts#sys_metadata`).
81
+
3.`loadMetaFromDb()` returned `{ loaded: 0 }` early in project-kernel mode (`#loadMetaFromDb`), so even if rows existed they would never be hydrated.
82
82
83
83
Worse, a separate detour — Studio's "Duplicate View" calls `POST /api/v1/data/sys_view` — wrote rows to a **physical projection table** (`sys_view`, 21 flat columns) that has nothing to do with the metadata protocol path. The same applies to `sys_flow`, `sys_agent`, `sys_tool`, `sys_object`. Each of these tables duplicates a Zod schema already defined in `@objectstack/spec` (`ui/view.zod.ts`, `automation/flow.zod.ts`, etc.) and goes out of sync the moment the spec evolves.
Copy file name to clipboardExpand all lines: docs/adr/0019-approval-as-flow-node.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -27,7 +27,7 @@ The platform is not launched; there is no production approval data and no legacy
27
27
ADR-0018 §Context argued — correctly — that *multiple authoring paradigms are fine; multiple execution vocabularies are not*. Approval is where that line is currently crossed at the **engine** level, not just the vocabulary level:
28
28
29
29
-`@objectstack/plugin-approvals` is ~1500 LOC of runtime: an 816-line `approval-service.ts` state machine, a **313-line parallel `action-executor.ts`**, 250-line lifecycle hooks, and a 128-line plugin.
30
-
- The contract is explicit that this is a separate engine: [`spec/contracts/approval-service.ts:11`](../../packages/spec/src/contracts/approval-service.ts#L11) — *"Sits on top of (but does not depend on) `IWorkflowService` … driven by humans rather than transition rules."*
30
+
- The contract is explicit that this is a separate engine: [`packages/spec/src/contracts/approval-service.ts`](../../packages/spec/src/contracts/approval-service.ts) — *"Sits on top of (but does not depend on) `IWorkflowService` … driven by humans rather than transition rules."*
31
31
- The parallel `action-executor.ts` re-implements `field_update` / `inbox_notify` / `webhook` and carries the **same**`connector_action` / `script` / `email_alert` "unimplemented, logged + skipped" stubs that ADR-0018 set out to retire.
32
32
- It has its **own** ADR-0009 execution pinning (`process_hash` → `getByHash`), parallel to Flow's.
33
33
- It registers its own lifecycle hooks: `afterInsert` auto-trigger, `beforeUpdate` record-lock ([`plugin-approvals/src/lifecycle-hooks.ts`](../../packages/plugins/plugin-approvals/src/lifecycle-hooks.ts)).
@@ -68,7 +68,7 @@ There is **one** execution loop: the Flow engine. The engine core owns a generic
68
68
The Approval node is registered through the **ADR-0018 open registry** (`registerNodeExecutor`), by a slimmed-down approval plugin — **not** baked into `service-automation` core. Rationale:
69
69
70
70
- It is the ADR-0018 thesis applied to ourselves: the engine is the substrate, capabilities are contributed nodes.
71
-
-**Layering.** Approver resolution depends on the org / sharing model — `sys_team`, `sys_department` (recursive BFS), `sys_user.manager_id`, `sys_department_member` ([`plugin-approvals/src/approval-service.ts:175`](../../packages/plugins/plugin-approvals/src/approval-service.ts#L175)). The Flow engine core must **not** depend on the org model; the approval plugin may. So approval cannot live in core.
71
+
-**Layering.** Approver resolution depends on the org / sharing model — `sys_team`, `sys_department` (recursive BFS), `sys_user.manager_id`, `sys_department_member` ([`packages/plugins/plugin-approvals/src/approval-service.ts#sys_team`](../../packages/plugins/plugin-approvals/src/approval-service.ts)). The Flow engine core must **not** depend on the org model; the approval plugin may. So approval cannot live in core.
72
72
-`service-automation` stays lean; approval becomes a well-behaved node provider that rides the engine instead of a parallel engine.
73
73
74
74
### D3 — Deprecate `ApprovalProcessSchema` as a top-level authoring type; re-home its concepts
**Zero enforcement — verified across `packages/{runtime,objectql,services,core,metadata*,plugins}` and the whole repo:**
42
42
43
-
-`IWorkflowService` (`packages/spec/src/contracts/workflow-service.ts:58` — unlinked: the contract file was deleted on 2026-08-01 by #4451 / #4473, which retired the `workflow` service slot outright, closing the follow-up this record left open below) has **no concrete implementation**.
43
+
-`IWorkflowService` (`packages/spec/src/contracts/workflow-service.ts:58`<!-- anchor-exempt: HISTORICAL -->— unlinked: the contract file was deleted on 2026-08-01 by #4451 / #4473, which retired the `workflow` service slot outright, closing the follow-up this record left open below) has **no concrete implementation**.
44
44
- There is **no XState interpreter** anywhere (no `createMachine` / `interpret` / transition engine).
45
45
- The write-path validator [`validateRecord`](../../packages/objectql/src/validation/record-validator.ts#L198) reads only `objectSchema.fields` and validates **field data types** (string/number/date/…). It **never reads `objectSchema.validations`** at all — so *not one* of the nine validation-rule types (`state_machine`, `cross_field`, `script`, `unique`, `format`, `json_schema`, `async`, `custom`, `conditional`) is enforced by it.
46
46
-**Nothing reads `object.stateMachines`.**
47
47
48
-
So the guardrail goal is currently unmet at runtime. The only artefacts that exist are declarations — e.g. `examples/app-crm/src/workflows/stale-opportunity.workflow.ts:19` (`StateMachineConfig`; unlinked — this file describes the pre-ADR state and was itself removed by this record's own implementation, see the checklist below), which additionally **mixes orchestration into the machine** (it carries `email_alert` / `task_creation` actions that no engine executes — that orchestration belongs to a record-triggered Flow per ADR-0019).
48
+
So the guardrail goal is currently unmet at runtime. The only artefacts that exist are declarations — e.g. `examples/app-crm/src/workflows/stale-opportunity.workflow.ts:19`<!-- anchor-exempt: HISTORICAL -->(`StateMachineConfig`; unlinked — this file describes the pre-ADR state and was itself removed by this record's own implementation, see the checklist below), which additionally **mixes orchestration into the machine** (it carries `email_alert` / `task_creation` actions that no engine executes — that orchestration belongs to a record-triggered Flow per ADR-0019).
49
49
50
50
#### The prior-state plumbing gap (the real implementation constraint)
51
51
52
-
A transition check needs **both** the prior and the new state. But the write path can't supply the prior state today: on update, [`engine.ts:1850`](../../packages/objectql/src/engine.ts#L1850) calls `validateRecord(schema, hookContext.input.data, 'update')` — passing only the **PATCH payload**, not the prior record. On `PATCH { status: 'done' }` there is no way to know the *from*-state without a read. So enforcing `state_machine` is not just "add a dispatch branch"; it requires **plumbing the prior (or merged) record into the rule-evaluation step**. This is a shared need: `cross_field` and `script` rules are equally crippled by receiving only the patch — so the fix should land **once for the whole `validations` union**, not as a `state_machine`-only patch (see D3).
52
+
A transition check needs **both** the prior and the new state. But the write path can't supply the prior state today: on update, [`packages/objectql/src/engine.ts#validateRecord`](../../packages/objectql/src/engine.ts) calls `validateRecord(schema, hookContext.input.data, 'update')` — passing only the **PATCH payload**, not the prior record. On `PATCH { status: 'done' }` there is no way to know the *from*-state without a read. So enforcing `state_machine` is not just "add a dispatch branch"; it requires **plumbing the prior (or merged) record into the rule-evaluation step**. This is a shared need: `cross_field` and `script` rules are equally crippled by receiving only the patch — so the fix should land **once for the whole `validations` union**, not as a `state_machine`-only patch (see D3).
53
53
54
54
### The design-center shift: AI is the author — optimise naming for the model's priors
55
55
@@ -84,7 +84,7 @@ The introspectability is the upgrade that serves the two design centers: **UI**
84
84
85
85
### Where it lives: one of nine validation-rule types
86
86
87
-
`state_machine` is one variant of the `ValidationRuleSchema` discriminated union ([`validation.zod.ts:362`](../../packages/spec/src/data/validation.zod.ts#L362)), alongside `script`, `unique`, `format`, `cross_field`, `json_schema`, `async`, `custom`, and `conditional`. It shares `BaseValidationSchema` (name/label/message/severity) and the same write-time enforcement semantics as its siblings. This is *why it stays in `validations`* (D1) rather than becoming a standalone metadata type or file: it is, precisely, a write-time validation whose payload happens to be a transition graph.
87
+
`state_machine` is one variant of the `ValidationRuleSchema` discriminated union ([`packages/spec/src/data/validation.zod.ts#ValidationRuleSchema`](../../packages/spec/src/data/validation.zod.ts)), alongside `script`, `unique`, `format`, `cross_field`, `json_schema`, `async`, `custom`, and `conditional`. It shares `BaseValidationSchema` (name/label/message/severity) and the same write-time enforcement semantics as its siblings. This is *why it stays in `validations`* (D1) rather than becoming a standalone metadata type or file: it is, precisely, a write-time validation whose payload happens to be a transition graph.
0 commit comments