Skip to content

Commit 3957abd

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15068-seed-read-dead-org-rung
2 parents 265409a + 3d3f60e commit 3957abd

54 files changed

Lines changed: 4962 additions & 309 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): retire the fourteen inert deadline keys of the incident-response, training and change-management schemas (#14477, ADR-0049)
6+
7+
<!-- adr-0087: registered incident-response-deadline-keys-retired, training-deadline-keys-retired, change-management-duration-keys-retired -->
8+
9+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
10+
launch-window convention ships it as `minor`; the migration prescriptions are
11+
registered under protocol major 18, where `os migrate meta` users will look).
12+
Maintainer ruling 2026-09-02 on the census card (ruled A: retire per family):
13+
ADR-0049 enforce-or-remove decides it — declared-but-unenforced deadline
14+
surface with zero measured readers comes off.
15+
16+
Fourteen hour/minute/day-shaped deadline, SLA and duration key sites — twelve
17+
distinct names, because `durationMinutes` and `estimatedMinutes` each occur at
18+
two sites — sat on the exported incident-response, training and
19+
change-management schemas and in the generated reference docs, and **nothing
20+
read them**: the schemas are exported from `@objectstack/spec/system`, mounted
21+
by no stack key, registered as no metadata type, absent from the 2026-06
22+
liveness ledgers, and the reader census over every package outside
23+
`packages/spec` (tests and changelogs excluded) and over objectui at the
24+
pinned sha returned zero hits for every key. An author could write
25+
`triageDeadlineHours: 4`, `validityDays: 365` or `regulatorDeadlineHours: 72`
26+
and reasonably expect the platform to escalate, expire or notify — it never
27+
did, and it never said so. Six of the keys carried defaults (30 minutes,
28+
1 hour, 2555 days; 365, 30 and 14 days) that were materialized into every
29+
parsed document without ever being consulted. A compliance-shaped deadline
30+
that fails silently is the worst form of the shape ADR-0049 names.
31+
32+
**What is refused:** authoring any of the keys below, with any value, on the
33+
base schema and through every carrier that nests it (`Incident.responsePhases[]`,
34+
`IncidentResponsePolicy.notificationMatrix`, `TrainingPlan.courses[]`,
35+
`ChangeRequest.impact` / `.rollbackPlan` / `.implementation`). None of the
36+
schemas is `.strict()`, so each key is a `retiredKey()` tombstone rather than a
37+
bare deletion (a deletion would have stripped it in silence): authoring it is a
38+
`tsc` error (`never`) and a parse error carrying the prescription
39+
(`invalid_type` at the path of the key).
40+
41+
| schema | retired keys |
42+
|:--|:--|
43+
| `IncidentResponsePhase` | `targetHours` |
44+
| `IncidentNotificationRule` | `withinMinutes`, `regulatorDeadlineHours` |
45+
| `IncidentNotificationMatrix` | `escalationTimeoutMinutes` (default 30) |
46+
| `IncidentResponsePolicy` | `triageDeadlineHours` (default 1), `retentionDays` (default 2555) |
47+
| `TrainingCourse` | `durationMinutes`, `validityDays` |
48+
| `TrainingPlan` | `recertificationIntervalDays` (default 365), `gracePeriodDays` (default 30), `reminderDaysBefore` (default 14) |
49+
| `ChangeImpact` | `downtime.durationMinutes` |
50+
| `RollbackPlan` | `steps[].estimatedMinutes` |
51+
| `ChangeRequest` | `implementation.steps[].estimatedMinutes` |
52+
53+
**What stays, byte-identical:** every other key of the three families with its
54+
default and its (absent) readers, and every export — no def leaves the public
55+
surface. Parsed documents no longer carry the six former defaults.
56+
57+
**Held, not touched:** the `ESignatureConfig` pair (`expirationDays`,
58+
`reminderDays` in `data/document.zod.ts`) — the ruling left that branch open
59+
pending the e-signature roadmap answer; it stays on the card.
60+
61+
## FROM → TO
62+
63+
```ts
64+
// before — parsed green; no engine ever read a single one of these numbers
65+
const policy: IncidentResponsePolicy = {
66+
notificationMatrix: {
67+
rules: [{ severity: 'critical', channels: ['pagerduty'], recipients: ['security_team'],
68+
withinMinutes: 15, notifyRegulators: true, regulatorDeadlineHours: 72 }],
69+
escalationTimeoutMinutes: 45,
70+
},
71+
defaultResponseTeam: 'security_team',
72+
triageDeadlineHours: 2,
73+
retentionDays: 3650,
74+
};
75+
const course: TrainingCourse = {
76+
id: 'COURSE-SEC-001', title: 'Security Fundamentals', description: '',
77+
category: 'security_awareness', targetRoles: ['all_employees'],
78+
durationMinutes: 60, validityDays: 365,
79+
};
80+
const rollback: RollbackPlan = {
81+
description: 'Restore from backup',
82+
steps: [{ order: 1, description: 'Restore backup', estimatedMinutes: 15 }],
83+
};
84+
85+
// after — delete the keys; there is no replacement because no incident-response,
86+
// training-management or change-management engine exists to keep a deadline.
87+
// Record retention is the object-level `lifecycle` block (ADR-0057), declared on
88+
// the object that stores the records.
89+
const policy: IncidentResponsePolicy = {
90+
notificationMatrix: {
91+
rules: [{ severity: 'critical', channels: ['pagerduty'], recipients: ['security_team'],
92+
notifyRegulators: true }],
93+
},
94+
defaultResponseTeam: 'security_team',
95+
};
96+
const course: TrainingCourse = {
97+
id: 'COURSE-SEC-001', title: 'Security Fundamentals', description: '',
98+
category: 'security_awareness', targetRoles: ['all_employees'],
99+
};
100+
const rollback: RollbackPlan = {
101+
description: 'Restore from backup',
102+
steps: [{ order: 1, description: 'Restore backup' }],
103+
};
104+
```
105+
106+
One-line fix: delete the key wherever it is authored. There is no
107+
`os migrate meta` edit list for these keys — none of the schemas is a stack
108+
collection member, so the conversion chain has no seam to walk (the
109+
`MetadataPluginConfig.additionalTypes` precedent); the tombstone prescription
110+
and the protocol-18 upgrade guide are the channels.
111+
112+
The retirement kit:
113+
114+
- `retiredKey()` tombstones at all fourteen sites (`packages/spec/src/system/
115+
incident-response.zod.ts`, `training.zod.ts`, `change-management.zod.ts`;
116+
each file's section comment records what the shape was and why no D2
117+
conversion exists)
118+
- ADR-0087 registration: fourteen `RETIRED_KEYS_BY_MAJOR[18]` entries (the
119+
three nested change-management sites spelled `ChangeImpact:downtime.durationMinutes`,
120+
`RollbackPlan:steps.estimatedMinutes`, `ChangeRequest:implementation.steps.estimatedMinutes`)
121+
and three D3 semantic entries, one per family
122+
- no liveness-ledger row: none of the three families is an enrolled ledger
123+
type, so there is no row to keep or drop
124+
- pin tests (`deadline-keys-retirement.test.ts`): a refusal pin per site
125+
asserting the issue path, code and prescription on the base schema and
126+
through the nesting carriers; the tsc `never` channel; no-materialize pins
127+
for the six former defaults; the ADR-0087 registration; and a tree-scoped
128+
absence pin over every authored source in the repo
129+
- generated baselines and docs follow the schema: `authorable-surface/` gains
130+
eleven `[RETIRED]` rows, `authorable-defaults/` loses six rows, the three
131+
system reference pages are regenerated, and the gitignored `json-schema/`
132+
output is re-emitted on the next build
133+
- `json-schema.manifest/` is unchanged, and correctly so: it ratchets def
134+
*names*, and retiring keys removes no def from the published surface
135+
- `spec-changes.json` and the protocol upgrade guide are unchanged too: both
136+
project the migration chain at the current protocol major (17), so these
137+
protocol-18 registrations reach them at the 18 cut
138+
- zero authored occurrences in this repo's examples, skills and hand-written
139+
docs, and zero hits in objectui at the pinned sha, so no in-repo source
140+
changes ride along beyond the three families' own unit tests
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/types": minor
3+
"@objectstack/plugin-approvals": minor
4+
"@objectstack/rest": minor
5+
---
6+
7+
An approval decision that lands while its flow run strands now says so in fields, not only in prose.
8+
9+
`POST /api/v1/approvals/requests/{id}/reject` — and its sibling decision doors — could produce three coexisting outcomes from one call: the caller read HTTP 500, the request row **was** in its terminal status and had left the pending inbox, and the workflow run was stranded. A caller reading 500 has one honest inference available — "the rejection did not happen" — and it was the wrong one, so scripts and operators retried or escalated against a decision that was already durable. The only carrier of the truth was English prose in `error`, so finding the affected run meant regexing a run id out of a sentence, and nothing said whether that run could be repaired at all.
10+
11+
The 500 stays. A recorded decision whose flow never advances is still a failure and is still reported as one; the door does not become atomic and no decision is ever rolled back. What changed is that it stops discarding what the engine already said:
12+
13+
- **The `RESUME_FAILED` body gains four fields**, additively — `finalized` (always `true`: the decision stands), `decision`, `runId`, and `repairable`. Existing consumers see the same `code`, the same `error` and the same status.
14+
- **`repairable` carries the engine's own discriminator**`AutomationResult.status === 'stranded'`, the state stamped on exactly the exit that journals a repair snapshot. `false` is the answer for every other failure, including a lost run: absence of the signal is not repairability, and a repair verb that would refuse is worse than no promise.
15+
- **`serviceResume` carries `status`** through to the door. It previously read only `success` / `code` / `error`, and the stranded exit reports a `status` and no `code` at all — so the platform's own repairability signal died one line before the envelope was built.
16+
17+
`@objectstack/types` gains `strandedDecisionFailure` / `strandedDecisionDetails` and the `StrandedDecisionDetails` type — the constructor and its recogniser in one module, so the producing service and the REST door cannot drift. A `RESUME_FAILED` raised without that carrier answers exactly the body it always did; the door never synthesises the envelope.

