Skip to content

Commit 09cc6be

Browse files
claude[bot]claude
andauthored
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>
1 parent 184f6b0 commit 09cc6be

39 files changed

Lines changed: 1435 additions & 406 deletions

File tree

.github/workflows/lint.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,6 +2154,74 @@ jobs:
21542154
- name: ADR anchors + number uniqueness (governed code names its decision)
21552155
run: pnpm check:adr-anchors
21562156

2157+
# #13556 ADR SYMBOL ANCHORS — the other direction of the same contract.
2158+
# `check:adr-anchors` above asks whether governed CODE still names its
2159+
# ADR; this asks whether an ADR's pointer INTO code still resolves.
2160+
#
2161+
# The census on #13556 enumerated every line anchor in docs/adr/** — 343
2162+
# across 35 records, plus 52 continuation anchors, a 395-anchor surface —
2163+
# and found 243 of 337 live anchors BROKEN: 72.1%, declared as a one-way
2164+
# LOWER bound because the census's own test accepted a symbol appearing
2165+
# in a comment. Rot tracked target-file CHURN, not ADR age: the four
2166+
# hottest targets were 100% broken across 50 anchors. A line number into
2167+
# a 16,000-line file has a lifetime measured in days, and it does not
2168+
# fail loudly — it silently points a reader at unrelated code.
2169+
#
2170+
# Maintainer ruling 2026-09-01 (总监批 #27), option A: migrate the whole
2171+
# corpus to symbol anchors with a resolver that reds on an absent symbol,
2172+
# in ONE migration with no transition period (「C 不作过渡」). That is why
2173+
# a surviving `path:NNN` is a hard finding rather than a warning here.
2174+
#
2175+
# ⭐ The mechanism is SHARED, by the same ruling (「共享同一个 resolver,
2176+
# ⛔ 不造第二套」): scripts/symbol-anchors.mjs holds the grammar, extractor
2177+
# and resolution rule, and this gate is a `defineCorpus` registration.
2178+
# #13788 (1,647 platform-checklist citations) joins by registering its own
2179+
# corpus — ⛔ not by forking the resolver.
2180+
#
2181+
# Invoked as `node` rather than a pnpm alias per the GATE INVOCATION IDIOM
2182+
# at the top of this file; the gate's own --self-test asserts BOTH lines
2183+
# below are present, so rewiring it silently reddens it.
2184+
#
2185+
# ⚠️ The two self-tests go through a `run_self_test` COLLECTOR, not a bare
2186+
# sequence. Under `bash -e` the first non-zero exit aborts the step, so a
2187+
# chained second self-test is neither green nor red — it never runs
2188+
# (#10814). The collector runs both unconditionally and fails at the end
2189+
# naming every one that broke.
2190+
#
2191+
# ⛔ AND THE PRODUCTION SWEEP IS A SEPARATE STEP, which is not cosmetic.
2192+
# `check-step-collectors` drives this very block under a real `bash -e`
2193+
# with a STUB planted at each collected command's script path, and holds
2194+
# "all green => every command runs" as an equality. A trailing
2195+
# `node scripts/check-adr-symbol-anchors.mjs` in the same block re-invokes
2196+
# that same stub, so the harness counts 3 executions against 2 collected
2197+
# commands and the gate reds (3/2) — which is exactly what it did here.
2198+
# Both existing collectors in this file carry self-tests ONLY, for the
2199+
# same reason. Keep the sweep out of the collector block.
2200+
- name: ADR symbol-anchor gate self-tests
2201+
run: |
2202+
failed=""
2203+
run_self_test() {
2204+
echo "-- $*"
2205+
if "$@"; then
2206+
echo "PASS $*"
2207+
else
2208+
echo "FAIL $*"
2209+
failed="${failed} $*"$'\n'
2210+
fi
2211+
return 0
2212+
}
2213+
run_self_test node scripts/symbol-anchors.mjs --self-test
2214+
run_self_test node scripts/check-adr-symbol-anchors.mjs --self-test
2215+
if [ -n "$failed" ]; then
2216+
echo ""
2217+
echo "ADR symbol-anchor self-tests — the following FAILED:"
2218+
printf "%s" "$failed"
2219+
exit 1
2220+
fi
2221+
2222+
- name: ADR symbol anchors resolve (no line numbers survive)
2223+
run: node scripts/check-adr-symbol-anchors.mjs
2224+
21572225
# NOTE: the standing platform test checklist (docs/qa/platform-checklist/)
21582226
# is validated by `pnpm check:platform-checklist`, but by MAINTAINER
21592227
# DECISION it is NOT wired into CI — it runs on a periodic manual cadence

docs/adr/0005-metadata-customization-overlay.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ Studio ships an in-browser editor for views and dashboards. Customers expect "Sa
7777
Until today, the runtime did not:
7878

7979
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.
8282

8383
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.
8484