.github/workflows/lint.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3188,6 +3188,23 @@ jobs:
31883188
- name: Console-intercept disarm
31893189
run: node scripts/check-console-intercept-disarm.mjs --self-test && node scripts/check-console-intercept-disarm.mjs
31903190

3191+
# Declared registry log level (#15425, origin #13517). objectql's
3192+
# SchemaRegistry logs one `[Registry] Registered …` line per registered
3193+
# item per construction, at its shipped `info` default; a suite that boots
3194+
# app stacks pays items x boots of that on a shard log nobody reads.
3195+
# PR #13985 and PR #14016 declared `OS_REGISTRY_LOG` in four harnesses and
3196+
# removed ~47,900 lines — and nothing held them there, so deleting the
3197+
# line restored five figures of output with every suite still green. This
3198+
# asserts the declaration in every package whose OWN TEST SOURCES boot the
3199+
# engine, a DERIVED population (registry construction / `bootStack` /
3200+
# example-app import), not a roster: the tree's own audit
3201+
# (docs/audits/2026-09-test-log-volume-census.md) measures packages/rest at
3202+
# 528 residual `[Registry]` lines, so a roster of the four that already
3203+
# declared would have shipped a known-incomplete list. Self-test first,
3204+
# then the real scan; reads ~72 manifests and their test sources.
3205+
- name: Declared registry log level
3206+
run: node scripts/check-registry-log-declared.mjs --self-test && node scripts/check-registry-log-declared.mjs
3207+
31913208
# Live-server database isolation (#10382). CI provisions ONE Postgres and
31923209
# ONE MySQL for the whole temporal-conformance job and points every live
31933210
# leg at them, and every live suite in the repo issues a `drop` when it