@@ -330,7 +330,7 @@ Implementation (`packages/objectql/src/protocol.ts`):
330330
- `saveMetaItem` runs `safeParse`. On failure, throws an error with
331331
`code='invalid_metadata'`, `status=422`, and a structured `issues` array
332332
carrying `path/message/code` for each Zod issue. REST layer
333-
(`packages/rest/src/rest-server.ts:973-979`) already propagates `status`
333+
(`packages/rest/src/rest-server.ts`) already propagates `status`
334334
and `code` to the response.
335335
- The persisted document is the **original** `request.item`, NOT
336336
`parsed.data`. Studio attaches auxiliary fields (`isPinned`,

docs/adr/0015-external-datasource-federation.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The protocol already includes:
1616

1717
- `Datasource` (`packages/spec/src/data/datasource.zod.ts`) — connection
1818
+ driver + capabilities (transactions, queryFilters, readOnly, …).
19-
- `Object.datasource` (`packages/spec/src/data/object.zod.ts:432`) — per-object
19+
- `Object.datasource` (`packages/spec/src/data/object.zod.ts#datasource`) — per-object
2020
routing key, defaulting to `'default'`.
2121
- `ISchemaDiffService` (`packages/spec/src/contracts/schema-diff-service.ts`) —
2222
introspect → diff → migration plan.
@@ -386,8 +386,8 @@ if (datasource.schemaMode !== 'managed') {
386386
}
387387
```
388388

389-
**Concretely**`packages/drivers/driver-sql/src/sql-driver.ts:1064`
390-
and `:1084` (current `createTable` / `alterTable` call sites) gain a
389+
**Concretely**`packages/drivers/driver-sql/src/sql-driver.ts`
390+
and `packages/drivers/driver-sql/src/sql-driver.ts` (current `createTable` / `alterTable` call sites) gain a
391391
guard at the top. The `applyMigrations` implementation (forthcoming
392392
in `service-migration` per ADR-0008) also calls this guard.
393393

docs/adr/0016-studio-package-authoring-and-publish.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ authoring loop:
4242

4343
But the **authoring → publish closed loop is not yet implemented**. Today:
4444

45-
1. Studio create/edit (`ResourceEditPage.tsx:733`) calls
45+
1. Studio create/edit (`objectui:ResourceEditPage.tsx`) calls
4646
`client.save(type, name, item, { force, mode:'draft' })` with **no
4747
`packageId`**. Per ADR-0003 this produces a **runtime/overlay** row
4848
(`env_id` set, `package_id` NULL, loaded under sentinel `'sys_metadata'`,

docs/adr/0019-approval-as-flow-node.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The platform is not launched; there is no production approval data and no legacy
2727
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:
2828

2929
- `@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."*
3131
- 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.
3232
- It has its **own** ADR-0009 execution pinning (`process_hash``getByHash`), parallel to Flow's.
3333
- 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
6868
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:
6969

7070
- 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.
7272
- `service-automation` stays lean; approval becomes a well-behaved node provider that rides the engine instead of a parallel engine.
7373

7474
### D3 — Deprecate `ApprovalProcessSchema` as a top-level authoring type; re-home its concepts

docs/adr/0020-state-machine-converge-and-enforce.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,22 @@ The design intent is a **runtime guardrail**: declare which `status` transitions
3434

3535
| # | Where | Schema | Reference |
3636
|---|-------|--------|-----------|
37-
| 1 | Top-level `workflow` metadata type | `StateMachineSchema` (XState-style: hierarchical/parallel states, entry/exit actions, guards, context) | [`metadata-type-schemas.ts:85`](../../packages/spec/src/kernel/metadata-type-schemas.ts#L85), [`metadata-plugin.zod.ts:90`](../../packages/spec/src/kernel/metadata-plugin.zod.ts#L90), [`metadata-plugin.zod.ts:612`](../../packages/spec/src/kernel/metadata-plugin.zod.ts#L612) |
38-
| 2 | Object-embedded | `object.stateMachines: Record<string, StateMachineSchema>` ("parallel lifecycles: status, payment, approval") | [`object.zod.ts:534`](../../packages/spec/src/data/object.zod.ts#L534) |
39-
| 3 | Validation rule | `state_machine` rule: `transitions: { fromState: [toStates] }` | [`validation.zod.ts:105`](../../packages/spec/src/data/validation.zod.ts#L105) |
37+
| 1 | Top-level `workflow` metadata type | `StateMachineSchema` (XState-style: hierarchical/parallel states, entry/exit actions, guards, context) | [`packages/spec/src/kernel/metadata-type-schemas.ts`](../../packages/spec/src/kernel/metadata-type-schemas.ts), [`packages/spec/src/kernel/metadata-plugin.zod.ts`](../../packages/spec/src/kernel/metadata-plugin.zod.ts) |
38+
| 2 | Object-embedded | `object.stateMachines: Record<string, StateMachineSchema>` ("parallel lifecycles: status, payment, approval") | [`packages/spec/src/data/object.zod.ts`](../../packages/spec/src/data/object.zod.ts) |
39+
| 3 | Validation rule | `state_machine` rule: `transitions: { fromState: [toStates] }` | [`packages/spec/src/data/validation.zod.ts#transitions`](../../packages/spec/src/data/validation.zod.ts) |
4040

4141
**Zero enforcement — verified across `packages/{runtime,objectql,services,core,metadata*,plugins}` and the whole repo:**
4242

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**.
4444
- There is **no XState interpreter** anywhere (no `createMachine` / `interpret` / transition engine).
4545
- 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.
4646
- **Nothing reads `object.stateMachines`.**
4747

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).
4949

5050
#### The prior-state plumbing gap (the real implementation constraint)
5151

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).
5353

5454
### The design-center shift: AI is the author — optimise naming for the model's priors
5555

@@ -84,7 +84,7 @@ The introspectability is the upgrade that serves the two design centers: **UI**
8484

8585
### Where it lives: one of nine validation-rule types
8686

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.
8888

8989
## Decision
9090

0 commit comments

Comments
 (0)