content/docs/permissions/system-context.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ not on any flag.
6464
## How the flag is set
6565

6666
`isSystem` is **server-constructed and never client-supplied**. Inbound HTTP
67-
cannot set it (`packages/rest/src/rest-server.ts:1548`, `:1577`), and neither
67+
cannot set it (`packages/rest/src/rest-server.ts:1553`, `:1582`), and neither
6868
can an action body (`packages/runtime/src/domains/actions.ts:414`). It is
6969
written by internal callers only, as an option on the engine call:
7070

@@ -103,7 +103,7 @@ that silently does not happen.
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |
104104
| 15 | **Read-audit rows are not written** | plugin-audit | Lose: the "a person opened this record" trail. `sudo()` keeps the caller's `userId`, so this flag is the only thing separating a human read from a platform one | `read-audit.ts:556` |
105105
| 16 | Approval snapshot payload redaction skipped | plugin-approvals | Get: the whole snapshot on `find` / `findOne` — the audit/replay channel. Lose: field-visibility redaction over approval payloads | `payload-redaction-middleware.ts:115` |
106-
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1580` |
106+
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1585` |
107107

108108
### 2. Write pipeline and data integrity
109109

@@ -145,7 +145,7 @@ The largest single consumer — **17 of the 106 sites**.
145145
|:--|:---|:---|:---|:---|
146146
| 40 | **Approval record lock released** — a locked record is writable | plugin-approvals | Get: engine self-writes (the status mirror) pass. Lose: the lock that stops edits while an approval is live. Note there is deliberately **no admin exemption** here — only `isSystem` | `lifecycle-hooks.ts:347` |
147147
| 41 | Delegation write guard bypassed | plugin-approvals | Get: service / seed / import may write delegation rows naming another delegator | `lifecycle-hooks.ts:570` |
148-
| 42 | Approval actor / submitter / pending-approver checks bypassed (8 sites) | plugin-approvals | Get: approve, reject, recall, reassign without being a pending approver or the submitter | `plugin-approvals/src/approval-service.ts:963`, `:1072`, `:3196`, `:3342`, `:3509`, `:3580`, `:3769`, `:3809` |
148+
| 42 | Approval actor / submitter / pending-approver checks bypassed (8 sites) | plugin-approvals | Get: approve, reject, recall, reassign without being a pending approver or the submitter | `plugin-approvals/src/approval-service.ts:963`, `:1072`, `:3248`, `:3396`, `:3564`, `:3635`, `:3824`, `:3864` |
149149
| 43 | Saved-report ownership is **assignable**, and an update may reassign it | plugin-reports | Get: `ownerId` from input is honoured. A non-system caller always owns what it creates and can never reassign | `plugin-reports/src/report-service.ts:404`, `:425` |
150150
| 44 | Saved-report access / export / mutation gates bypassed | plugin-reports | Get: read, bulk-export and overwrite any report | `plugin-reports/src/report-service.ts:343`, `:372`, `:447`, `:684` |
151151
| 45 | Attachment access hooks return early (insert + update + delete, and the read AST) | service-storage | Lose: attachment visibility scoping | `attachment-access-hooks.ts:300`, `:349`, `:448`, `:524` |
@@ -158,7 +158,7 @@ The largest single consumer — **17 of the 106 sites**.
158158
|:--|:---|:---|:---|:---|
159159
| 48 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `action-execution.ts:138` |
160160
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` |
161-
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4967`, `:6381`, `:6629`, `:7060`, `:7253` |
161+
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4972`, `:6386`, `:6634`, `:7065`, `:7258` |
162162
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` |
163163
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:422`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
@@ -199,7 +199,7 @@ assuming `isSystem` covers it is a documented source of bugs.
199199
| "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1580` (#3493 / #6640) |
200200
| "It stamps `created_by`" | **No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error | `runtime-identity.ts:280``281` |
201201
| "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:299` |
202-
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1548`, `:1577`; `domains/actions.ts:414` |
202+
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1553`, `:1582`; `domains/actions.ts:414` |
203203

204204
---
205205

0 commit comments

Comments
 (0)